
Надежная структура проекта
Основным шагом является создание и поддержка строгой структуры проекта, с целью установки и комбинирования компонентов из инфраструктуры любых фрэймворков. Я посвятил этому целую статью, чтобы охватить вопросы структуры каталогов, организации и группировки исходников, соглашений об именовании и прочим связанным вещам.
Выбор правильного инструмента для работы
В течении разработки проекта, необходимо всегда уделять внимание бизнес логики ядра. Для всех общих задач, которые необходимо реализовать в вашем проекте, вы должны использовать различные open source решения, компоненты и библиотеки, который облегчат процесс разработки приложения. DBAL, ORM, routing, mailer, cache, logger – это далеко не полный список примеров того, что не нужно заново создавать.
Напомню, что вы можете использовать компоненты независимо от фрэймворка (Zend Framework, Symfony, Laravel, Aura и т.д.) Соответственно, зависимости в созданном composer.json могут выглядеть так:
{ "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" } }
Составляющие фрэймворка
Использование различных компонентов фрэймворка дает нам большое преимущество, но, если пользоваться ими не обдуманно, это может привести к безвыходным ситуациям. Главной, но не простой задачей, является разделение вашей бизнес логики фрэймворка или библиотеки для автономного использования. Если не уделить этой задаче достаточно внимания, то у вас могут возникнуть проблемы при попытке перейти на компонент другого разработчика или, даже, при обновлении версии текущего компонента.
Невозможно на 100% разделить код от фрэймворка, только если вы совсем не используете его, но вы можете значительно уменьшить связанности. Создайте интерфейсный уровень абстракций и разделите ваш код на внешние зависимости или используете PSR интерфейсы для того, чтобы снизить трудозатраты при переходе на альтернативные имплементации компонента. Короче говоря, создание интерфейсов – является лучшей практикой, который вы должны овладеть и уметь применять ее на деле.
В идеале, вот список того, где у вас могут быть прямые зависимости:
- Реализации сервисов, чтобы использовать абстрактные внешние зависимости
- Фабрики
- Middleware, контроллеры, заголовки, CLI, при этом предполагается, что все они не должны содержать в себе бизнес логики.
Управление конфигурацией
Вместо того, чтобы хардкодом писать параметры для подключения к БД, вы должны использовать отдельные файлы, в которых можно переопределить различные настройки. Это будет полезно, при использовании разных сред (например, для разработки, для продакшен версии и т.д.)
Существуют несколько подходов при конфигурации файлов. Самым распространенным является наличие одного конфигурационного файла для каждой из сред, который, соответственно, загружается в зависимости от установленной переменной среды:
config/ config_development.php config_production.php config_testing.php
Основным недостатком такого подхода является дублирование параметров в нескольких конфигурационных файлах.
Я предпочитаю другой способ для работы с конфигурацией сред, который практикует Zend Framework (о нем хорошо написано в документации). При использовании этого метода, структура конфигурационных файлов выглядит так:
config/ database.global.php development.php global.php logger.global.php production.php services.global.php
В этом примере параметры могут быть четко распределены по разным конфигурационным файлам, основываясь на их назначении, при этом они переопределяются в зависимости от среды окружения. Такие конфигурационные файлы содержат только переопределяемые параметры. Эти файлы объединяются в единую конфигурацию с помощью glob brace.
Инъекция зависимостей
Практическое использование инъекции зависимостей (Dependency Injection) очень важна для гибкости и надежности вашего кода. DI контейнер – это ключевая концепция, которая управляет логикой при построении блоков вашего приложения.
Вот что должно быть определено в DI контейнере:
- Сервисы общего назначения (database adapter, cache, mailer, logger и т.д.)
- Доменные сервисы, репозитории
- middleware, контроллеры, заголовки (да, у них есть зависимости для инъекций!)
- службы запуска Web и CLI приложений
Все эти объекты называются сервисами. Сервис – это общее имя для любого PHP объекта, который служит определенной цели (например, отправка почты) и используется в приложении лишь тогда, когда нам действительно необходим конкретный функционал. Если сервис имеет сложную логику построения (имеет зависимости) или является зависимостью для другого класса, и не предназначен для создания нескольких экземпляров внутри одного запроса, то он должен быть зарегистрирован в DI контейнере.
Другие группы классов представляют такие типы как доменные объекты, сущности, значения объектов. Думайте о User, Post, DateTime, как о конкретных примерах этих классов. Все они не являются сервисами, поэтому не должны определятся в контейнере.
Настройка DI контейнера
Вместо того, чтобы программно заполнять DI контейнер, логичнее определить все зависимости внутри конфигурации:
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, ], ], ];
Некоторые DI контейнеры, такие как, например, Zend Service Manager, поддерживают такой подход из коробки, в противном случае вам придется написать простую логику для его заполнения на основе массива конфигурации.
Возможно вы заметили, что я предпочитаю использовать полное имя интерфейса в качестве имени сервиса на котором реализуется интерфейс. В местах, где нет интерфейса, я использую полное имя класса. причина проста, извлечение служб из контейнеров не только делает код более читаемым, но и облегчает для пользователя понимание того с чем он работает.
Бутстрэппинг
Код, который загружает конфигурацию и инициализирует DI контейнер обычно содержится в, так называемом, сценарии начальной загрузки. В зависимости от конфигурации и реализации DI контейнера, он может принимать следующие формы:
$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 контейнер – это конечный результат операции начальной загрузки, через который реализуются все дальнейшие действия.
Хотя это очень простой пример, логика загрузки и слияния конфигурации может быть достаточно сложной. В случае модульных систем, конфигурация собирается из разных источников, поэтому в бутстрэппинге будет использован более расширенный механизм настройки.
Phoundation
Логика бутстрэппинга может быть достаточно громоздкой и дублироваться между проектами, поэтому я создал библиотеку Phoundation, благодаря которой у меня получается более компактный загрузочный файл:
$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;
Полный пример
Чтобы получить общую картину, возьмите, в качестве примера, это простое приложение для работы с блогами, которым можно воспользоваться как через браузер (public/index.php), так и через командную строку (bin/app). Он использует микро-фреймворк Slim для вэб части приложения и Symfony Console для CLI.
Структура проекта
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();
Подводя итоги
Описанная концепция — это скелет, оболочка вокруг ядра кодовой базы, состоящая из логики домена, поддерживаемой различными компонентами общего назначения. Эта оболочка является фундаментом для создания приложений с использованием библиотек и инструментов на свой вкус.
Когда приступаешь к новому проекту, вопрос должен быть не в том «какой фреймворк мне использовать?», а в том «какие компоненты я буду использовать в проекте?».

