Getting Started Going From Zend Framework 2 to 3


Over the last two weeks I’ve been working on figuring out Zend Framework 3 and how to migrate an app from 2 to 3.  Zend does have a migration guide as part of their ZF3 docs, but I found it woefully inadequate for the reality of the upgrade.  I also found the tutorial, while updated for ZF3, is still far to basic to do much more than frustrate me.

So, using the fuller ZF3 and the the free online book, Using Zend Framework 3 by Oleg Krivtsov, I set about taking my An Eclectic World website from ZF2 to ZF3.  Along the way, of course, it required upgrading it for PHP 7.x as it was running on PHP 5.6 at the time.

I started the recommended way, with the Zend Skeleton App.  It is a very basic skeleton, but it is an improvement over the previous one as it really does include just the very basics you need to get an app started.  While I typically prefer starting with our own skeleton app, for obvious reasons I would recommend doing at least your first ZF3 app with the official one.  There are changes in core files, like the index.php and Module.php, that are easier to deal with by just starting with new and then readding your customizations as needed.

Oleg’s book is an excellent resource for understand some broadly recommended best practices for organizing a module and for figuring out the differences between ZF2 and ZF3 file organization.   For one thing, they dropped the secondary plural named directory inside the src file for a module, so now you just put the files and folders directly in source.

To give a more concrete example, here is a side by side comparison of my Book module’s src folder from the ZF2 app to the ZF3:


ZendFramework 2


ZendFramework 3

There is also a difference in how files are named. Gateways, the classes that store the DB querying functions, are now called Repositories.  They also dropped the mix of plural and singular file names, so instead of BooksController and BookEntity, it’s now all just Book.  This does make it a bit cleaner to me versus trying to remember plural versus singular rules.

One really big change is in the heavier reliance on factories for dependency injection.  At least for us, this is a big change.  We did very little dependency injection using ZF2, and instead used the ServiceLocator to pull in Gateways in the Controller, and just directly called Services and Gateways in a Service or Gateway as needed.   The DB Adapter was handled by heavy use of the static adapter function, so that we didn’t have to constantly pass it around from class to class every time we called them.

So the first place the factories really play into things is with controllers.  Almost every controller, it seems, will need a factory unless it isn’t doing anything but serving static views.  The nice side is the controller code does look cleaner without all the ServiceLocator calls, but it also means you have to inject every single repository and/or service you may need with the controller factory.  For a smaller app, like my personal site, this isn’t a big deal, but I can see it being a lengthy chunk of arguments being sent to the constructor on some of our more complex apps.

Again, let’s look at a concrete example to better explain.   Here is a chunk of my BooksController from the ZF2 version of my app:

public function booksHomeAction() {
     $aNewest = $this->getGateway('Books')->getNewest();
     $qRandom = $this->getGateway('Books')->getRandom();
      
     $oView = new ViewModel(array(
          'aNewest' => $aNewest,
          'qRandom' => $qRandom,
     ));
      
     return $oView;
}

Here is the same code in the ZF3 version BookController:

public function indexAction()
{
    $newest = $this->book_repository->getNewest();
    $random = $this->book_repository->getRandom();
 
    return [
        'newest' => $newest,
        'random' => $random,
    ];
}

And this is what the BookControllerFactory looks like:

<?php
    namespace Book\Controller\Factory;
 
    use Book\Controller\BookController;
    use Book\Repository\BookRepository;
    use Interop\Container\ContainerInterface;
    use Zend\Db\Adapter\AdapterInterface;
    use Zend\ServiceManager\Factory\FactoryInterface;
 
    class BookControllerFactory implements FactoryInterface
    {
        public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
        {
            $db_adapter = $container->get(AdapterInterface::class);
            $book_repository = new BookRepository($db_adapter);
 
            return new BookController($book_repository);
        }
    }

As you can see, it is only injecting a single repository, since everything for it goes through the BookRepository anyway, or through the Book entity/object.  All of the other controller factories for the Book module though must inject two repositories: the one for that bit of function (like GenreRepository) and the BookRepository for getting the related list of books for some pages of the site.

<?php
    namespace Book\Controller\Factory;
 
    use Book\Controller\GenreController;
    use Book\Repository\GenreRepository;
    use Book\Repository\BookRepository;
    use Interop\Container\ContainerInterface;
    use Zend\Db\Adapter\AdapterInterface;
    use Zend\ServiceManager\Factory\FactoryInterface;
 
    class GenreControllerFactory implements FactoryInterface
    {
        public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
        {
            $db_adapter = $container->get(AdapterInterface::class);
            $genre_repository = new GenreRepository($db_adapter);
            $book_repository = new BookRepository($db_adapter);
 
            return new GenreController($genre_repository, $book_repository);
        }
    }
<?php
    namespace Book\Controller;
 
    use Zend\Mvc\Controller\AbstractActionController;
 
    class GenreController extends AbstractActionController
    {
        private $genres_repository;
        private $book_repository;
 
        public function __construct($genres_repository, $book_repository)
        {
            $this->genres_repository = $genres_repository;
            $this->book_repository = $book_repository;
        }
...

Repositories also need factories for injecting the DB adapter.  I had hoped I could do as we did on our current apps and just have a nice core repository that the others extended that set up the DB adapter using the static adapter, but so far, the static adapter seems much less reliably set from the config method than it was in ZF2.

Another big change is in the significantly heavier reliance on Composer.  With ZF2 you could install the framework without Composer if needed, while with ZF3 it is pretty much impossible to run (as far as I can tell) without it.  As I was developing on my local system, I had to install Composer for Windows to get the framework installed.  You also need to use composer to handle the auto loading of classes, including getting new modules installed. If you are in a hosted environment without shell access, you can at least do the composer stuff on your desktop and then upload it and it will still work.

As part of this whole redo, ZF3 is also much more decoupled from itself than ZF2.  With ZF2 you pretty much just installed the whole framework, regardless of how much of it you were actually using, while ZF3 has you install each individual module, allowing, née encouraging you only install what will actually be used in the app by making it rather tedious and cumbersome to install it all.  They do have a way to install the whole thing, but it isn’t encouraged.

Figuring out what modules you need though can be a game of guesswork and testing.  Going through the skeleton seemed to offer up the core modules I needed, but as I began actually trying to run bits of the app, I was constantly finding more that I needed to install.  In the end, for my app, this ended up being my module list in composer:

	"require" : {
		"php" : "&gt;=7",
		"phpoffice/phpspreadsheet" : "~1.5",
		"zendframework/zend-component-installer" : "^2.1",
		"zendframework/zend-mvc" : "^3.1",
		"zendframework/zend-mvc-form" : "^1.0",
		"zendframework/zend-mvc-i18n" : "^1.1",
		"zendframework/zend-mvc-plugins" : "^1.0",
		"zendframework/zend-cache" : "^2.8",
		"zendframework/zend-db" : "^2.9",
		"zendframework/zend-log" : "^2.10",
		"zendframework/zend-router" : "^3.2",
		"zendframework/zend-form" : "^2.12",
		"zendframework/zend-validator" : "^2.10",
		"zendframework/zend-json" : "^3.1",
		"zendframework/zend-session" : "^2.8",
		"zendframework/zend-servicemanager" : "^3.3",
		"zendframework/zend-debug" : "^2.6",
		"zendframework/zend-authentication" : "^2.6",
		"zendframework/zend-eventmanager" : "^3.2",
		"zendframework/zend-modulemanager" : "^2.8",
		"zendframework/zend-http" : "^2.8",
		"zendframework/zend-view" : "^2.10",
		"zendframework/zend-filter" : "^2.8",
		"zendframework/zend-inputfilter" : "^2.8",
		"zendframework/zend-mvc-plugin-flashmessenger" : "^1.1",
		"zendframework/zend-mvc-plugin-identity" : "^1.1",
		"zendframework/zend-serializer" : "~2.9",
		"zendframework/zend-feed" : "^2.10",
		"zendframework/zend-hydrator" : "^2.4",
		"zendframework/zend-mail" : "^2.10",
		"zendframework/zend-crypt": "^3.3"
	},

That gave me the necessary database functions, access to view helpers, identity, flash messenger, forms, form/input filters, mail functions for error handling, and RSS functions I needed to pull in my feeds.  Figuring them out literally took putting in the code, then figuring out it wasn’t working because it was missing a component.

One thing I have found thus far with ZF3 is the error messages are practically useless.  I was getting “cannot map to a factory” errors when the code was missing a use statement, or when the path of the original class (not the factory) was wrong, or because the module wasn’t there.  It took me three days to figure out that some bits of code weren’t working because I needed to install another module, versus there being some other change I needed in my code.

All that said, it took me about two weeks to completely redo the app for ZF3.  Once I figured out the error message issues and got the hang of the factory stuff and the requisite changes to the module config, redoing the app went quickly.  The Gateways/Repositories needed the least amount of recode, just adding the construct functions to handled the DB adapter injection.  The controllers needed the most reworking, which is not surprising since that is what you would expect when changing out a framework. I took the opportunity to clean up my URLs though, which was a side bonus.

I did see significant performance improvements in the app, even with my removing a lot of the caching stuff (because my local system couldn’t handle it and was crashing PHP while trying to use it), but that is as likely to be from the upgrade to PHP 7 as it is the switch to ZF3.  And fortunately for me, while composer is required to set up the app, my running all the composer on my local system and then uploading to resulting files worked fine for running the app in my hosted environment, which is good.

These are just some of my initial thoughts on ZF3.  I’m sure I’ll have more as I explore the next aspects: trying to set up an authentication system for my app to begin the long overdue process of making an administration system for it.