📜 ⬆️ ⬇️

Building flexible PHP applications

The era of fulstak frameworks in the past. Modern framework developers share their monolithic repositories into components using branches in Git, allowing the developer to choose what his project really needs. This means that you can build your application on top Zend Service Manager, Aura Router, Doctrine ORM, Laravel (Illuminate) Eloquent, Plates, Monolog, Symfony Cache or any other components that can be installed via Composer.

image

Reliable project structure


The main step is to create and maintain a strict project structure, in order to install and combine components from the infrastructure of any frameworks. I dedicated a whole article to this to cover the structure of directories, the organization and grouping of sources, naming conventions and other related things.

Choosing the right tool for the job.


During the development of the project, it is necessary to always pay attention to the core logic business. For all common tasks that need to be implemented in your project, you must use various open source solutions, components and libraries that will facilitate the application development process. DBAL, ORM, routing, mailer, cache, logger - this is not a complete list of examples of what does not need to be re-created.
')
Let me remind you that you can use components regardless of the framework (Zend Framework, Symfony, Laravel, Aura, etc.) Accordingly, the dependencies in the created composer.json may look like this:

{ "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" } } 

Framework Components


Using various components of the framework gives us a great advantage, but if you do not use them deliberately, this can lead to hopeless situations. The main, but not simple task, is the separation of your business logic framework or library for offline use. If you do not pay enough attention to this task, then you may have problems when trying to switch to a component of another developer, or even when updating the version of the current component.

It is impossible to 100% separate the code from the framework only if you do not use it at all, but you can significantly reduce the connectivity. Create an interface level of abstractions and divide your code into external dependencies or use PSR interfaces in order to reduce labor costs when switching to alternative component implementations. In short, the creation of interfaces - is the best practice that you should master and be able to apply it in practice.
Ideally, here is a list of where you can have direct dependencies:


Configuration management


Instead of writing parameters for connecting to the database with a hardcode, you should use separate files in which you can override various settings. This will be useful when using different environments (for example, for development, for production versions, etc.)
There are several approaches to configuring files. The most common is the presence of one configuration file for each of the environments, which, respectively, is loaded depending on the environment variable:

 config/ config_development.php config_production.php config_testing.php 

The main disadvantage of this approach is the duplication of parameters in several configuration files.

I prefer another way to work with the configuration of environments that the Zend Framework practices (it is well written in the documentation ). When using this method, the structure of the configuration files looks like this:

 config/ database.global.php development.php global.php logger.global.php production.php services.global.php 

In this example, the parameters can be clearly distributed among different configuration files, based on their purpose, and they are redefined depending on the environment. Such configuration files contain only override parameters. These files are combined into a single configuration using glob brace .

Dependency injection


The practical use of Dependency Injection is very important for the flexibility and reliability of your code. A DI container is a key concept that controls the logic when building blocks of your application.

Here is what should be defined in the DI container:


All of these objects are called services. A service is a common name for any PHP object that serves a specific purpose (for example, sending mail) and is used in an application only when we really need a specific functionality. If the service has complex build logic (has dependencies) or is a dependency for another class, and is not intended to create multiple instances within a single request, then it must be registered in a DI container.

Other groups of classes represent such types as domain objects, entities, values ​​of objects. Think of User, Post, DateTime as concrete examples of these classes. All of them are not services, so they should not be defined in a container.

Configure DI container


Instead of programmatically filling the DI container, it is more logical to determine all dependencies within 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, such as, for example, Zend Service Manager, support this approach out of the box, otherwise you will have to write simple logic to populate it based on the configuration array.

You may have noticed that I prefer to use the full name of the interface as the name of the service on which the interface is implemented. In places where there is no interface, I use the full class name. The reason is simple; retrieving services from containers not only makes the code more readable, it also makes it easier for the user to understand what it is working with.

Bootstrapping


The code that loads the configuration and initializes the DI container is usually contained in a so-called bootstrap script. Depending on the configuration and implementation of the DI container, it can take the following forms:

 $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; 


A DI container is the end result of a boot operation, through which all further actions are implemented.

Although this is a very simple example, the logic of loading and merging configurations can be quite complex. In the case of modular systems, the configuration is collected from various sources, so a more advanced configuration mechanism will be used in bootstrapping.

Phoundation


The bootstrapping logic can be quite cumbersome and duplicate between projects, so I created the Phoundation library, thanks to which I get a more compact boot file:

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

Full example


To get the big picture, take, as an example, this is a simple blogging application that can be used both through a browser (public / index.php) and through the command line (bin / app). It uses the micro-framework Slim for the web part of the application and the symfony Console for CLI.

Project structure

 bin/ app config/ database.global.php development.php global.php production.php services.global.php public/ index.php src/ Framework/ # general-purpose code, interfaces, adapters for framework components Cache/ CacheFactory.php Logger/ Handler/ IndexesCapableMongoDBHandler.php Queue/ PheanstalkQueueClient.php QueueClientInterface.php QueueClientFactory.php Web/ ActionFactory.php ConsoleAppFactory.php WebAppFactory.php Post/ # domain code Web/ SubmitPostAction.php ViewPostAction.php Post.php PostRepository.php PostRepositoryFactory.php PostService.php PostServiceFactory.php User/ # domain code CLI/ CreateUserCommand.php Web/ ViewUserAction.php User.php UserRepository.php UserRepositoryFactory.php UserService.php UserServiceFactory.php bootstrap.php 

config / services.global.php

 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, ], ], ]; 

bin / app

 #!/usr/bin/env php <?php /* @var \Interop\Container\ContainerInterface $container */ $container = require __DIR__ . '/../src/bootstrap.php'; /* @var $app \Symfony\Component\Console\Application */ $app = $container->get('App\Console'); $app->run(); 

public / index.php

 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'); })->setName('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); $app->run(); 

Summing up


The concept described is a skeleton, a shell around the code base kernel, consisting of domain logic supported by various general purpose components. This shell is the foundation for building applications using libraries and tools to your taste.

When you start a new project, the question should not be “what framework should I use?”, But “what components will I use in the project?”.

Source: https://habr.com/ru/post/327746/


All Articles