The era of full-stack frameworks is behind us. Nowadays, framework vendors are splitting their monolithic repositories into components with the help of Git subtree, allowing you to cherry-pick ones that you need for your project. This means that you can build your application on top of Zend Service Manager, Aura Router, Doctrine ORM, Laravel (Illuminate) Eloquent, Plates, Monolog, Symfony Cache, or any component out there that can be installed via Composer.

Robust project structure

Basic step is to establish and maintain a solid, framework-agnostic project structure in order to be able to combine and assemble diverse framework components in an efficient manner. I've dedicated a full article to this subject covering matters of directory structure, organizing and grouping sources, naming conventions, and similar.

Choose the right tool for the job

Throughout the development of a project focus should always be on its core business logic. For all the things that are not the domain matters of your application you should turn to open source goodies, components and libraries that facilitate some common, cross-cutting concerns of the application development. DBAL, ORM, routing, mailer, cache, logger are only some of the examples of something that you should not be reinventing.

Let me remind you that you can use components independently of almost every framework, for example Zend Framework, Symfony, Laravel, Aura, etc., thus making composer.json dependencies look very diverse:

   "require": {
     "php": "^7.0",
     "container-interop/container-interop": "^1.0",
     "zendframework/zend-servicemanager": "^3.0.3",
     "symfony/console": "^3.1",
     "symfony/event-dispatcher": "^2.8",
     "doctrine/dbal": "^2.5",
     "zendframework/zend-filter": "^2.7",
     "aura/intl": "^3.0",
     "psr/log": "^1.0",
     "monolog/monolog": "^1.21",
     "illuminate/support": "^5.3",
     "league/plates": "^3.1",
     "slim/slim": "^3.7",
     "mongodb/mongodb": "^1.0",
     "filp/whoops": "^2.1",
     "ramsey/uuid": "^3.5",
     "robmorgan/phinx": "^0.6.5",
     "psr/simple-cache": "^1.0",
     "symfony/cache": "3.3.*@dev"

Decouple from the framework

While being able to use different framework components is a gracious privilege, it can put you in a hopeless situation if used recklessly. Crucial, but not an easy task is to decouple your business logic from the framework or library that you're using. Otherwise, you might have difficulties not only when trying to switch to a component from some different vendor, but also when upgrading to a newer version of the very same component.

Being 100% decoupled from the framework code is impossible unless you're not using it at all, but what you can do is to significantly reduce coupling. Establish an interface layer that abstracts and decouples your code from external dependencies or use PSR interfaces for things that are standardized so far in order to minimize the efforts needed for switching to some alternative implementation of a component. In general, programming to an interface is the principle you should practice. Briefly, this means that in your code you should use interfaces and not concrete implementations for type hinting.

Ideally, these are the only places in the system where you should allow direct coupling to happen:

  • concrete implementations of services that abstract external dependencies
  • factories
  • dispatch targets, for example middleware, controllers, action handlers, CLI commands, assuming that they are "thin", containing no business logic

Configuration management

Instead of hard-coding database connection parameters in your code, you put those and similar kind of parameters in configuration files, so they can be easily manipulated and changed in different environments (development, production, etc.).

There are several strategies for managing configuration files. The most obvious is having one configuration file per environment which is then dynamically loaded based on the environment variable that specifies environment in which application is running:


The main drawback of this simplistic approach is the necessity for maintaining duplicated parameters across multiple configuration files.

What I prefer is a rather different principle for handling environment-specific configuration, advocated by Zend Framework, nicely described in framework's documentation. It allows for organizing configuration files this way:


In this case, parameters can be nicely distributed in separate configuration files based on their purpose and environment-specific overrides happen in discrete, unversioned configuration files which contain only parameters that are different. Files are merged into single configuration in the order specified through a glob brace definition.

Dependency Injection is the key

Practicing Dependency Injection technique is essential for making your code flexible and robust. In this regard, the DI Container is the key surrounding concept that manages logic for constructing and wiring up building blocks of the application.

Stuff that should be defined in the DI Container:

  • general-purpose services (database adapter, cache, mailer, logger, etc.)
  • domain services, repositories
  • middleware, controllers, action handlers (yes, these have dependencies that need to be injected!)
  • web and CLI application runners

There's a common name for all these objects - services. A service is a generic name for any PHP object that is designed for a specific purpose (e.g. sending emails) and is used throughout the application whenever functionality it provides is needed. If a service has a complex construction logic (has dependencies) or it is a dependency of some other class, and it's not meant to be instantiated several times within the same request, it should be registered with the DI Container.

Other group of classes are those that represent type, such as domain objects, entities, value objects. Think of User, Post, DateTime as concrete examples of those classes. Those are not services, thus they should not be defined in the container.

Configuring DI Container

Instead of populating DI Container instance programmatically, it is advisable to define application dependencies in the configuration:

return [
    'di' => [
        'factories' => [
            Psr\SimpleCache\CacheInterface::class => App\Cache\CacheFactory::class,
            App\Db\DbAdapterInterface::class => App\Db\DbAdapterFactory::class,
            App\User\UserService::class => App\User\UserServiceFactory::class,
            App\User\UserRepository::class => App\User\UserRepositoryFactory::class,

Some DI Containers, like Zend Service Manager for example, support this approach out of the box, otherwise you'll have to write some simple logic for populating it based on configuration array.

You may have noticed that I prefer to use fully-qualified name of the interface as a service name for services that provide interface implementations. In case there's no interface I use fully-qualified class name. Reason is simple: code that involves retrieval of services from the container becomes apparent, but also it is easier for a consumer to reason what he is dealing with.


Code that loads configuration and initializes DI Container is typically contained in what is known as a bootstrap script. Depending on the configuration strategy and DI Container implementation, it can have the following form:

$config = [];

$files = glob(sprintf('config/{{,*.}global,{,*.}%s}.php', getenv('APP_ENV') ?: 'local'), GLOB_BRACE);

foreach ($files as $file) {
    $config = array_merge($config, include $file);

$config = new ArrayObject($config, ArrayObject::ARRAY_AS_PROPS);

$diContainer = new Zend\ServiceManager\ServiceManager($config['services']);
$diContainer->set('Config', $config);

return $diContainer;

DI Container is the end result of bootstrapping operation, through which all further actions take place.

Although this is very simple example, logic for loading and merging configuration can be quite complex. In case of modular systems, configuration is collected from different sources, so some more advanced mechanism for that purpose would be used within bootstrapping.


Bootstrapping logic can become cumbersome and duplicated between projects, so I've created a library called Phoundation, which allows me to have less verbose bootstrap file:

$bootstrap = new Phoundation\Bootstrap\Bootstrap(
    new Phoundation\Config\Loader\FileConfigLoader(glob(
        sprintf('config/{{,*.\}global,{,*.}%s}.php', getenv('APP_ENV') ?: 'local'), 
    new Phoundation\Di\Container\Factory\ZendServiceManagerFactory()
$diContainer = $bootstrap();

return $diContainer;

Full example

To have an overall picture of the matter, take this simple blog engine application as an example, which can be used both through web browser (public/index.php) and a command-line (bin/app). It uses Slim micro-framework for the web part of the application and Symfony Console for the CLI part.

Project structure

    Framework/                                # general-purpose code, interfaces, adapters for framework components
    Post/                                     # domain code
    User/                                     # domain code


return [
    'di' => [
        'factories' => [
            //Domain services
            Blog\User\UserService::class => Blog\User\UserServiceFactory::class,
            Blog\User\UserRepository::class => Blog\User\UserRepositoryFactory::class,
            Blog\Post\PostService::class => Blog\Post\PostServiceFactory::class,
            Blog\Post\PostRepository::class => Blog\Post\PostRepositoryFactory::class,

            Blog\User\Web\ViewUserAction::class => Blog\Framework\Web\ActionFactory::class,
            Blog\Post\Web\SubmitPostAction::class => Blog\Framework\Web\ActionFactory::class,
            Blog\Post\Web\ViewPostAction::class => Blog\Framework\Web\ActionFactory::class,

            //App-wide (system) services
            Blog\Framework\Queue\QueueClientInterface::class => Blog\Framework\Queue\QueueClientFactory::class,
            Psr\SimpleCache\CacheInterface::class => Blog\Framework\Cache\CacheFactory::class,

            //App runners
            'App\Web' => Blog\Framework\WebAppFactory::class,
            'App\Console' => Blog\Framework\ConsoleAppFactory::class,


#!/usr/bin/env php

/* @var \Interop\Container\ContainerInterface $container */
$container = require __DIR__ . '/../src/bootstrap.php';

/* @var $app \Symfony\Component\Console\Application */
$app = $container->get('App\Console');



use Slim\Http\Request;
use Slim\Http\Response;

/* @var \Interop\Container\ContainerInterface $container */
$container = require __DIR__ . '/../src/bootstrap.php';

/* @var $app \Slim\App */
$app = $container->get('App\Web');

$app->get('/', function (Request $request, Response $response) {
    return $this->get('view')->render($response, 'app::home');
$app->get('/users/{id}', Blog\User\Web\ViewUserAction::class);
$app->get('/posts/{id}', Blog\Post\Web\ViewPostAction::class);
$app->post('/posts', Blog\Post\Web\SubmitPostAction::class);


Final thoughts

Described concept is a skeleton, a shell around a core codebase consisting of the domain logic supported by different general-purpose components. This shell provides a solid basis for building applications using libraries and tools of your choice.

When starting a new project, you should not be asking "what framework should I use?", but "which components should I use?".