How To Generate Class Factories The Easy Way with FactoryCreator

How To Generate Class Factories The Easy Way with FactoryCreator

Want to save time generating factories for your Zend ServiceManager dependencies? ServiceManager 3.2.0 contains FactoryCreator. In today’s tutorial, let’s see how to use it and how to save you time and effort.


How To Generate Class Factories The Easy Way in Zend Framework - Master Zend Framework

If there’s one thing that’s always frustrated me when working with Zend Framework, it’s having to create factories for classes. Sure, it’s gotten easier as Zend ServiceManager’s continued to ever improve. And PhpStorm and Zend ServiceManager Grand Master, Gary Hockin, has given me a number of great tips and suggestions.

However, it’s always been something I’ve felt frustrated by. Perhaps you feel the same. Before you think this is a hack job, Zend Framework has (generally) always encouraged us to develop apps the right way, using best practices such as dependency injection.

However, what I’ve felt for some time is that they could also make it easier for us to follow these best practices too, such as with some tooling support. In the latest release of Zend ServiceManager, version 3.2.0, they have.

Specifically, they’ve included two tools in the release:

  • Zend\ServiceManager\Tool\ConfigDumper
  • Zend\ServiceManager\Tool\FactoryCreator

Given that, in this two-part mini-series, I’m going to give you a rapid run-through of both of them, specifically their command-line tools. Hey, I could have gotten in and developed them, instead of being grumpy. So, the very least that I can do is help get the word out about them.

In part one, I’m starting with FactoryCreator. We’ll cover ConfigDumper in part two. Quoting the release notes for FactoryCreator:

It will introspect a given class and generate a factory for it. It also adds a vendor binary, generate-factory-for-class, for generating these from the command line.

Short On Time - Watch The Video

Installing FactoryCreator

To use FactoryCreator, naturally, the first thing that you need to do is to ensure that you have at least version 3.2.0 of ServiceManager included as part of your project. If your composer.json includes the following require definition for ServiceManager, then all you need to do is to run composer update.

"zendframework/zend-servicemanager": "^2.7.3 || ^3.0",

If not, then change it to include at least ^3.2, or run composer require zendframework/zend-servicemanager, and after a minute or so, you’ll have the latest release installed, and be ready to following along with the remainder of the tutorial.

Using FactoryCreator

In this tutorial, I’m going to show three examples of creating factories for classes with FactoryCreator, starting with the one for the most straightforward class I have in a repository of mine, JournalService. You can see the definition here:

<?php

namespace App\Service;

use App\Entity\Journal;

class JournalService implements JournalServiceInterface
{
    public function createJournalEntry(Journal $journal)
    {

    }
}

To generate a factory for the class, in the terminal, in the root directory of the project, I’ll run the following command:

vendor/bin/generate-factory-for-class "App\Service\JournalService"
Note: I've enclosed the fully-qualified class namespace in double-quotes — I could also have used single quotes. If I hadn’t, then the shell would have stripped the backslashes from the name, and passed AppServiceJournalService to `generate-factory-for-class`, resulting in the following error: Class "AppServiceJournalService" does not exist or could not be autoloaded.

So, make sure that you don’t get caught by that. I did at first. Now let’s look at the factory which is generated and displayed to stdout, which you can see below.

<?php

namespace App\Service;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use App\Service\JournalService;

class JournalServiceFactory implements FactoryInterface
{
    /**
     * @param ContainerInterface $container
     * @param string $requestedName
     * @param null|array $options
     * @return JournalService
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        return new JournalService();
    }
}

You can see that it’s a basic class which implements FactoryInterface, providing a basic implementation of the required __invoke magic method. It’s also generated excellent docblock comments, and use statements for the included classes — extra points for that!

Now, let’s generate a factory for a slightly more complicated class, one which uses a TableGateway object. You can see its definition below.

<?php

namespace App\ServiceManager\TableGateway;

use App\ServiceManager\JournalServiceInterface;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\TableGateway\TableGateway;
use Zend\Stdlib\Exception\InvalidArgumentException;

class JournalTable implements JournalServiceInterface
{
    protected $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    // other methods removed...
}

As before, to generate a factory, I’d run the command below:

vendor/bin/generate-factory-for-class "App\ServiceManager\TableGateway\JournalTable"

The command would then generate the class definition below to stdout.

<?php

namespace App\ServiceManager\TableGateway;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use App\ServiceManager\TableGateway\JournalTable;

class JournalTableFactory implements FactoryInterface
{
    /**
     * @param ContainerInterface $container
     * @param string $requestedName
     * @param null|array $options
     * @return JournalTable
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        return new JournalTable($container->get(\Zend\Db\TableGateway\TableGateway::class));
    }
}

Again, it’s generated a clean class which implements FactoryInterface, along with proper use statements and docblock comments. Nothing special. Again, an excellent job.

The one thing that caught my eye though was $container->get(\Zend\Db\TableGateway\TableGateway::class). Perhaps this is me being pedantic, but I would have thought that if all the other classes in the __invoke method weren’t generated with fully-qualified class namespaces, then this one wouldn’t either.

But long-time ServiceManager contributor, Gary Hockin, set me straight. To quote him:

The reason for the FQNS in the factory generator is simple: not adding it adds a ton of complexity to the process. Consider I have \My\App\User\Mapper and \My\App\News\Mapper. We can import both and then have the Mapper::class in the factory. However, which mapper do we use? We would have to add aliases to the import statements. It wasn’t worth the effort to be able to think of all edge cases to remove FQNS.

Now, let’s finish up by generating a final factory class for one, slightly more complicated, class. Again, you can see the class definition below.

<?php

namespace App\Action;

use App\ServiceManager\JournalServiceInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Router;
use Zend\Expressive\Template;

class HomePageAction
{
    const PAGE_TEMPLATE = 'app::home-page';
    private $router;
    private $template;
    private $journal;

    public function __construct(
        Router\RouterInterface $router,
        Template\TemplateRendererInterface $template = null,
        JournalServiceInterface $journal
    ) {
        $this->router = $router;
        $this->template = $template;
        $this->journal = $journal;
    }

    // other methods removed...
}

It’s a slightly revised version of the standard HomePageAction class which the Zend Expressive Skeleton Installer creates. Yes, the Skeleton Installer also generates a factory class for it. However, I wanted to generate one all the same.

Running vendor/bin/generate-factory-for-class "App\Action\HomePageActionv" generates the class definition below.


<?php

namespace App\Action;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use App\Action\HomePageAction;

class HomePageActionFactory implements FactoryInterface
{
    /**
     * @param ContainerInterface $container
     * @param string $requestedName
     * @param null|array $options
     * @return HomePageAction
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        return new HomePageAction(
            $container->get(\Zend\Expressive\Router\RouterInterface::class),
            $container->get(\Zend\Expressive\Template\TemplateRendererInterface::class),
            $container->get(\App\ServiceManager\JournalServiceInterface::class)
        );
    }
}

As before, you can see that it does everything correctly, and again, for the classes retrieved from the container, it supplies the fully-qualified class namespace. I think it would be better to include the namespace in a use statement or to have a switch where you can choose this. That said, any competent IDE would sort that out quite quickly.

In Conclusion

And that’s a rapid run-through of generate-factory-for-class. It’s an excellent addition to Zend ServiceManager, one that I’m eager to put to use in my current projects. If this is your first time seeing it, and are looking for a tool to save you time creating factories for your classes, then check it out.

In part two of this mini-series, we’ll have a rapid run-through of ConfigDumper’s command-line tool: generate-deps-for-config-factory.

Special thanks to Matthew Weier O’Phinney and Gary Hockin for being technical reviewers on the tutorial.


You might also be interested in...


Want more tutorials like this?

If so, enter your email address in the field below and click subscribe.

You can unsubscribe at any time by clicking the link in the footer of the emails you'll receive. Here's my privacy policy, if you'd like to know more. I use Mailchimp to send emails. You can learn more about their privacy practices here.


Join the discussion

comments powered by Disqus