4

I've been reading / watching a lot of recommended material, most recently this - MVC for advanced PHP developers. One thing that comes up is Singletons are bad, they create dependency between classes, and Dependency Injection is good as it allows for unit testing and decoupling.

That's all well and good until I'm writing my program. Let's take a Product page in a eshop as an example. First of all I have my page:

class Page {
    public $html;

    public function __construct() {

   } 

    public function createPage() {
        // do something to generate the page
   } 

    public function showPage()  {
        echo $this->html;
   } 
} 

All fine so far, but the page needs a product, so let's pass one in:

class Page {
    public $html;
    private $product;

    public function __construct(Product $product) {
        $this->product = $product;
   } 

    public function createPage() {
        // do something to generate the page
   } 

    public function showPage()  {
        echo $this->html;
   } 
} 

I've used dependency injection to avoid making my page class dependent on a product. But what if page had several public variables and whilst debugging I wanted to see what was in those. No problem, I just var_dump() the page instance. It gives me all the variables in page, including the product object, so I also get all the variables in product.

But product doesn't just have all the variables containing all the details of the product instantiated, it also had a database connection to get those product details. So now my var_dump() also has the database object in it as well. Now it's starting to get a bit longer and more difficult to read, even in <pre> tags.

Also a product belongs to one or more categories. For arguments sake let's say it belongs to two categories. They are loaded in the constructor and stored in a class variable containing an array. So now not only do I have all the variables in product and the database connection, but also two instances of the category class. And of course the category information also had to be loaded in from the database, so each category instance also has a database private variable.

So now when I var_dump() my page I have all the page variables, all the product variables, multiples of the category variables in an array, and 3 copies of the database variables (one from the products instance and one from each of the category instances). My output is now huge and difficult to read.

Now how about with singletons? Let's look at my page class using singletons.

class Page {
    public $html;

    public function __construct() {

   } 

    public function createPage() {
        $prodId = Url::getProdId();
        $productInfo = Product::instance($prodId)->info(); 
        // do something to generate the page
   } 

    public function showPage()  {
        echo $this->html;
   } 
} 

And I use similar singletons inside the Product class as well. Now when I var_dump() my Page instance I only get the variables I wanted, those belonging to the page and nothing else.

But of course this has created dependencies between my classes. And in unit testing there's no way to not call the product class, making unit testing difficult.

How can I get all the benefits of dependency injection but still make it easy to debug my classes using var_dump()? How can I avoid storing all these instances as variables in my classes?

Community
  • 1
  • 1
Styphon
  • 10,304
  • 9
  • 52
  • 86
  • 2
    How about an dispatcher? Page class acts as a base class, ProductPage is a derived class and the Page dispatches to the ProductPage with the variables needed. If you need another page create a new class InfoPage and derive from Page and dispatch – Ello Feb 23 '15 at 10:34
  • This code you are using represents quick example or it's part of your system's architecture? If second, I suppose, you're not using any framework and building your own skeleton? – Damaged Organic Feb 23 '15 at 19:44
  • It's just a quick example, not my real architecture. Normally I use Laravel or another symphony based framework. – Styphon Feb 23 '15 at 20:09
  • What is the responsibility of the Page class? – yanivel Mar 01 '15 at 17:38
  • @yanivel It would be responsible for collating all the page parts and outputting them. In the real world `Page` would probably be a parent class that others extended, such as `ProductPage` or `CategoryPage`. – Styphon Mar 01 '15 at 21:16
  • First, Let Product have only business logic and not storage logic, so change the db property to a different class such as ProductGateway that handles saving/restoring Products into/from a storage. Also if the page's responsibility is to collate between page parts and render it, then why should it know about Product? It should be fed page parts – yanivel Mar 02 '15 at 05:10

4 Answers4

1

I'll try to write about several things here.

About the var_dump():

I'm using Symfony2 as a default framework, and sometimes, var_dump() is the best option for a quick debug. However, it can output so much information, that there is no way you're going to read all of it, right? Like, dumping Symfony's AppKernel.php, or, which is more close to your case, some service with an EntityManager dependency. IMHO, var_dump() is nice when you debugging small bits of code, but large and complex product make var_dump() ineffective. Alternative for me is to use a "real" debugger, integrated with your IDE. With xDebug under PhpStorm I have no real need of var_dump() anymore.

Useful link about "Why?" and "How-to?" is here.

About the DI Container:

Big fan of it. It's simple and makes code more stable; it's common in modern applications. But I agree with you, there is a real problem behind: nested dependencies. This is over-abstraction, and it will add complexity by adding sometimes unnecessary layers.

Masking the pain by using a dependency injection container is making your application more complex.

If you want to remove DIC from your application, and you actually can do it, then you don't need DIC at all. If you want alternative to DIC, well... Singletons are considered bad practice for not testable code and a huge state space of you application. Service locator to me has no benefits at all. So looks like there is the only way, to learn using DI right.

About your examples:

I see one thing immediately - injecting via construct(). It's cool, but I prefer optional passing dependency to the method that requires it, for example via setters in services config.yml.

class Page
{
    public $html;

    protected $em;
    protected $product;

    public function __construct(EntityManager $em) {
        $this->em = $em;
    }

    //I suppose it's not from DB, because in this case EM handles this for you
    protected function setProduct(Product $product)
    {
        $this->product = $product;
    }

    public function createPage()
    {
        //$this->product can be used here ONLY when you really need it

        // do something to generate the page
    } 

    public function showPage()
    {
        echo $this->html;
    } 
}

I think it gives needed flexibility when you need only some objects during execution, and at the given moment you can see inside your class only properties you need.

Conclusion

Excuse me for my broad and somewhat shallow answer. I really think that there is no direct answer to your question, and any solution would be opinion based. I just hope that you might find that DIC is really the best solution with limited downside, as well as integrated debuggers instead of dumping the whole class (constructor, service, etc...).

Damaged Organic
  • 8,175
  • 6
  • 58
  • 84
1

I exactly know that it's possible to reach result what you wish, and don't use extreme solutions.
I am not sure that my example is good enough for you, but it has: di and it easy to cover by unit test and var_dump will be show exactly what you wish, and i think it encourage SRP.

<?php

class Url
{
    public static function getProdId()
    {
        return 'Category1';
    }
}

class Product
{
    public static $name = 'Car';

    public static function instance($prodId)
    {
        if ($prodId === 'Category1') {
            return new Category1();
        }
    }
}

class Category1 extends Product
{
    public $model = 'DB9';

    public function info()
    {
        return 'Aston Martin DB9 v12';
    }
}

class Page
{
    public $html;

    public function createPage(Product $product)
    {
        // Here you can do something more to generate the page.
        $this->html = $product->info() . PHP_EOL;
    }

    public function showPage()
    {
        echo $this->html;
    }
}

$page = new Page();
$page->createPage(Product::instance(Url::getProdId()));
$page->showPage();
var_export($page);

Result:

Aston Martin DB9 v12
Page::__set_state(array(
   'html' => 'Aston Martin DB9 v12
',
))
Styphon
  • 10,304
  • 9
  • 52
  • 86
cn007b
  • 16,596
  • 7
  • 59
  • 74
  • This is singleton. Where is DI? – Damaged Organic Feb 28 '15 at 00:44
  • public function createPage(Product $product) // $product - isn't di? Behaviour of our page don't depend on what exactly we receive as product? I think it's exactly di, or i'm wrong? – cn007b Feb 28 '15 at 06:36
  • Let me argue that DI is about loose coupling, while code above represents factory method for instantiating objects. This looks like Service locator, more or less, which I prefer not to use in any way. However, I'll vote up for this, because your post is pretty straightforward and actually directly answers the question. – Damaged Organic Feb 28 '15 at 10:25
  • Is there any badge on SO for users who accidentally meet in real world in 3 years after their discussion in comments? : ) – Damaged Organic Nov 30 '17 at 11:02
  • @KidBinary I'm pretty sure there is no such badges...((( – cn007b Nov 30 '17 at 11:06
0

Maybe this will help you:

  class Potatoe {
    public $skin;
    protected $meat;
    private $roots;

    function __construct ( $s, $m, $r ) {
        $this->skin = $s;
        $this->meat = $m;
        $this->roots = $r;
    }
}

$Obj = new Potatoe ( 1, 2, 3 );

echo "<pre>\n";
echo "Using get_object_vars:\n";

$vars = get_object_vars ( $Obj );
print_r ( $vars );

echo "\n\nUsing array cast:\n";

$Arr = (array)$Obj;
print_r ( $Arr );

This will returns:

Using get_object_vars:
Array
(
    [skin] => 1
)

Using array cast:
Array
(
    [skin] => 1
    [ * meat] => 2
    [ Potatoe roots] => 3
)

See the rest here http://php.net/manual/en/function.get-object-vars.php

Styphon
  • 10,304
  • 9
  • 52
  • 86
Catalin Cardei
  • 304
  • 4
  • 15
  • That's dependency injection, exactly the thing I wanted to avoid, or find a similar way but without storing the variables in the class. – Styphon Feb 27 '15 at 23:16
  • Sorry @Styphon. I was thinking that you to keep using di and filter somehow when debugging ... My intention was to help. – Catalin Cardei Feb 28 '15 at 09:59
0

The short answer is, yes you can avoid many private variables and using dependency injection. But (and this is a big but) you have to use something like an ServiceContainer or the principle of it.

The short answer:

class A
{

    protected $services = array();

    public function setService($name, $instance)
    {
        $this->services[$name] = $instance;
    }

    public function getService($name)
    {
        if (array_key_exists($name, $this->services)) {
            return $this->services[$name];
        }

        return null;
    }


    private function log($message, $logLevel)
    {
        if (null === $this->getService('logger')) {
            // Default behaviour is to log to php error log if $logLevel is critical
            if ('critical' === $logLevel) {
                error_log($message);
            }

            return;
        }
        $this->getService('logger')->log($message, $logLevel);
    }

    public function actionOne()
    {
        echo 'Action on was called';
        $this->log('Action on was called', 0);
    }

}

$a = new A();

// Logs to error log
$a->actionOne();

$a->setService('logger', new Logger());

// using the logger service
$a->actionOne();

With that class, you have just one protected variable and you are able to add any functionality to the class just by adding a service.

A more complexer example with an ServiceContainer can be somthing like that

 <?php

/**
 * Class ServiceContainer
 * Manage our services
 */
class ServiceContainer
{
    private $serviceDefinition = array();

    private $services = array();

    public function addService($name, $class)
    {
        $this->serviceDefinition[$name] = $class;
    }

    public function getService($name)
    {
        if (!array_key_exists($name, $this->services)) {
            if (!array_key_exists($name, $this->serviceDefinition)) {
                throw new \RuntimeException(
                    sprintf(
                        'Unkown service "%s". Known services are %s.',
                        $name,
                        implode(', ', array_keys($this->serviceDefinition))
                    )
                );
            }
            $this->services[$name] = new $this->serviceDefinition[$name];
        }

        return $this->services[$name];

    }
}

/**
 * Class Product
 * Part of the Model. Nothing too complex
 */
class Product
{
    public $id;
    public $info;

    /**
     * Get info
     *
     * @return mixed
     */
    public function getInfo()
    {
        return $this->info;
    }

}

/**
 * Class ProductManager
 *
 */
class ProductManager
{
    public function find($id)
    {
        $p = new Product();
        $p->id = $id;
        $p->info = 'Product info of product with id ' . $id;

        return $p;
    }
}


class UnusedBadService
{
    public function _construct()
    {
        ThisWillProduceAnErrorOnExecution();
    }
}

/**
 * Class Page
 * Handle this request.
 */
class Page
{
    protected $container;


    /**
     * Set container
     *
     * @param ServiceContainer $container
     *
     * @return ContainerAware
     */
    public function setContainer(ServiceContainer $container)
    {
        $this->container = $container;

        return $this;
    }

    public function get($name)
    {
        return $this->container->getService($name);
    }

    public function createPage($productId)
    {
        $pm = $this->get('product_manager');
        $productInfo = $pm->find($productId)->getInfo();

        // do something to generate the page
        return sprintf('<html><head></head><body><h1>%s</h1></body></html>', $productInfo);
    }

}

$serviceContainer = new ServiceContainer();

// Add some services
$serviceContainer->addService('product_manager', 'ProductManager');
$serviceContainer->addService('unused_bad_service', 'UnusedBadService');

$page = new Page();
$page->setContainer($serviceContainer);


echo $page->createPage(1);


var_dump($page);

You can see, if you look at the var_dump output, that just the services, you called are in the output. So this is small, fast and sexy ;)

skroczek
  • 2,289
  • 2
  • 16
  • 23