Why I Use Arraycollections Over Arrays?
2 months ago
We all should know by now that array are terrible for typing we can create a single array containing, an object, string, int and boolean, like so: 

$array = [
    new Quote(quote: 'That\'s all any of us are: amateurs. we don\'t live long enough to be anything else.', author: 'Charlie Chaplin'),
    'dowj',
    215,
    false
];

If we dump this out: 

array:4 [▼
  0 => 
App\Model
\Quote {#725 ▶}
  1 => "dowj"
  2 => 215
  3 => false
]

This is fantastic for transferring data of different types, but troublesome when looping over. You have to add type checks in to your code, like so:

foreach ($array as $data){
    if($data instanceof Quote){
        // do somthing...
    }elseif (is_string($data)){
        // do somthing...
    }elseif (is_int($data)){
        // do somthing...
    }elseif (is_bool($data)){
        // do somthing...
    }else{
        // do something else
    }
}

Ick! It's like robbing a house and not knowing if the owners are home. 

Obviously and hopefully the above example is not something we would actually do and don't get me wrong arrays are great and of course I still use them, they are very useful, but I'll generally chose to use an ArrayCollection whenever I need to do some computation, let's have a look at a common use case for arrays:

$someSpecificDataFromArray = [];
foreach ($array as $data){
    if(/** do some check */) {
        $someSpecificDataFromArray[] = $data['some_key'];
    }
}
// do somehting with $someSpecificDataFromArray

1. We are accessing the array with the `some_key` array key, this can get confusing as we have no idea what type the property we are accessing is or even if it exists.
2. We are creating another array to essentially filter out data we don't need to get the good stuff we do. 

Now, let's have a look at an ArrayCollection, Doctrine Collections is a library that contains classes for working with arrays of data. 

$quotes = new ArrayCollection([
    new Quote(quote: 'That\'s all any of us are: amateurs. we don\'t live long enough to be anything else.', author: 'Charlie Chaplin'),
    new Quote(quote: 'Pull back the curtain on your process', author: 'Ann Friedman'),
    new Quote(quote: 'This is the true joy in life, being used for a purpose recognized by yourself as a mighty one. Being a force of nature instead of a feverish, selfish little clod of ailments and grievances, complaining that the world will not devote itself to making you happy. I am of the opinion that my life belongs to the whole community and as long as I live, it is my privilege to do for it what I can. I want to be thoroughly used up when I die, for the harder I work, the more I live. I rejoice in life for its own sake. Life is no brief candle to me. It is a sort of splendid torch which I have got hold of for the moment and I want to make it burn as brightly as possible before handing it on to future generations.', author: 'George Bernard Shaw'),
    new Quote(quote: 'Wealth of information creates a poverty of attention', author: 'Herbert Simon'),
]);

$charlieChaplinQuotes = $quotes->filter(fn(Quote $quote) => str_contains( haystack: $quote->getAuthor(), needle: 'Charlie Chaplin' ));

dd($charlieChaplinQuotes);

Wow, in one line we can extract the exact data we need and it's 100% typed.

This will dump the following: 

AppController.php on line 42:
Doctrine\Common\Collections\ArrayCollection {#6877 ▼
  -elements: array:1 [▼
    0 => 
App\Model
\Quote {#1267 ▼
      -author: "Charlie Chaplin"
      -quote: "That's all any of us are: amateurs. we don't live long enough to be anything else."
    }
  ]
}

  
 In this example we are creating the ArrayCollection, normally this would be a property on an entity and you can control the type that can be added to your Collection, let's have a look:


#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{

  ....

/**
 * @var Collection<int, Tag>|ArrayCollection<int, Tag>
 */
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'articles')]
private Collection $tags;


public function __construct()
{
    $this->tags = new ArrayCollection();
}

/**
 * @return Collection<int, Tag>
 */
public function getTags(): Collection
{
    return $this->tags;
}

public function addTag(Tag $tag): static
{
    if (!$this->tags->contains($tag)) {
        $this->tags->add($tag);
    }

    return $this;
}

public function removeTag(Tag $tag): static
{
    $this->tags->removeElement($tag);

    return $this;
}

}


In this example we are controlling the type of object that can be added to out ArrayCollection, this ensures consistency, every time we get the $tags, it'll be an ArrayCollection of Tag objects.

We could easily achieve this with an array too, but let's be honest we don't and also we still won't have all these really convenient methods for manipulating and interacting with the collection, such as filtering, sorting, mapping, and reducing elements.

I created a open source project Composer Json Parser This is a perfect example of array problems. Firstly I had to `json_decode` the composer.json data, this give use an array of the file data. 
then I had to check if an array key existed before adding it to the object.  


if (array_key_exists(key: 'name', array: $composerJsonData)) {
    $composer->setName($composerJsonData['name']);
}

 
But once we have it as an object with ArrayCollection for the require and dev-require, the code becomes so much cleaner. 

Here is how the package can get an package by name: 


public function getPackageByName(string $name): null|Package
{
    $packages = new ArrayCollection([
        ...$this->getRequires(),
        ...$this->getDevRequires()
    ]);

    return $packages->findFirst(fn(int $key, Package $package) => $name === $package->getName());
}

How clear, clean and cool is that! 

We can actually use this in our projects like this:

$composer = (new ParserFacade())->extract();
$package = $composer->getPackageByName('rector/rector');

//        object(ComposerJsonParser\Model\PackageVersion)#29 (2) {
//            ["version"] => float(0.18)
//            ["versionConstraints"] => string(1) "^" 
//       }

This makes our code far more readable, durable and maintainable and most importantly no type guessing.