Четыре столпа PSR

Мастер-класс по Symfony завершили, теперь пополним теперь рубрику ответов на вопросы.

Недавно пришло письмо с большим вопросом по мотивам нашей серии по PSR-7, на который родился ещё больший ответ. В качестве вопроса хотел привести цитаты, но для целостного понимания письмо с письменного разрешения автора привожу почти целиком:

Здравствуйте. Хотелось бы узнать ваше мнение о размышлении над PHP-FIG. Прошу прощения за сумбурность – трудно сформулировать мысль.

По сути FIG (Framework Interoperability Group) – группа взаимодействия framework'ов. Стандарты же называются PSR (PHP Standard Recommendations) – стандартные рекомендации PHP.

Но пол интернета кипит тем, что это именно стандарты и нужно следовать только им. Не следуешь – не нормальный программист.

То есть группа взаимодействия framework'ов пишет стандарты по работе с языком, что странно.

И меня беспокоит беспрекословное следование этим стандартам.

CodeStyle можно принять т.к у него нет побочного эффекта, и эти стандарты влияют только на форматирование самого кода.

Вопрос возникает к остальным стандартам:

 

PSR-11: Container Interface

Не понятна суть этого стандарта.

По сути – нам доступно два метода: has и get. Но прокидывать куда-то контейнер – плохая идея. Если его прокинуть, то он превращается в сервис-локатор, что само по себе имеет минусы.

Правильнее – настроить контейнер и получать из него определенный класс, в который в глубину будут автоматически инжектиться остальные зависимости. При этом применение даже в framework'ах всего этого вызывает сомнение – при замене контейнера все-равно придется переписывать все те места, в которых происходит заполнение контейнера.

В том-же symfony – это происходит внутри бандлов, в laravel в провайдерах. Что делает невозможным легкую замену контейнеру. Можно было бы написать interface для контейнера, но тут уже получится большой костыль – все контейнеры биндят зависимости по разному и имеют разный функционал. Разве нет?

 

PSR-15: HTTP Server Request Handlers

Тут мы имеем RequestHandlerInterface и MiddlewareInterface.

В каждом по одному методу:

– handle(ServerRequestInterface $request): ResponseInterface; – process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;

Также – не ясно зачем это нужно?

RequestHandlerInterface – не вижу ни одной причины менять его. Если нужно будет менять его – придется менять весь framework, т.к по сути – это его каркас.

MiddlewareInterface – в стандарте даже не имеет связи с RequestHandlerInterface. И в чем сложность – написать свой middleware внутри которого вызывать метод какого-либо подключенной зависимости?

 

PSR-17: HTTP Factories

Также – не ясна причина, из-за которой будет необходимо поменять request|response. Прокидывать внутрь проекта request|respone – плохо. Разве нет? И даже если нужно будет поменять их – при правильной логике придется поправить только в middleware и в контроллерах. Тот-же symfony до сих пор использует свой http foundation.

 

Для каких-то вещей, вроде, роутера (у вас есть в уроках) – можно прокинуть интерфейс и написать adapter, необходимость замены в будущем остальных вещей – вызывает сомнение, но и это при отделении логики можно будет легко произвести. Есть паттерны, разделение проекта на части. Что еще нужно?

Если я захочу поменять кэш, например – напишу адаптер для интерфейса нужной мне библиотеки.

Для тех же, кто в ответ на "У всех свои проблемы" отвечает "У большинства проблемы одинаковые" – будет полностью писать на определенном framework'е – laravel, symfony, yii и.т.д., их библиотеки и будет завязан на те-же бандлы, контейнеры, конфиги, доктрину и т.д., и не будет заморачиваться работой в стиле: "сегодня заменю библиотеку для config, через месяц заменю http, еще через месяц роутер" и т.д.

Смущает стандартизация не только стиля программирования, но и способов реализации, от которой не видно настолько большой выгоды, о которой говорят.

Хотел бы услышать ваше мнение на это рассуждение.

Здравствуйте! Да, я часто говорю и пишу про PSR-ы. Раз непонятных моментов и претензий к ним возникает много, то стоит разделить рекомендации на смысловые группы с сайта PHP-FIG, но в более удобном порядке:

Coding Styles

  • PSR-1: Basic Coding Standard
  • PSR-12: Extended Coding Style Guide

Autoloading

  • PSR-4: Improved Autoloading

Interfaces

  • PSR-3: Logger Interface
  • PSR-6: Caching Interface
  • PSR-11: Container Interface
  • PSR-13: Hypermedia Links
  • PSR-14: Event Dispatcher
  • PSR-16: Simple Cache

HTTP:

  • PSR-7: HTTP Message Interfaces
  • PSR-15: HTTP Handlers
  • PSR-17: HTTP Factories
  • PSR-18: HTTP Client

Они и представляют пока четыре столпа актуальных рекомендаций. Числа здесь не все, так как остальные либо уже устарели, либо до сих пор не приняты.

CodeStyle можно принять т.к у него нет побочного эффекта, и эти стандарты влияют только на форматирование самого кода.

Так что первая группа – это...

Всеобщий Code Style

Пока в других языках не утихают споры про «табы vs пробелы», пока каждая компания выпускает свои гайды:

и пока там до сих пор рушатся чьи-то судьбы

знайте, что в PHP с форматированием кода нам повезло.

В своих проектах у всех программистов вкусы и привычки разные. Самому себе можно оформлять код как угодно. Но как только программисты начинают использовать чужие компоненты или переходят из проекта в проект, то такое разнообразие начинает напрягать. В более распущенных случаях в одном файле проекта разные куски кода оказываются написанными разными программистами в разных стилях.

Для борьбы с бесполезным разнообразием стилей теперь для PHP есть конкретно описанные рекомендации PSR-1, PSR-2 и PSR-12.

Пусть рождённые до принятия этих рекомендаций Wordpress и Yii1 любят или не любят ставить или не ставить пробелы до/после скобок и называть поля в snake_case/camelCase, но сейчас с новыми фреймворками нам проще. Это даёт хорошие бонусы:

  • Никому не нужно изобретать свои стили, как все это делают для Java. Можно всё делать по единым для всего мира PSR.
  • Умные IDE содержат автоформатирование, настроенное по PSR. Стоит нажать Reformat Code в PhpStorm и код автоматически выровнится.
  • Есть PHP CodeSniffer, который может в консоли проверить и автоматически исправить код по тем же PSR.

В итоге если все программисты привыкнут оформлять код одинаково по PSR, то будет проще всем.

А в языке Go с этим всё ещё строже, так как при неверном форматировании вылетит ошибка компиляции.

Так что PSR для Code Style – это как правила хорошего тона. Можете не придерживаться, но тогда постоянно вам будут об этом напоминать.

Автозагрузка

Идея PSR-4 проста: путь в namespace и имя класса совпадают с именами папок и файла.

Стоит где-то в одном месте указать автозагрузчику, что базовый неймспейс App соответствует папке src, так сразу класс App\Blog\Entity\Post спокойно можно будет найти в файле src/Blog/Entity/Post.php любому PSR-4-совместимому загрузчику.

Если по такому принципу будут работать все фреймворки и библиотеки, то можно будет пользоваться одним общим автозагрузчиком.

Делая свой мини-фреймворк или мини-CMS вам может быть лень подключать Composer. И может показаться, что проще сложить классы по-особому и сочинить свой загрузчик с spl_autoload_register для импорта ваших классов из файлов. Если показалось, но вы решили выложить этот код на GitHub, то ущипните себя за что-нибудь.

Почему? Потому что как только я возьму вашу CMS и начну на ней что-то делать, то её кода мне скорее всего не хватит. Помимо него я захочу подключить готовый Markdown-парсер и почтовик вроде SwiftMailer. Или даже без них захочу подключить PHPUnit или Codeception для тестов. И для этого мне... всё равно придётся подключать Composer. И после этого ещё и скрещивать ваш автозагрузчик с имеющимся в Composer, чтобы они теперь не мешали друг-другу.

Так что в любой ситуации удобнее будет придерживаться именования файлов по PSR-4 и подключить Composer. Другим PSR-ам следуйте по вкусу, а автозагрузку не ломайте.

Итак, с рекомендациями по форматированию и автозагрузке разобрались. Их желательно придерживаться независимо от ваших вкусов. А теперь переходим к вещам менее важным опциональным.

Обобщённые интерфейсы

Для каких-то вещей, вроде, роутера (у вас есть в уроках) – можно прокинуть интерфейс и написать adapter, необходимость замены в будущем остальных вещей – вызывает сомнение, но и это при отделении логики можно будет легко произвести. Есть паттерны, разделение проекта на части. Что еще нужно?

Если я захочу поменять кэш, например – напишу адаптер для интерфейса нужной мне библиотеки.

Это вопрос к списку:

  • PSR-3: Logger Interface
  • PSR-6: Caching Interface
  • PSR-16: Simple Cache
  • PSR-14: Event Dispatcher
  • PSR-18: HTTP Client

Адаптеры для своего кода – это понятно. Но что насчёт не своего?

Вася пишет библиотеку для определения города по IP-адресу. Сочинил класс Locator:

src/
    Locator.php

Теперь подумал, что неплохо бы вынести HTTP-клиент. Написал свой:

src/
    Locator.php
    HttpClient.php

Ну и лучше интерфейс для клиента предусмотреть:

src/
    Locator.php
    HttpClientInterface.php
    HttpClient.php

И неплохо бы добавить логер. Написал свой логер:

src/
    Locator.php
    HttpClientInterface.php
    HttpClient.php
    FileLogger.php

Потом решил, что было бы неплохо не ограничиваться файловыми логами, а объявить интерфейс:

src/
    Locator.php
    HttpClientInterface.php
    HttpClient.php
    LoggerInterface.php
    FileLogger.php

чтобы другие программисты могли вместо файлового свои логеры через адаптеры подсовывать.

Ну и чтобы слишком часто сторонний сервис локации не дёргать, решил добавить кэш:

src/
    Locator.php
    HttpClientInterface.php
    HttpClient.php
    LoggerInterface.php
    FileLogger.php
    CacheInterface.php
    FileCache.php

Всё, библиотека готова. Можно выкладывать на GitHub.

Теперь чтобы подключить эту библиотеку к своему проекту я просто делаю:

composer require vasya/ip-locator

и пишу свои адаптеры для него

class YiiLoggerAdapter implements Vasya\LoggerInterface
{
    public function error(string $message, string $ip): void
    {
        Yii::error($message . ': ' . $ip, 'ip-locator'); 
    }
}
class YiiCacheAdapter implements Vasya\CacheInterface
{
    public function get(string $id)
    {
        return Yii::$app->cache->get($id); 
    }
 
    ...
}
class GuzzleHttpClientAdapter implements Vasya\HttpClientInterface
{
    public function get(string $url)
    {
        return ...; 
    }
}

И всё работает.

Но проекты обычно большие. И так через Composer я установил себе восемь чужих библиотек. И вижу, что к каждой библиотеке её автор написал свои собственные Logger и Cache.

Итого я имею восемь библиотек со своими логерами и кэшами и мне для них нужно сочинить шестнадцать адаптеров. Грустно...

Но что за глобальная несправедливость такая? Почему тысячи программистов для своих тысяч библиотек тысячи раз пишут логеры и кэшеры? Вот бы было классно, если бы все программисты договорились и сделали один общий LoggerInterface и CacheInterface.

Вот они и договорились в PHP-FIG и сделали всё общее.

Если теперь Васе нужно в своём компоненте сделать логирование, то вместо своего самописа он спокойно принимает Psr\Log:

namespace Vasya\IpLocator;
 
use Psr\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface;
 
class Locator
{
    private $client;
    private $logger;
 
    public function __construct(HttpClientInterface $client, LoggerInterface $logger)
    {
        $this->client = $client;
        $this->logger = $logger;
    }
 
    ...
}

Теперь я могу передать ему любой PSR-совместимый клиент и PSR-логер фреймворка напрямую без адаптеров:

$locator = new Locator($client, $logger);

Так что не только наш код может использовать чужие компоненты, но и чужим компонентам порой нужен наш код.

Если теперь я задумал через Composer установить себе восемь чужих библиотек, то если все они принимают PSR-овские логер и кэш, то я спокойно закину в них свои monolog/monolog и zendframework/zend-cache без всяких адаптеров.

В итоге имеем общепринятые интерфейсы LoggerInterface, CacheInterface, EventDispatcherInterface и рассматриваемый нами далее HttpClientInterface. Быстро и удобно.

Теперь Вася вместо кучи кода:

src/
    Locator.php
    HttpClientInterface.php
    HttpClient.php
    LoggerInterface.php
    FileLogger.php
    CacheInterface.php
    FileCache.php

может сочинить всего один файл:

src/
    Locator.php

и в нём завязаться на общепринятые интерфейсы.

Теперь клиент, логер и кэш к нему можно подключить любые готовые на усмотрение другого программиста.

Framework Interoperability сэкономил время на написание кода автору и на написание адаптеров пользователю библиотеки.

PSR-11: Container Interface

Не понятна суть этого стандарта.

По сути – нам доступно два метода: has и get. Но прокидывать куда-то контейнер – плохая идея. Если его прокинуть, то он превращается в сервис-локатор, что само по себе имеет минусы.

Правильнее – настроить контейнер и получать из него определенный класс, в который в глубину будут автоматически инжектиться остальные зависимости. При этом применение даже в framework'ах всего этого вызывает сомнение – при замене контейнера все-равно придется переписывать все те места, в которых происходит заполнение контейнера.

В том-же symfony – это происходит внутри бандлов, в laravel в провайдерах. Что делает невозможным легкую замену контейнеру. Можно было бы написать interface для контейнера, но тут уже получится большой костыль – все контейнеры биндят зависимости по разному и имеют разный функционал. Разве нет?

Да, прокидывать куда-то контейнер – плохая идея. И если его прокинуть, то он превращается в сервис-локатор, что само по себе имеет минусы.

Но всё-таки придётся в самом верху именно получить из него корневой сервис, в который в глубину будут автоматически инжектиться зависимости. И есть компоненты и фреймворки, которым для работы мы передаём именно сам контейнер.

Например, классу Slim\App или ControllerResolver любого фреймворка или микрофреймворка он нужен, чтобы доставать оттуда контроллер через $container->get($controllerName).

Также к шине команд league/tactician прилагается готовый резолвер ContainerLocator, который достаёт обработчик команды из PSR-контейнера как из локатора через $container->get($handlerName).

При замене контейнера все-равно придется переписывать все те места, в которых происходит заполнение контейнера.

Код Application, ControllerResolver и HandlerLocator всех библиотек и микрофреймворков использует контейнер только на чтение методами has и get. Им не интересно, чем и как мы переданный контейнер заполняем.

И если мне при разработке проекта на микрофреймворке Slim 3 с удобным роутером не нравился его примитивный контейнер Slim\Container без автовайринга, ленивой загрузки и фабрик, то я его могу заменить на zend/service-manager и передать в new Slim\App($container) уже его.

И установив Tactician я могу ему спокойно передать любой контейнер из моего Yii, Symfony, Laravel, Slim или Zend, так как все они реализуют общий ContainerInterface.

А так да, при переходе на другой контейнер мы переписываем только его заполнение в конфиге.

А дальше у нас...

Обобщение работы с HTTP

С общими интерфейсами компонентов разобрались. Они описывают простые конкретные библиотеки.

Из этой группы к ним можно отнести PSR-18 для описания компонента HTTP Client. А PSR-15 уже является обобщением для построения целых фреймворков.

Вся группа работает поверх PSR-7. С него и начнём.

PSR-7: HTTP Messages

Откуда всё это пошло и зачем понадобилось?

С данными запроса в PHP ввиду его процедурного прошлого мы можем работать напрямую через набор разрозненных глобальных массивов $_SERVER, $_GET, $_POST, $_FILES, $_COOKIES. У $_SESSION особый случай, так как сессии работают через файлы и Cookies.

Но передавать такую большую пачку было бы дико:

$controller->doAction($_SERVER, $_GET, $_POST, $_FILES, $_COOKIES);

В синхронных PHP-приложениях можно и не передавать, а дёргать напрямую из этих массивов. Но в асинхронном режиме это не сработает.

Для удобства можно их совместить в массив:

$request = [
    'server' => $_SERVER,
    'query' => $_GET,
    'data' => $_POST,
    'files' => $_FILES,
    'cookies' => $_COOKIES,
];
 
$response = $controller->doAction($request);

чтобы можно было отвязаться от суперглобальных массивов и даже работать вообще без них.

Но вместо процедурной работы с переменными:

echo $request['server']['HTTP_HOST'] . ' ' . $request['server']['REQUEST_URI'];

удобнее работать с объектом $request:

echo $request->getHost() . ' ' . $request->getUri();

Для этого в PHP тоже можно написать свой класс:

$request = new Request($_SERVER, $_GET, $_POST, $_FILES, $_COOKIES);
$response = $controller->doAction($request);

И всё бы работало в реале и легко эмулировалось в тестах.

В экосистеме NodeJS в наборе http.* есть устоявшиеся классы ClientRequest IncomingMessage и ServerResponse и многие фреймворки пишут на них.

В PHP с унификацией HTTP сложнее, так как в ядре нет своих собственных HTTP-объектов. И фреймворки появились раньше, чем стало модно распространять компоненты через Composer.

PSR-7 как раз и решил ввести общие RequestInterface, ServerRequestInterface и ResponseInterface, заточенные на работу без оглядки на суперглобальные массивы. И сделал их иммутабельными для безопасной работы в асинхронных EventLoop-ах.

Зачем ещё это можно обобщать, если каждому фреймворку со своими классами запроса и ответа и так хорошо? В общем смысле с HTTP связано много интересных вещей. Например, атутентификация, HTTP-кеширование. Да и сами контроллеры.

Можно придти к мысли, что сделав обобщённые Request и Response можно будет не делать общие утилиты и контроллеры.

Каждый фреймворк это делал по-разному. Но после прихода PSR-7 на данный момент имеем:

  • популярный набор symfony/http-foundation, на котором работают Symfony и Laravel;
  • универсальный psr/http-message для написания фреймворконезависимого кода;
  • свои классы Request и Response других фреймворков вроде Yii;

В Symfony используются свои классы, но имеется мост symfony/psr-http-message-bridge для конвертации Symfony\HttpFoundation в Psr\Message и обратно. В итоге в Symfony и Laravel в контроллер можно принимать хоть HttpFoundation\Request, хоть универсальный Psr\Message\Request.

PSR-18: HTTP Client

На сайте жены по бисероплетению я сделал виджет вывода фотографий из Instagram через Instagram PHP Scrapper, подключающийся через mashape/unirest-php.

Другие библиотеки для доступа к чужим API разных SMS-провайдеров, рассылок и других сервисов работают то напрямую через CURL, то через file_get_contents, то через Guzzle.

В итоге наставив кучу библиотек тоже получаем такой же зоопарк способов подключения. И чтобы банально указать им всем один прокси-сервер или добавить кэш приходится ковыряться в настройках каждого клиента и переопределять методы, где жёстко вписан curl.

Раз мы определились с общим Request, то в идеале для той же унификации сейчас появился повод перевести все библиотеки на использование PSR-18, чтобы им всем можно было передать один и тот же Psr\HttpClient, чтобы я мог подсунуть им логирующий, проксирующий или кеширующий декоратор для HttpClientInterface.

PSR-15: HTTP Server Request Handlers

Также не ясно зачем это нужно?

RequestHandlerInterface – не вижу ни одной причины менять его. Если нужно будет менять его – придется менять весь framework, т.к по сути – это его каркас.

MiddlewareInterface – в стандарте даже не имеет связи с RequestHandlerInterface. И в чем сложность – написать свой middleware внутри которого вызывать метод какого-либо подключенной зависимости?

PSR-17: HTTP Factories

Также – не ясна причина, из-за которой будет необходимо поменять request|response. Прокидывать внутрь проекта request|respone – плохо. Разве нет? И даже если нужно будет поменять их – при правильной логике придется поправить только в middleware и в контроллерах. Тот-же symfony до сих пор использует свой http foundation.

Вот мы и дошли до контроллеров и посредников.

Имея общие Request и Response помимо обобщённой работы в синхронных или асинхронных фреймворках мы можем обобщить и сами контроллеры с посредниками.

API своего проекта я сделал на микрофреймворке Zend Expressive. И мне понадобилось определять IP-адрес комментатора. Что мне делать? Можно сочинить самому цикл по обходу заголовков. Но вместо этого я подключил готовый IpAddress Middleware.

С появлением PSR-15 теперь можно безболезненно делиться наборами middleware единого формата. Например, я могу сочинить и из проекта в проект копировать свой TrailingSlashMiddleware для автообрезки слеша в конце адреса страницы:

namespace App\Gateway\Http\Middleware;
 
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\RedirectResponse;
 
class TrailingSlashMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $path = $request->getUri()->getPath();
 
        if ($path === '/') {
            return $handler->handle($request);
        }
 
        $normalized = rtrim($path, '/');
        if ($path !== $normalized) {
            return new RedirectResponse($request->getUri()->withPath($normalized));
        }
 
        return $handler->handle($request);
    }
}

Или выложу на GitHub. Но пока есть проблема. В PSR есть только интерфейс ResponseInterface, но нет самого класса Response. Я использую Zend\Diactoros\Response из пакета zend-diactoros:

use Zend\Diactoros\Response\RedirectResponse;
...
return new RedirectResponse($request->getUri()->withPath($normalized));

Но в другом проекте у меня вместо него Slim\Http\Response из пакета slim/http и пришлось бы переписать:

use Slim\Http\Response;
...
return (new Response(301))->withRedirect($request->getUri()->withPath($normalized));

И чтобы внутри универсальных Middleware не вписывать жёстко new Response там вместо этого можем использовать фабрику:

namespace App\Gateway\Http\Middleware;
 
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
 
class TrailingSlashMiddleware implements MiddlewareInterface
{
    private $factory;
 
    public function __construct(ResponseFactoryInterface $factory)
    {
        $this->factory = $factory;
    }
 
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $path = $request->getUri()->getPath();
 
        if ($path === '/') {
            return $handler->handle($request);
        }
 
        $normalized = rtrim($path, '/');
        if ($path !== $normalized) {
            return $this->factory->createResponse(301)
                ->withHeader('Location', $request->getUri()->withPath($normalized));
        }
 
        return $handler->handle($request);
    }
}

Здесь мы создаём Response через метод createResponse фабрики:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
 
return $this->factory->createResponse(301)
    ->withHeader('Location', $request->getUri()->withPath($normalized));
}

Теперь никакие классы Zend и Slim мы здесь не используем, поэтому TrailingSlashMiddleware можно выложить на GitHub и подключать к любому PSR-15-совместимому фреймворку. Достаточно только подсунуть ему фабрику из zend-diactoros или slim/http.

Также не ясна причина, из-за которой будет необходимо поменять request и response.

Из вышеизложенного можем сделать вывод, что внутри своего проекта Request и Response мы менять не собираемся. Они меняются лишь когда мы наш TrailingSlashMiddleware переносим из фреймворка во фреймворк или подключаем к нескольким проектам на разных фреймворках.

MiddlewareInterface позволяют делать универсальные переносимые middleware, которые можно выкладывать на GitHub и использовать в проектах на разных синхронных или асинхронных фреймворках.

RequestHandlerInterface аналогично бонусом позволяет делать фреймворконезависимые контроллеры. Но не для выкладывания, а для своего проекта. Получаем бонус в том, что можно начать проект на Zend Expressive, а потом перейти на Slim3 или Laravel практически не переписывая контроллеры и посредники.

Следовать или нет

Главный вопрос. Нужно ли всё это?

CodeStyle можно принять т.к у него нет побочного эффекта, и эти стандарты влияют только на форматирование самого кода. Вопрос возникает к остальным стандартам. Смущает стандартизация не только стиля программирования, но и способов реализации, от которой не видно настолько большой выгоды, о которой говорят.

Выгоды от PSR-ов не видны в своём проекте со своими компонентами, так как свой код мы можем сочинять как хотим. Когда каждый программист велосипедит свой Logger и Cache проблем не возникает.

Но как только начинаем работать с кучей чужих фреймворков и компонентов, так сразу сталкиваемся с горой проблем несовместимости чужих компонентов друг с другом и с нашими классами и фреймворком. И приходится в каждом ковыряться и сочинять адаптеры.

Здесь-то и становится видна польза универсальности и стандартизации для общемировой совместимости компонентов и фреймворков друг с другом.

Да, разные фреймворки следуют им в разной степени. Кто-то модифицирует свой код. Кто то прикладывает адаптеры, как Symfony выложила psr-bridge и убрав PSR-16 из компонета symfony/cache добавила к нему переходник Psr16Cache.

И меня беспокоит беспрекословное следование этим стандартам.

Если теперь при выборе очередной библиотеки с Packagist я увижу, что там лежит свой Logger, Container, Event Dispatcher или Cache, то мне станет грустно и я напишу issue с просьбой заменить их на PSR-овские, чтобы я и тысячи программистов с написанием адаптеров не мучались. Это разумно.

Не уверен. Обычно, адаптеры не содержат никакой логики, а являются просто мостом. Время затраченное на написание issue может легко сравниться со временем написания адаптера... Для корректной же смены данных частей, в.т.ч и фреймворка можно просто разделять код на слои. Не использовать Request\Response, передавать данные через DTO,

Так Framework Interoperability как раз и нужен для упрощения interoperability. Чтобы код был совместим из коробки без десятков адаптеров и DTO.

Но пол интернета кипит тем, что это именно стандарты и нужно следовать только им. Не следуешь – не нормальный программист.

  • Если пишешь только для себя, то пишешь как хочешь. Даже хоть без Composer.
  • Если пишешь что-то для публики и уместно следуешь общепринятому, то тебя хвалят.
  • Если пишешь публично, но ничему не следуешь, то тебя ругают.

Логично.

Сегодня принято одно, завтра другое. Тот-же Symfony – вышел из FIG. А сколько недовольства было по PSR Event Dispatcher...

То, что уже принято уже кардинально не изменится. А Symfony настолько крут, что может себе позволить выйти из FIG и сделать свой набор Contracts. При этом он не следует многим PSR нативно, но тут же сам предоставляет к ним адаптеры. Недовольство - недовольством, но теперь его Cache и Event Dispatcher начнут чаще использовать не для Symfony-проектов.

Существует множество библиотек и компонентов, которые не используют PSR, но при этом достаточно известнее PHP-FIG.

Да, есть монополисты, которые ещё держатся. Но если посмотреть на некоторых, то:

  • Логер Monolog сразу внедрил PSR-3 и теперь де-факто все подключают его.
  • Guzzle впрыгнула в тренд и внедрила PSR-7. PSR-18 в пути, но давно есть готовый адаптер.

Легаси-проекты могут также приложить к себе готовый опциональный адаптер.

Интервьюировался недавно, в несколько крупных компаний недавно, и от PSR там было только – стандарты оформления кода, да composer (не PSR). Никаких Http, Middleware, Container и.т.д. PSR интерфейсов.

Они не следуют общепринятому, но зная о том, что проект достаточно известный - нельзя сказать о том, что там сидят некомпетентные люди.

Крупные Bitrix-оиды не пользуются нэймспейсами и Composer. Крупные WordPress-ники не пользуются Git и ООП. Каждый пользуется только тем, что ему нужно и не пользуется тем, что ему неинтересно.

Framework Interoperability предоставляет обобщения для облегчения переносимости кода и компонентов между фреймворками.

Поэтому если работаете с несколькими фреймворками, пилите микросервисы на микрофреймворках, публикуете компоненты и не хотите завязываться на вендоров, то вам это нужно. Если же у вас только один проект на одном стеке на всю жизнь, то и думать ни о чём не нужно.

Сейчас стало популярно делать легковесные API для JS-фронтенда. И там вместо полноценных фреймворков проще собрать всё на микрофреймворке из компонентов. Так там PSR-ы очень кстати.

Напоследок: больше всего беспокоит то, что PHP-FIG продвигает стандарты, а не полное общество. Если бы данные моменты обсуждались на php.net гораздо большим сообществом, и реализовывались в виде SPL, то я имел бы другое мнение.

В данном же я вижу лишь продвижение интересов определенной группы людей.

В PHP уже лежат заброшенные HTTP Interface с 2014-го и Request and Response с 2016-го. Обсуждайте на здоровье всем сообществом :)

А в PHP-FIG участвуют именно авторы фреймворков и библиотек, знающие это изнутри.

Так что PSR-ы – вещь опциональная, но иногда полезная.

Комментарии

 

Andrey B.

Дмитрий, Вы даете развернутые ответы на вопрос "зачем нужно ...". Насколько я смог понять, автор писем тоже спрашивает, *почему* именно эти люди (разработчики фреймворков, Framework Interoperability Group) придумывают общие стандарты для всех PHP-программистов (PHP Standard Recommendations)?

Разработчики фреймворков имеют собственные интересы и цели, а в итоге, получается, они называют решения для своих (никто не спорит, важных для всех) проблем стандартом для всего мирового PHP-сообщества. В этом видится несогласованность, неясность.

Яркий пример того, что участники Framework Interoperability Group больше заботятся о себе и своих продуктах, чем об общих интересах сообщества — недавний уход Фабьена.

Ответить

 

Александр Макаров

PHP-FIG изначально был группой, которая придумывала рекомендации для того, чтобы разработчики фреймворков больше делали общего и меньше велосипедили. Цели сделать какие-то глобальные стандарты не было и никто из PHP-FIG не называл их таковыми.

Но по факту сообщество восприняло эти рекомендации именно как стандарты и пришлось группу немного реформировать чтобы она была более репрезентативной. В неё вошли ещё представители сообществ, которые фреймворки не разрабатывают. Сейчас же в рабочую группу может войти каждый. Так что PHP-FIG не закрыт, хотите что-то поменять там, возможности есть.

Уход Symfony (год назад) — это, конечно, плохо как для PSR, так и для Symfony. FIG лишился довольно сильных разработчиков, которые приносили хорошие технические аргументы. Symfony лишился возможности влиять на PSR и теперь просто их запиливает как есть. Тот же PSR-14, на который ругался Fabien, запилили некоторое время спустя.

То есть тут вопроса два:

1. Об интересах которого сообщества мы говорим. О Symfony или о PHP в целом?
2. PHP-FIG ли заботится об интересах своего продукта, а не PHP в целом?

Ответить

 

Александр Макаров

В PSR-11 get() нужен не для того, чтобы его вызывать в userland-коде, а для того чтобы написать то, что будет как раз рассовывать объекты из контейнера в нужные места как, например, injector.

В PSR-7 главный профит в облегчении функционального тестирования без поднятия полного окружения и возможности запуска под нетрадиционными окружениями типа Swoole или RoadRunner (это как раз замена реализации).

Ответить

 

Алексей Rox

Александр, про has/get в PSR-11 Вы, конечно, правы, и прокидывать контейнер тоже не дело (если это не ядро приложения), но автор письма говорит немного о другом и в том я его поддерживаю.
Он пишет о том, что нельзя так просто взять и заменить контейнер, ибо на стадии заполнения контейнера все контейнеры разные. Пробуя разные реализации контейнеров я сам с этим сталкивался. Хотя тот же контейнер Laravel даже PSR-11-му в полной мере не соответствует

Я вижу проблему PSR в том, что они не являются "необходимыми и достаточным" в полной мере.

На примере PSR-11: имеем один интерфейс ContainerInterface, в нём имеется has() и get() - необходимо и достаточно ли этого?
Если текущие реализации контейнеров в полной мере не взаимозаменяемы, то не достаточно. Возможно, как и в случае с PSR-7/17, проблему можно было бы решить с помощью ContainreFactoryInteface, который определил бы порядок заполнения. Но мне кажется почти нереальным подведение всех под одну гребёнку в этом вопросе
Вопрос необходимости has/get тоже остаётся открытым. Есть те, кто считает, что эти методы вообще не нужны, а я, например, думаю, что неплохо было бы ещё и call() добавить, ибо в разных реализациях он и так есть. Часто в фреймворках эти has/get и используются только для вызова методов и каждый пишет свой костыль-аналог call()

На примере PSR-3 (Logger): не предусмотрены ошибки самого логгера. От того Monolog вроде и соответствует PSR-3, но самого PSR-3 не достаточно в полной мере для логгера. И что, если логгеру запрещено бросать Exception, то теперь каждый логгер будет придумывать свой потенциально несовместимый exceptionHandler, не предусмотренный в интерфейсе?

Ответить

 

Александр Макаров

Для контейнера PSR-11 вполне достаточен чтобы отвязать фреймворк от него. Это даст возможность конечному пользователю выбрать, какой контейнер использовать. И да, это не даст возможности заменить без боли контейнер вместе с его конфигом. Конфиг придётся переписывать.

Ответить

 

Sergei Maslovskiy

Спасибо, интересная статья. Да, жаль что Symfony ушли из PSR.

Ответить

 

Vladimir Kamuz

Крупные WordPress-ники не пользуются Git и ООП. - да всё там есть при желании

Ответить

 

Алексей Rox

Загонять вордпресс в гит — та ещё боль. Из-за плагинов, которые устанавливаются онлайн и при этом меняют структуру БД очень непросто синхронизировать прод с девом. Вряд ли там кто-то продумал нормальную систему миграций, а если и продумал, то вряд ли все производители плагинов ей следуют.

Ответить

 

xfg

Дмитрий, а в других языках существует, что-то подобное как PSR-3, PSR-6 и т.д. ? В Java например?

Я не очень понимаю, зачем может потребоваться прокидывать свой код внутрь чужой библиотеки. Я считаю, что библиотеку необходимо рассматривать как black box. Тем более, что обычно мы не хотим напрямую вызывать Locator.php в своем коде и пишем для него адаптер и в этом адаптере можно добавить кеширование/логирование независимо от того есть это в библиотеке или нет. Непонятно зачем для этого лезть внутрь библиотеки. Ни в одном авторитетном источнике не встречал подобных практик. Есть такие?

Ответить

 

Дмитрий Елисеев

В Java есть исторически сложившаяся экосистема Apache Commons.

> Пишем для него адаптер и в этом адаптере можно добавить кеширование/логирование независимо от того есть это в библиотеке или нет.

При подключении компонента Locator будет удобно, если к interface Locator будет в комплекте идти готовый прокси PsrCacheLocator и декоратор PsrLogLocator.

Как раз это я делаю сейчас в новой серии по IP Locator.

> Ни в одном авторитетном источнике не встречал подобных практик. Есть такие?

Например, к symfony/http-client в комплете есть прокси CachingHttpClient и адаптер Psr18Client.

Ответить

 

Spirit Absolute

Крупные Bitrix-оиды не пользуются нэймспейсами
Не правда! Работаю разработчиком в битриксе. Давно активно используем неймспейсы.
Из psr нам подходит максимум code style. У нас есть свои стандарты близкие к "псровскому". Psr нужен для опенс соурса.

Ответить

 

Иван

Крупные Bitrix-оиды не пользуются нэймспейсами - познания автора о рынке слабы, и видимо он не сталкивался особо с крупными =)
У мелких да, подходы страдают. Как в целом и любых мелких.

Ответить

 

Дмитрий Елисеев

Взяли свои особо крупные и на D7 переписали?

Прямо-таки объектную ORM в 2018-ом сделали и symfony/console через свой Composer для генерации к ней аннотаций подключили. Прогрессируют :)

Ответить

Оставить комментарий

Войти | Завести аккаунт | Войти через

(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>