Composition vs Aggregation in PHP

I was reading an article about relationships in OOP, association, composition, aggregation, etc. Something is confusing & I keep finding conflicting information online so I hope that someone can shed some light on this.

So in PHP, we call the following code composition & a lot of articles/tutorials point out to use composition over the inheritance.

class A
{
  
}

class B
{
    public function __construct(protected A $a)
    {
    }
}

After reading a few articles about composition & aggregation, it seems that the above is actually an example of aggregation & not composition because in composition the object of class A can not exist without class B, so when an object of class B is destroyed the object of class A should also be destroyed. That is clearly not the case in the above code because the object of class A can exist outside of class B, so its lifecycle is not dependent on class B.

The following would be the example of composition:

class B
{
    public A $a;
  
    public function __construct()
    {
        $this->a = new A();
    }
}

So from my understanding aggregation means that an object of class A can exist outside of class B while composition means that the object of class A's lifecycle depends on class B & cant exist outside of class B.

Am I understanding this correctly?


Solution 1:

It's important to keep in mind that these are patterns for relating classes and objects.

The first point I would make is that any blanket rule like "use composition instead of inheritance" reflects some missing context. Use inheritance if it's appropriate.

These other patterns are for relating classes together.

Aggregation is typically used for hierarchy/parent/child relationships.

A classic example would be relating Employee and Department.

  • An Employee object exists
  • An Employee is "owned by/a member of" a Department
  • You could say that "Department Foo" Aggregates 0-N employees.

So to model this, Department would typically have an internal employees[] array.

Conversely, the employee could store the department Object internally. This essentially matches the relationship between the two classes as Many >-< Many.

Composition reflects a stricter parent/child relationship, with the implication being that ObjectA is composed of 1-N of Objects B.

The classic example of a Composition relationship is a Shopping Cart, which has 0-N product lineitems in it.

Assuming the classes of Cart and Lineitem, a Cart would store an array of Lineitems.

The main difference between Aggregation and Composition then, is that with Composition if a Cart is destroyed, then all the associated Lineitem objects should be destroyed.

With the Aggregation example, if a Department is eliminated, that does not necessarily mean that all the aggregated employees would be eliminated, but rather that the deletion of a department simply breaks the relationship between employee and department, and might require that the variable storing an employee's department object would be null, until such time as the employee is assigned to another department, or perhaps that their employment ends.

Practically, Since PHP is page scoped, variables and object composition is short lived, so you don't really find many cases outside perhaps of an ORM, where these UML patterns are employed. Data needs to be persisted, and typically that persistence is via some sort of RDBMS or Document database where the ORM is going to mimic the relationship between tables, or possibly a hierarchy.

To your examples:

These sort of foo/bar A/B examples have no semantic meaning or value. The only mechanic in example #1 is that ObjB can't be constructed without their first being an ObjA. The syntax you provided is new in PHP8, which I missed at first.

It seems that this example would more likely need to be:

class A
{
    private bSet = array();

    public function addB(B $b) {
       $this->bSet[] = new B($this);
    }  
}

class B
{
    private $a;

    public function __construct(A $a)
    {
        $this->a = $a;            
    }
}


// B shouldn't be made without a Parent A
$objA = new A();
$objA->addB();
$objA->addB();

However nothing technically will stop you from making a B, as constructors are always public.

$objB = new B($objA);   

With aggregation this would be a more likely scenario:

class A
{
    private bSet = array();

    public function addB(B $b) {
       $this->bSet[] = $b;
       $b->setA($this);
    }

    public function delB(B $b) {
        for ($i=0; $i < count($this->bSet); $i++) {
            if ($b === $this->bSet[$i]) {
                unset($this->bSet[$i];
                break;
            }
        }
    }  
}

class B
{
    private $a;

    public function setA(A $a) {
        if ($this->a) {
            $this->a->delB($this);
        }
        $this->a = $a;
    }

    public function getA() {
        return $this->a;
    }

}

$objA1 = new A();
$objA2 = new A();

$objB1 = new B();
$objB2 = new B();
$objB3 = new B();

$objA1->addB($objB2);
$objA2->addB($objB1);
$objA2->addB($objB3);
//Change $objB3's parent from A1 to A2.
$objB3->setA($objA1);

A's and B's are independent of each other, but still have ownership.

What I find more practically valuable in PHP development is Dependency Injection as the basis for the leading PHP frameworks (Symfony, Laravel) as well as implementation of other common OOP design patterns you find in the Gang of 4 book, Domain Driven Design, or other books and websites these days.