Clayton Engle
Clayton Engle

Reputation: 581

Nested Objects in PHP

Likely this has already been asked, but nevertheless, here goes. This may fall under best practice or security... I'm not really sure.

In my application, I am using a nested object, that is called in the __construct() function. Sort of like this:

class user {
    public $userID = NULL;
    public $someObject = NULL;

    public function __construct() {
        $this->userID = getThisUser();
        $this->someObject = new objectBuilder($this->userID);
    }
    public function getThisUser() {
        // ... 
    }
}

class objectBuilder {
    public $buriedVar = NULL;

    public function __construct($uid = NULL) {
        if( !isset($uid) ) {
            $this->buriedVar = setTheObject($uid);
        } else {
            $this->buriedVar = setTheObject(0);
        }
    }
    public function setTheObject($id) {
        // ...
        return "random string";
    }
}

$tom = new user();

Obviously terrible outline here, but the point is, I can then call $tom->someObject->buriedVar and it'll return "random string".

While looking for a way to nest classes, I noticed no one recommends this as a method for storing objects inside of another object. I'm curious of a few things:

1) Is this insecure?
2) Are the vars inside the nested object exclusive to the call made inside $tom->__construct(), or if I create another object using new objectBuilder() is it overwriting the one inside $tom->someObject? I haven't noticed this, but am not sure how to test for that entirely.
3) Is there something else I'm missing? A best practice reason not to instantiate an object inside a class? I've been using it for years and it works great for what I've done. Is it a speed thing?

Upvotes: 2

Views: 2066

Answers (1)

smcjones
smcjones

Reputation: 5600

1) Is this insecure?

Not inherently, no.

2) Are the vars inside the nested object exclusive to the call made inside $tom->__construct(), or if I create another object using new objectBuilder() is it overwriting the one inside $tom->someObject? I haven't noticed this, but am not sure how to test for that entirely.

This is a fundamental question between class and object. Objects are instances of a class and there can be multiple. The only things that would be overwritten are static properties and methods. You could test it like this:

<?php
$obj1 = new objectBuilder();
$obj2 = new objectBuilder();
if ($obj1 !== $obj2) {
    echo "objects are not the same\n";
}
if ($obj1->buriedVar !== $obj2->buriedVar) {
    echo "nested objects are not the same either\n";
}
$obj3 = new objectBuilder(1);
if ($obj1->buriedVar != $obj3->buriedVar) {
    echo "even the values of two different buried vars with different values are different.\n";
}

if ($obj1->buriedVar == $obj2->buriedVar) {
    echo "counter-example: nested variables with the same values set are similar.\n";
}

It helps to know the difference between equality and identity (see this SO post).

3) Is there something else I'm missing? A best practice reason not to instantiate an object inside a class? I've been using it for years and it works great for what I've done. Is it a speed thing?

You touched on it briefly. What you should know is that this is not scalable and is difficult to test.

Imagine you're creating a website for dogs.

<?php
class Bio
{
    public function __construct()
    {
        $this->dog = new Dog('Terrier');
    }
}

class Dog
{
    private $animal = 'dog';
    private $noise = 'woof!';
    private $breed;

    public function __construct($breed=null)
    {
        $this->setBreed($breed);
    }

    public function setBreed($breed)
    {
        $this->breed = $breed;
    }
}

What if you want to add a new breed? Well... That's easy enough:

class Bio
{
    // ...
    public function __construct($breed)
    {
        $this->dog = new Dog($breed);
    }
    // ...
}

Cool! You've solved everything.

Except...

One day you want to create a section for cats, because one of your best writers also loves cats, and you sense an untapped market.

Uh oh...

You can refactor the code, of course. But you wrote it a long time ago. Now you have to go in and figure out where everything went. No big deal.. A bit annoying but you fixed it!

But now you have another problem. Turns out that the same author wants to add different traits to the breed. You're surprised this hasn't come up sooner but, hey, it's probably a good thing to have.

Now you need to go in to the Dog object, and the Cat object, and add traits.

Every single time.

On. Every. Bio.

After some reconfiguring, you've created something monstrous like this:

$article1 = new Bio('Terrier', 'dog', ['independent']);
$article2 = new Bio('Persian', 'cat', ['flat-faced']);
//... and so on, and so on

The next time the author asks for something, you fire her and then tear your hair out in a mad rage.

Or, from the beginning, you use Dependency Injection.

<?php
class Bio
{
    private $animal;

    public function __construct(AnimalInterface $animal)
    {
        $this->animal = $animal;
    }
}

interface Animal
{
    public function getType();
    public function setBreed($breed);
    public function getBreed();
    public function setTraits(array $traits);
    public function getTraits();
}

abstract class AbstractAnimal implements AnimalInterface
{
    private $breed;
    private $traits = [];

    abstract public function getType();

    public function setBreed($breed)
    {
        $this->breed = $breed;
    }

    public function getBreed()
    {
        return $this->breed;
    }

    public function setTraits(array $traits)
    {
        $this->traits = $traits;
    }

    public function getTraits()
    {
        return (array)$this->traits;
    }
}

class Cat extends AbstractAnimal
{
    public function getType()
    {
        return 'cat';
    }
}

class Dog extends AbstractAnimal
{
    public function getType()
    {
        return 'dog';
    }
}

This pattern requires little to no editing after it has been created.

Why? Because you are injecting the object to nest into the class, rather than instantiating it in the object.

$bio1 = new Bio($dog); $bio2 = new Bio($cat); can always stay like this. Now you just edit the $dog and $cat objects. The added benefit is that these objects can be used anywhere.

But what about utility classes?

(This is where testability comes in. If you haven't worked with unit testing, I recommend reading up on it in the link to PHPUnit below. I'm not going to dwell on how that works as it's off topic).

Dependency Injection is well and good if you have classes that require customization. But what about utility classes that just house various functions?

class Utils
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

You might think that you can call this function safely from the constructor. And you can. However, one day you might create a log method in your Utils class:

public function log($msg)
{
    exec("cat '$msg' > /tmp/log.txt");
}

This works just fine. However, when you run tests, your /tmp/log.txt file complains. "Invalid permissions!". When this method is run via your website, log.txt needs to be writeable by www-data.

You could just chmod 777 /tmp/log.txt, but that would mean everyone who has access to your server can write to that log. Additionally, you may not want to always write to the same log when you're testing as when you're navigating through the web interface (Personally, I would find it confusing and cluttering).

PHPUnit and other unit testing services allow you to mock various objects. The problem is that you have classes calling Utils directly.

You have to find a way to manually override the constructor. Look at PHPUnit's manual to find out why this maybe isn't ideal.

So if you're not using Dependency Injection, what do you do?

PHPUnit suggests, amongst other fixes, moving this Utils object instantiation to another method and then stubbing/mocking that method in your unit test (I want to emphasize that this is after recommending Dependency Injection).

So the next best?

public function __construct()
{
    $this->init();
}

private function init()
{
    $this->utils = new Utils;
}

Now when you unit test, you can create a fake init method and it will be called as soon as the class is created.

In conclusion, the way you are currently instantiating classes is not scalable or easily testable in many real world situations. While it may be all right in limited situations, it is better to get used to the DI (Dependency Injection) pattern, because it will save you lots of headaches in the future.

Upvotes: 2

Related Questions