- PVSM.RU - https://www.pvsm.ru -
Эра фулстэк фрэймворков в прошлом. Современные разработчики фрэймворков разделяют свои монолитные репозитории на компоненты с помощью ответвлений в Git, позволяя разработчику выбрать то, что действительно необходимо его проекту. Это означает, что вы можете построить свое приложение на топовых Zend Service Manager, Aura Router, Doctrine ORM, Laravel (Illuminate) Eloquent, Plates, Monolog, Symfony Cache или любых других компонентах, которые можно установить через Composer.

Основным шагом является создание и поддержка строгой структуры проекта, с целью установки и комбинирования компонентов из инфраструктуры любых фрэймворков. Я посвятил этому целую статью [1], чтобы охватить вопросы структуры каталогов, организации и группировки исходников, соглашений об именовании и прочим связанным вещам.
В течении разработки проекта, необходимо всегда уделять внимание бизнес логики ядра. Для всех общих задач, которые необходимо реализовать в вашем проекте, вы должны использовать различные 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% разделить код от фрэймворка, только если вы совсем не используете его, но вы можете значительно уменьшить связанности. Создайте интерфейсный уровень [2] абстракций и разделите ваш код на внешние зависимости или используете PSR интерфейсы [3] для того, чтобы снизить трудозатраты при переходе на альтернативные имплементации компонента. Короче говоря, создание интерфейсов – является лучшей практикой, который вы должны овладеть и уметь применять ее на деле.
В идеале, вот список того, где у вас могут быть прямые зависимости:
Вместо того, чтобы хардкодом писать параметры для подключения к БД, вы должны использовать отдельные файлы, в которых можно переопределить различные настройки. Это будет полезно, при использовании разных сред (например, для разработки, для продакшен версии и т.д.)
Существуют несколько подходов при конфигурации файлов. Самым распространенным является наличие одного конфигурационного файла для каждой из сред, который, соответственно, загружается в зависимости от установленной переменной среды:
config/
config_development.php
config_production.php
config_testing.php
Основным недостатком такого подхода является дублирование параметров в нескольких конфигурационных файлах.
Я предпочитаю другой способ для работы с конфигурацией сред, который практикует Zend Framework (о нем хорошо написано в документации [4]). При использовании этого метода, структура конфигурационных файлов выглядит так:
config/
database.global.php
development.php
global.php
logger.global.php
production.php
services.global.php
В этом примере параметры могут быть четко распределены по разным конфигурационным файлам, основываясь на их назначении, при этом они переопределяются в зависимости от среды окружения. Такие конфигурационные файлы содержат только переопределяемые параметры. Эти файлы объединяются в единую конфигурацию с помощью glob brace [5].
Практическое использование инъекции зависимостей (Dependency Injection) очень важна для гибкости и надежности вашего кода. DI контейнер – это ключевая концепция, которая управляет логикой при построении блоков вашего приложения.
Вот что должно быть определено в DI контейнере:
Все эти объекты называются сервисами. Сервис – это общее имя для любого PHP объекта, который служит определенной цели (например, отправка почты) и используется в приложении лишь тогда, когда нам действительно необходим конкретный функционал. Если сервис имеет сложную логику построения (имеет зависимости) или является зависимостью для другого класса, и не предназначен для создания нескольких экземпляров внутри одного запроса, то он должен быть зарегистрирован в DI контейнере.
Другие группы классов представляют такие типы как доменные объекты, сущности, значения объектов. Думайте о User, Post, DateTime, как о конкретных примерах этих классов. Все они не являются сервисами, поэтому не должны определятся в контейнере.
Вместо того, чтобы программно заполнять DI контейнер, логичнее определить все зависимости внутри конфигурации:
return [
'di' => [
'factories' => [
PsrSimpleCacheCacheInterface::class => AppCacheCacheFactory::class,
AppDbDbAdapterInterface::class => AppDbDbAdapterFactory::class,
AppUserUserService::class => AppUserUserServiceFactory::class,
AppUserUserRepository::class => AppUserUserRepositoryFactory::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 ZendServiceManagerServiceManager($config['services']);
$diContainer->set('Config', $config);
return $diContainer;
DI контейнер – это конечный результат операции начальной загрузки, через который реализуются все дальнейшие действия.
Хотя это очень простой пример, логика загрузки и слияния конфигурации может быть достаточно сложной. В случае модульных систем, конфигурация собирается из разных источников, поэтому в бутстрэппинге будет использован более расширенный механизм настройки.
Логика бутстрэппинга может быть достаточно громоздкой и дублироваться между проектами, поэтому я создал библиотеку Phoundation, благодаря которой у меня получается более компактный загрузочный файл:
$bootstrap = new PhoundationBootstrapBootstrap(
new PhoundationConfigLoaderFileConfigLoader(glob(
sprintf('config/{{,*.}global,{,*.}%s}.php', getenv('APP_ENV') ?: 'local'),
GLOB_BRACE
)),
new PhoundationDiContainerFactoryZendServiceManagerFactory()
);
$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
BlogUserUserService::class => BlogUserUserServiceFactory::class,
BlogUserUserRepository::class => BlogUserUserRepositoryFactory::class,
BlogPostPostService::class => BlogPostPostServiceFactory::class,
BlogPostPostRepository::class => BlogPostPostRepositoryFactory::class,
BlogUserWebViewUserAction::class => BlogFrameworkWebActionFactory::class,
BlogPostWebSubmitPostAction::class => BlogFrameworkWebActionFactory::class,
BlogPostWebViewPostAction::class => BlogFrameworkWebActionFactory::class,
//App-wide (system) services
BlogFrameworkQueueQueueClientInterface::class => BlogFrameworkQueueQueueClientFactory::class,
PsrSimpleCacheCacheInterface::class => BlogFrameworkCacheCacheFactory::class,
//App runners
'AppWeb' => BlogFrameworkWebAppFactory::class,
'AppConsole' => BlogFrameworkConsoleAppFactory::class,
],
],
];
bin/app
#!/usr/bin/env php
<?php
/* @var InteropContainerContainerInterface $container */
$container = require __DIR__ . '/../src/bootstrap.php';
/* @var $app SymfonyComponentConsoleApplication */
$app = $container->get('AppConsole');
$app->run();
public/index.php
use SlimHttpRequest;
use SlimHttpResponse;
/* @var InteropContainerContainerInterface $container */
$container = require __DIR__ . '/../src/bootstrap.php';
/* @var $app SlimApp */
$app = $container->get('AppWeb');
$app->get('/', function (Request $request, Response $response) {
return $this->get('view')->render($response, 'app::home');
})->setName('home');
$app->get('/users/{id}', BlogUserWebViewUserAction::class);
$app->get('/posts/{id}', BlogPostWebViewPostAction::class);
$app->post('/posts', BlogPostWebSubmitPostAction::class);
$app->run();
Описанная концепция — это скелет, оболочка вокруг ядра кодовой базы, состоящая из логики домена, поддерживаемой различными компонентами общего назначения. Эта оболочка является фундаментом для создания приложений с использованием библиотек и инструментов на свой вкус.
Когда приступаешь к новому проекту, вопрос должен быть не в том «какой фреймворк мне использовать?», а в том «какие компоненты я буду использовать в проекте?».
Автор: TutmeeAgency
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/254269
Ссылки в тексте:
[1] целую статью: http://blog.nikolaposa.in.rs/2017/01/16/on-structuring-php-projects/
[2] интерфейсный уровень: http://blog.nikolaposa.in.rs/2016/08/10/using-3rd-party-code/
[3] PSR интерфейсы: http://www.php-fig.org/psr/
[4] документации: https://docs.zendframework.com/tutorials/advanced-config/#environment-specific-application-configuration
[5] glob brace: http://php.net/manual/en/function.glob.php
[6] Источник: https://habrahabr.ru/post/327746/
Нажмите здесь для печати.