DEV Community

Matt Kenefick
Matt Kenefick

Posted on

Writing a custom "pluckMultiple" function for Laravel

pluck is used to grab a single field quickly; like a list of email addresses.

If you need multiple fields, you could use select(...) and then run additional operations on it.

If you're comfortable with writing SQL, you could also run a raw query that looks like:

SELECT CONCAT(`fieldA`, `fieldB`) as `fieldName`
  FROM ...
Enter fullscreen mode Exit fullscreen mode

For Laravel, we can write a builder function that simplifies the process a bit and allows us to implode our results into a plucked list.

Let's say we want to get a type and slug glued by a hyphen, such that our results look like:

Array
(
    [0] => 1-acting
    [1] => 1-animation
    [2] => 1-cinematography
    [3] => 2-concept
    [4] => 2-directing
    [5] => 2-editing
    [6] => 3-music
    [7] => 3-none
    [8] => 3-production-design
)
Enter fullscreen mode Exit fullscreen mode

Our Laravel builder function looks like this (⚠️ There are multiple ways to do this; this is just one way.):

/**
 * Selects multiple columns and concatenates them using a specified glue
 *
 * @param array $fields
 * @param string $glue
 * @return BaseBuilder
 */
public function pluckMultiple(array $fields, string $glue = '-'): \Illuminate\Support\Collection
{
    return $this->select($fields)
        ->get()
        ->map(function($model, $key) use ($fields, $glue) {
            return implode($glue, array_values(array_intersect_key($model->toArray(), array_flip($fields))));
        });
}
Enter fullscreen mode Exit fullscreen mode

And we would execute that code like so:


$result = MyModel::pluckMultiple(['type', 'slug'], '-')->toArray();
// produces the above output
Enter fullscreen mode Exit fullscreen mode

What is the code doing?

This is a breakdown of the function with additional comments. See below the breakdown for notes about additional optimizations.

public function pluckMultiple(
    // Accept an array of fields, e.g. ['id', 'name']
    array $fields, 

    // Define the glue for implosion, could be empty ''
    string $glue = '-'

    // Return Laravel collection for chaining more methods
): \Illuminate\Support\Collection
{
    // Run basic SELECT `fieldA`, `fieldB`
    // ❗️ Might want to modify this to accept all fields
    //    so you can leverage cached queries.
    // ❗️ It's also possible to write the raw query we
    //    talked about before here
    return $this->select($fields)

        // Receive all results (❗️ This could get expensive)
        ->get()

        // Run operations on each row, pass in $fields + $glue
        // Our code above combines these steps into a single line
        ->map(function($model, $key) use ($fields, $glue) {
            // Get our model as basic array
            $arrayOfData = $model->toArray();

            // Flip our fields values into their keys position
            // e.g. ['id', 'slug'] becomes 
            //      ['id' => 0, 'slug' => 1]
            $reversedFields = array_flip($fields);

            // Find intersection of keys e.g. 'id' and 'slug'
            $intersection = array_intersect_key($arrayOfData, $reversedFields);

            // Get only values from resulting intersection
            // e.g. [1, 'my-slug']
            $values = array_values($intersection);

            // Combine values using glue
            // e.g. '1-my-slug'
            $gluedResult = implode($glue, $values);

            // Use this result as our collection item
            return $gluedResult;
        });
}
Enter fullscreen mode Exit fullscreen mode

Additional Optimizations

1.
By limiting our select(...) query to only the fields we want, we are now creating additional read queries for what might be a commonly used table.

If we may have already fetched this table before using SELECT *, then it'd likely be valuable to use the results from the previous execution here rather than creating a new transaction.

End of the day, look at your table indexes and what's been cached to get a feel for how to break that out.

2.
Using the get() + map() allows for maximum flexibility but it could be slow if you are dealing with large datasets. If you're intending to perform this method on a large dataset or if you think it'll be run frequently with dynamic data, it may be wise to put this logic directly into the SQL query itself rather than iterating through it.

AGAIN, this really depends on how your application is designed. If you pull the full table because you're going to use it anyway, then there's no reason not to re-use your memory.

The aforementioned SQL query could end up looking something like:

$fields = ['id', 'slug'];

$sql = sprintf("
    SELECT CONCAT(`%s`) as `concatenatedResult`
    FROM `table`
    LIMIT 9999
", implode('`, `', $fields));

// Prints: 
//    SELECT CONCAT(`id`, `slug`) as `concatenatedResult`
//    FROM `table`
//    LIMIT 9999
Enter fullscreen mode Exit fullscreen mode

Conclusion

There is more than one way to do this as pointed out and there are additional optimizations for such a technique that would depend on how your application is structured. Regardless, this is a quick and easy method for pluckMethod in a Laravel environment.

Top comments (0)