Oussama GHAIEB

Tips, tricks, and code snippets for developers

From Arrays to Collections: Building a Laravel-Style Wrapper in PHP

Laravel's Collections are one of the most powerful and expressive features of the framework. They provide a fluent, object-oriented way to work with arrays of data, offering a variety of helpful methods for filtering, transforming, and manipulating data. If you've ever wondered how to create a similar class that converts a PHP array into a Laravel-like collection, this blog post is for you.

In this tutorial, we'll walk through the process of building a simple Collection class in PHP that mimics some of the functionality of Laravel's Collections. By the end, you'll have a better understanding of how Laravel's Collections work under the hood and how you can implement similar functionality in your own projects—complete with method chaining for a fluent and expressive API.


What Are Laravel Collections?

Laravel Collections are essentially an object-oriented wrapper for arrays. They provide a wide range of methods for performing common operations on arrays, such as map, filter, reduce, pluck, and values. These methods allow you to write more expressive and readable code when dealing with arrays of data.

For example, instead of writing a foreach loop to filter an array, you can use the filter method:

$collection = collect([1, 2, 3, 4, 5]);

$filtered = $collection->filter(function ($value, $key) {
    return $value > 2;
});

// Result: [3, 4, 5]

Our goal is to create a similar Collection class that can wrap a PHP array and provide some of these useful methods, with support for method chaining to make the API more expressive.


Building the Collection Class

Let's start by creating a basic Collection class. This class will accept an array as input and provide methods to manipulate the data. We'll also ensure that the class supports method chaining by returning $this or a new Collection instance from each method.

Step 1: Create the Class

First, create a new PHP file, e.g., Collection.php, and define the class:

class Collection
{
    protected $items = [];

    public function __construct(array $items = [])
    {
        $this->items = $items;
    }

    public static function make(array $items = [])
    {
        return new static($items);
    }

    public function all()
    {
        return $this->items;
    }
}

Here's what's happening:

  • The __construct method initializes the class with an array of items.
  • The make method is a static factory method that allows us to create a new Collection instance more fluently.
  • The all method returns the underlying array.

Step 2: Add the map Method

The map method applies a callback to each item in the array and returns a new Collection with the results.

public function map(callable $callback)
{
    $mapped = [];

    foreach ($this->items as $key => $item) {
        $mapped[$key] = $callback($item, $key);
    }

    return new static($mapped);
}

Example usage:

$collection = Collection::make([1, 2, 3]);

$mapped = $collection->map(function ($item) {
    return $item * 2;
});

// Result: [2, 4, 6]

Step 3: Add the filter Method

The filter method removes items from the array that don't pass a given truth test.

public function filter(callable $callback)
{
    $filtered = [];

    foreach ($this->items as $key => $item) {
        if ($callback($item, $key)) {
            $filtered[$key] = $item;
        }
    }

    // Reindex the array keys
    $filtered = array_values($filtered);

    return new static($filtered);
}

Example usage:

$collection = Collection::make([1, 2, 3, 4, 5]);

$filtered = $collection->filter(function ($item) {
    return $item > 2;
});

// Result: [3, 4, 5]

Step 4: Add the reduce Method

The reduce method reduces the array to a single value by applying a callback function.

public function reduce(callable $callback, $initial = null)
{
    $result = $initial;

    foreach ($this->items as $key => $item) {
        $result = $callback($result, $item, $key);
    }

    return $result;
}

Example usage:

$collection = Collection::make([1, 2, 3, 4, 5]);

$sum = $collection->reduce(function ($carry, $item) {
    return $carry + $item;
}, 0);

// Result: 15

Step 5: Add the pluck Method

The pluck method extracts a list of values for a given key from an array of arrays or objects.

public function pluck($key)
{
    $plucked = [];

    foreach ($this->items as $item) {
        if (is_array($item) && isset($item[$key])) {
            $plucked[] = $item[$key];
        } elseif (is_object($item) && isset($item->$key)) {
            $plucked[] = $item->$key;
        }
    }

    return new static($plucked);
}

Example usage:

$collection = Collection::make([
    ['name' => 'John', 'age' => 30],
    ['name' => 'Jane', 'age' => 25],
]);

$names = $collection->pluck('name');

// Result: ['John', 'Jane']

Step 6: Add the each Method

The each method allows you to perform an action on each item without modifying the collection.

public function each(callable $callback)
{
    foreach ($this->items as $key => $item) {
        $callback($item, $key);
    }

    return $this; // Return $this for chaining
}

Example usage:

$collection->each(function ($item) {
    echo $item['name'] . "\n";
});

Step 7: Add the sort Method

The sort method sorts the collection by a given callback or by natural order.

public function sort(callable $callback = null)
{
    $items = $this->items;

    if ($callback) {
        uasort($items, $callback);
    } else {
        asort($items);
    }

    return new static($items);
}

Example usage:

$sorted = $collection->sort(function ($a, $b) {
    return $a['age'] <=> $b['age'];
});

Step 8: Add the values() Method

The values() method reindexes the array numerically, removing any existing keys and resetting them to start from zero.

public function values()
{
    // Use array_values to reindex the array numerically
    $reindexed = array_values($this->items);

    return new static($reindexed);
}

Example usage:

$collection = Collection::make([
    10 => 'John',
    20 => 'Jane',
    30 => 'Doe',
]);

$reindexed = $collection->values();

print_r($reindexed->all());

Output

The output will be a reindexed array:

Array
(
    [0] => John
    [1] => Jane
    [2] => Doe
)

Putting It All Together

Here’s the complete Collection class with all the methods we've implemented, including support for method chaining:

class Collection
{
    protected $items = [];

    public function __construct(array $items = [])
    {
        $this->items = $items;
    }

    public static function make(array $items = [])
    {
        return new static($items);
    }

    public function all()
    {
        return $this->items;
    }

    public function map(callable $callback)
    {
        $mapped = [];

        foreach ($this->items as $key => $item) {
            $mapped[$key] = $callback($item, $key);
        }

        return new static($mapped);
    }

    public function filter(callable $callback)
    {
        $filtered = [];

        foreach ($this->items as $key => $item) {
            if ($callback($item, $key)) {
                $filtered[$key] = $item;
            }
        }

        // Reindex the array keys
        $filtered = array_values($filtered);

        return new static($filtered);
    }

    public function reduce(callable $callback, $initial = null)
    {
        $result = $initial;

        foreach ($this->items as $key => $item) {
            $result = $callback($result, $item, $key);
        }

        return $result;
    }

    public function pluck($key)
    {
        $plucked = [];

        foreach ($this->items as $item) {
            if (is_array($item) && isset($item[$key])) {
                $plucked[] = $item[$key];
            } elseif (is_object($item) && isset($item->$key)) {
                $plucked[] = $item->$key;
            }
        }

        return new static($plucked);
    }

    public function each(callable $callback)
    {
        foreach ($this->items as $key => $item) {
            $callback($item, $key);
        }

        return $this; // Return $this for chaining
    }

    public function sort(callable $callback = null)
    {
        $items = $this->items;

        if ($callback) {
            uasort($items, $callback);
        } else {
            asort($items);
        }

        return new static($items);
    }

    public function values()
    {
        // Use array_values to reindex the array numerically
        $reindexed = array_values($this->items);

        return new static($reindexed);
    }
}

Example Usage with Method Chaining

Here’s how you can use the updated Collection class with method chaining:

$collection = Collection::make([
    10 => ['name' => 'John', 'age' => 30],
    20 => ['name' => 'Jane', 'age' => 25],
    30 => ['name' => 'Doe', 'age' => 35],
]);

$result = $collection
    ->filter(function ($item) {
        return $item['age'] > 25; // Filter out ages <= 25
    })
    ->map(function ($item) {
        return [
            'name' => strtoupper($item['name']), // Transform names to uppercase
            'age' => $item['age'],
        ];
    })
    ->values(); // Reindex the array numerically

print_r($result->all());

Output

The output will be a filtered, transformed, and reindexed array:

Array
(
    [0] => Array
        (
            [name] => JOHN
            [age] => 30
        )

    [1] => Array
        (
            [name] => DOE
            [age] => 35
        )
)

Conclusion

In this blog post, we've built a simple Collection class that mimics some of the functionality of Laravel's Collections, complete with method chaining for a fluent and expressive API. By enabling method chaining, we've made the class much more powerful and user-friendly.

You can extend this class further by adding more methods like groupBy, keys, first, last, and more. The possibilities are endless, and you can tailor the class to suit your specific needs.

By understanding how these methods work, you'll not only gain a deeper appreciation for Laravel's Collections but also improve your ability to write clean, expressive, and efficient PHP code.

Happy coding! 🚀

More Posts :