Вообще я разработчик фронтенда. Но порой приходится работать и с серверной частью. Команда у нас небольшая, и когда все настоящиебэкенд-программисты заняты, бывает быстрее реализовать какой-то метод самому. А иногда мы садимся вместе поработать над задачами, чтобы не терять времени на перегон коммитов туда-сюда. Недавно во время одного из таких раундов парного программирования мы с товарищем по команде столкнулись с багом, который меня так впечатлил, что я решил с вами поделиться.
Баг
Итак, когда после обеда я подошёл к своему коллеге Роману parpalak, он как раз закончил приводить в порядок юнит-тесты, и запустил всю пачку. Один из тестов выкинул исключение и упал. Ага, подумали мы, сейчас исправим баг. Запустили тест в одиночестве, вне пакета, и он прошёл успешно.
Прежде чем сбросить с себя послеобеденную дремоту, мы запустили Codeception ещё несколько раз. В пакете тест падал, в одиночку проходил, в пакете падал…
Мы полезли в код.
Фаталка Call to private method
вылетала из метода, преобразующего объект сущности в массив для отправки клиенту. Недавно механизм этого процесса немного изменился, но ещё не все классы отрефакторили, поэтому в методе стоит проверка, переопределён ли метод, возвращающий список необходимых полей (это старый способ), в дочернем классе. Если нет, список полей формируется через рефлексию (это новый способ), и вызываются соответствующие геттеры. В нашем случае один из геттеров был объявлен как private, и, соответственно, недоступен из базового класса. Всё это выглядит примерно так:
abstract class AbstractEntity
{
/* Много кода */
public function toClientModel()
{
static $isClientPropsOriginal = null;
if ($isClientPropsOriginal === null) {
$reflector = new \ReflectionMethod($this, 'getClientProperties');
$isClientPropsOriginal = $reflector->getDeclaringClass()->getName() === 'AbstractEntity';
}
if ($isClientPropsOriginal) {
// TODO В будущем использовать только новую реализацию
return $this->toClientModelNew($urlGenerator);
}
$result = [];
foreach ($this->getClientProperties() as $clientKey => $property) {
$value = call_user_func([$this, 'get' . ucfirst($property)]);
$result[$clientKey] = $this->formatValueForClient($value);
}
return $result;
}
public function toClientModelNew()
{
$result = [];
/* Считать аннотации полей класса, получить маппинг полей сущности, сформировать массив данных */
return $result;
}
public function getClientProperties()
{
/* Вернуть массив свойств сущности */
}
/* Ещё код */
}
class Advertiser extends AbstractEntity
{
/* Много кода */
private $name;
private function getName()
{
return $this->getCalculatedName();
}
public function toClientModel()
{
$result = parent::toClientModel();
$result['name'] = $this->getName();
$result['role_id'] = $this->getRoleId();
return $result;
}
public function getClientProperties()
{
return array_merge(parent::getClientProperties(), [
'role_id' => 'RoleId' /* одно из полей для примера */
/* А name тут нет, он добавляется выше в toClientModel */
]);
}
/* Ещё код */
}
Как видите, результат работы рефлектора кешируется в статической переменной $isClientPropsOriginal
внутри метода.
— А что, рефлексия такая тяжёлая операция? — спросил я.
— Ну да, — кивнул Роман.
Брейкпоинт на строчке с рефлексией вообще не срабатывал в этом классе. Ни разу. Статической переменной уже было присвоено значение true
, интерпретатор лез в метод toClientModelNew
и падал. Я предложил посмотреть, где же тогда происходит присвоение:
$isClientPropsOriginal = $reflector->getDeclaringClass()->getName() === 'AbstractEntity' ? get_class($this) : false;
В переменной $isClientPropsOriginal
стояло "PaymentList"
. Это ещё один класс, унаследованный от AbstractEntity
, примечательный ровно двумя вещами: он не переопределяет метод getClientProperties
и он тестировался юнит-тестом, который уже успешно отработал чуть раньше.
— Как такое может быть? — спросил я. — Статическая переменная внутри метода шарится при наследовании? Почему тогда мы раньше этого не заметили?
Роман был озадачен не меньше моего. Пока я ходил за кофе, он набросал небольшой юнит-тест с имитацией нашей иерархии классов, но он не падал. Мы что-то упускали из виду. Статическая переменная вела себя неправильно, не так, как мы ожидали, но не во всех случаях, и мы не могли понять, почему. Гугление по запросу "php static variable inside class method" не давало ничего путного, кроме того, что статические переменные — это нехорошо. Well, duh!
Теперь Роман пошёл за кофе, а я в задумчивости открыл PHP-песочницу и написал самый простой код:
простой пример 1
class A {
function printCount() {
static $count = 0;
printf("%s: %d\n", get_class($this), ++$count);
}
}
class B extends A {
}
$a = new A();
$b = new B();
$a->printCount(); // A: 1
$a->printCount(); // A: 2
$b->printCount(); // B: 1
$b->printCount(); // B: 2
$b->printCount(); // B: 3
Как-то так это и должно работать. Принцип наименьшего удивления, все дела. Но у нас ведь статическая переменная определена внутри метода toClientModel
, а он переопределён в дочернем классе. А что, если мы запишем так:
простой пример 2
class A {
function printCount() {
static $count = 0;
printf("%s: %d\n", get_class($this), ++$count);
}
}
class B extends A {
function printCount() {
parent::printCount();
}
}
$a = new A();
$b = new B();
$a->printCount(); // A: 1
$a->printCount(); // A: 2
$b->printCount(); // B: 3
$b->printCount(); // B: 4
$b->printCount(); // B: 5
"Как странно," подумал я. Но какая-то логика тут есть. Во втором случае метод, содержащий статическую переменную, вызывается через parent::
, выходит, используется её экземпляр из родительского класса? А как же выйти из этого положения? Я почесал в затылке и немного дополнил свой пример:
простой пример 3
class A {
function printCount() {
$this->doPrintCount();
}
function doPrintCount() {
static $count = 0;
printf("%s: %d\n", get_class($this), ++$count);
}
}
class B extends A {
function printCount() {
parent::printCount();
}
}
$a = new A();
$b = new B();
$a->printCount(); // A: 1
$a->printCount(); // A: 2
$b->printCount(); // B: 1
$b->printCount(); // B: 2
$b->printCount(); // B: 3
Вот оно! Роман как раз вернулся, и я, довольный собой, продемонстрировал свои наработки. Ему понадобилось всего несколько нажатий на клавиатуру в PHPStorm, чтобы отрефакторить участок со статической переменной в отдельный метод:
private function hasOriginalClientProps()
{
static $isClientPropsOriginal = null;
if ($isClientPropsOriginal === null) {
$reflector = new \ReflectionMethod($this, 'getClientProperties');
$isClientPropsOriginal = $reflector->getDeclaringClass()->getName() === 'AbstractEntity';
}
return $isClientPropsOriginal;
}
Но не тут-то было! Наша ошибка сохранялась. Присмотревшись, я заметил, что метод hasOriginalClientProps
объявлен как private
, в моём примере был public
. Быстрая проверка показала, что работают protected
и public
, а private
не работает.
простой пример 4
<?php
class A {
function printCount() {
$this->doPrintCount();
}
private function doPrintCount() {
static $count = 0;
printf("%s: %d\n", get_class($this), ++$count);
}
}
class B extends A {
function printCount() {
parent::printCount();
}
}
$a = new A();
$b = new B();
$a->printCount(); // A: 1
$a->printCount(); // A: 2
$b->printCount(); // B: 3
$b->printCount(); // B: 4
$b->printCount(); // B: 5
В итоге мы объявили метод hasOriginalClientProps
как protected
и снабдили пространным комментарием.
Анализ
Время не ждало, и мы перешли к дальнейшим задачам, но всё же такое поведение озадачивало. Я решил разобраться, почему же PHP ведёт себя именно таким образом. В документации не удалось нарыть ничего, кроме неясных намёков. Ниже я попробую восстановить картину происходящего, основываясь на вдумчивом чтении PHP Internals Book, PHP Wiki, изучении исходников и информации о том, как реализуются объекты в других языках программирования.
Функция внутри интерпретатора PHP описывается структурой op_array
, которая, среди прочего, содержит хеш-таблицу со статическими переменными этой функции. При наследовании, если статических переменных нет, функция переиспользуется в дочернем классе, а если есть — создаётся дубликат, чтобы у дочернего класса в методе были свои статические переменные.
Пока всё хорошо, но если мы вызываем родительский метод через parent::printCount()
, то, естественно, попадаем в метод родительского класса, который работает со своими статическими переменными. Поэтому пример 2 не работает, а пример 1 — работает. А когда мы вынесли статическую переменную в отдельный метод, как в примере 3, нас выручает позднее связывание: метод A::printCount
всё равно вызовет копию метода A::doPrintCount
из класса B
(которая, конечно, идентична оригиналу A::doPrintCount
).
Лично мне такое копирование показалось довольно тяжеловесным. Видимо, разработчики PHP подумали так же и отказались от копирования для приватных методов. Ведь они же всё равно не видны из дочерних и родительских классов! Вон, мы даже фаталку в самом начале рассказа словили из-за этого. Поэтому приватный метод существует в единственном экземпляре по всей иерархии классов, и статические переменные в нём тоже существует в единственном контексте. Поэтому и не заработал пример 4.
Такое поведение повторяется на всех версиях PHP, которые я попробовал в песочнице, начиная с мохнатой 5.0.4.
Почему же баг в коде нашего проекта раньше никак не давал о себе знать? Видимо, сущности редко создавались разнотипными группами, а если и создавались — то рефакторили их одновременно. А вот при прогоне тестов в один запуск скрипта попали два объекта, работающие через разные механизмы, и один из них испортил другому состояние.
Выводы
(ведь в каждой серьёзной статье должны быть выводы)
- Статические переменные — зло.
Ну то есть как и любое другое зло в программировании, они требуют осторожного и вдумчивого подхода. Конечно, можно критиковать нас за использование скрытого состояния, но при аккуратном применении это позволяет писать достаточно эффективный код. Однако за static'ами могут скрываться подводные камни, один из которых я вам продемонстрировал. Поэтому - Пишите юнит-тесты.
Никто не поручится, что скрытый косяк в вашем коде не вылезет на свет после очередного рефакторинга. Так что пишите тестируемый код и покрывайте его тестами. Если бы подобный описанному мной баг возник в боевом коде, а не в тестах, на его отладку вполне мог бы уйти весь день, а не полтора-два часа. - Не бойтесь влезть в дебри.
Даже такая простая штука, как статические переменные, может послужить поводом для того, чтобы глубоко погрузиться в системную документацию и исходники PHP. И даже что-то в них понять.
На этой воодушевляющей ноте я прощаюсь с вами. Надеюсь, что эта статья поможет кому-то избежать тех граблей, на которые наступила мы. Спасибо за внимание!
P.S.: Благодарю Романа parpalak за ценные советы при подготовке материала.
Комментарии (49)
1kachan
23.05.2016 17:26$isClientPropsOriginal = $reflector->getDeclaringClass()->getName() === 'AbstractEntity'? get_class($this): false;
А не лучше ли сделать вот так? Так оно читабельней.
$isClientPropsOriginal = false;
if($reflector->getDeclaringClass()->getName() === 'AbstractEntity') {
$isClientPropsOriginal = get_class($this);
}Avenger911
23.05.2016 17:31Вы правы, обычно
if
, конечно, читабельнее, чем тернарный оператор. Но я привёл одну строчку, а в полном коде в этом месте уже стоит выше проверка
if ($isClientPropsOriginal === null) {
…
}
В тот момент, когда я писал текст, присвоение внутри одного оператора казалось мне более понятным.
aol-nnov
23.05.2016 17:41> 1. Статические переменные — зло.
> 1. Пишите юнит-тесты.
> 1. Не бойтесь влезть в дебри.
а у вас -ус отклеился- для статьи юнит тесты не написаны! ;)Avenger911
23.05.2016 17:46+1Будем считать ваш комментарий первым таким тестом =)
А вообще, мне только с третьей попытки удалось заставить ХабраМаркдаун отобразить нумерованный список как надо.
crmMaster
23.05.2016 19:11-3Гуглить «позднее/раннее статическое связывание». В статье описано нормальное поведение статических полей при наследовании.
Avenger911
23.05.2016 20:10+4В статье речь идёт о статических переменных внутри метода класса, а не о статических методах/полях классов.
AlexLeonov
23.05.2016 19:28Спасибо за статью, однако мне показалось, что ничего нового. Ну кроме постоянного смешивания понятий «статическая локальная переменная» и «статическое свойство»
То, что методы (обычные, не статические!) связаны с классом, где они написаны, а не с объектом и про вызове в них просто прокидывается $this — мне думалось, что общеизвестно…
Впрочем, может это всё просто показалось при беглом чтении.Avenger911
23.05.2016 20:17Я старался везде подчёркивать, что речь идёт именно о статической локальной переменной, а статические свойства классов вообще не использовал.
То, что методы (обычные, не статические!) связаны с классом, где они написаны, а не с объектом и про вызове в них просто прокидывается $this — мне думалось, что общеизвестно…
Вы правы, но на деле всё получается несколько сложнее. Контекст статических переменных (и некоторая другая информация) создаётся отдельно для каждого класса, даже если сам метод в нём не объявлен (см. мой пример 1).
michael_vostrikov
23.05.2016 19:34+1Я бы сделал нормальную статическую переменную (свойство) в отдельном классе ReflectionCache. Так проще и понятнее, и работает как надо. Стараюсь избегать использования статических переменных внутри функций, как раз из-за таких возможных багов.
Скрытый текстclass ReflectionCache { static $counts; public static function incrementCount($key) { if (!isset(self::$counts[$key])) self::$counts[$key] = 0; ++self::$counts[$key]; return self::$counts[$key]; } } class A { function printCount() { printf("%s: %d\n", get_class($this), ReflectionCache::incrementCount(static::class)); } } class B extends A { function printCount() { parent::printCount(); } } $a = new A(); $b = new B(); $a->printCount(); // A: 1 $a->printCount(); // A: 2 $b->printCount(); // B: 1 $b->printCount(); // B: 2 $b->printCount(); // B: 3
oxidmod
23.05.2016 20:43извините не по теме, но что за странная сущность, которая знает о своих клиентах?
Avenger911
23.05.2016 21:04«Клиент» здесь — это браузер пользователя, то есть сущность знает, какие её поля нужно отдавать по REST API (ну и какие записать в базу — тоже знает)
oxidmod
23.05.2016 21:05а если появится еще другой клиент для сущности?
Avenger911
23.05.2016 21:25… то этот вопрос надо будет обдумать :-)
Вообще я так с ходу даже не могу придумать, кто третий может прийти в наше уютненькое приложение с фронтендом и бэкендом. Но да, в существующей архитектуре знания сущности умножатся. Это не кажется мне большой проблемой, учитывая что 90% полей передаются как есть из базы, не требуя дополнительного описания, а для оставшихся мы сейчас используем симпатичные аннотации. Механизм из примера с возвратом массива — это старый вариант, который мы ещё не везде переписали. Если же клиенты размножатся до проблемного количества, придётся задуматься над рефакторингом.
А какую архитектуру вы предлагаете?oxidmod
23.05.2016 21:30ну это трудно назвать архитектурой.
сущность имеет геттеры которые возвращают поля как есть в бд.
завести сериализаторы который умеют конвертировать сущность в нужны вид.
их можно унаследовать от общего интерфейса, чтобы для остального кода было без разницы какой именно сериализатор использовать.
Новый клиент — новый сериализатор, который гарантировано не порушит работу уже написанных. И + 2 строки где-то в фабричном методе, который возвращает нужный серилизатор.oxidmod
23.05.2016 21:35+1уже по истечению время редактирования пришла еще одна мысль в голову. Клиент то может быть тот же. Но в зависимости от учетной записи может разниться список доступной информации. Такое знание в сущности уж точно не к месту будет.
Avenger911
23.05.2016 21:55Да, я вас понял. Но дело в том, что у нас иногда требуется какое-то поле передать на клиент под другим именем, или вообще не нужно передавать. Простой пример: в базе в сущности
User
хранится полеmanager_id
, а клиенту удобнее передавать полеmanager
, чтобы там оперировать полями связанной сущностиManager
какuser.manager.name
, а неuser.manager_id.name
. Сейчас мы просто пишем в аннотации к полюmanager_id
/* * @Annotation\MappingClient(alias="manager") */
Если для другого клиента понадобится другой набор полей или другие их имена, то всю эту информацию всё равно придётся где-то хранить.
Если же говорить о формате данных, то сериализацией и сейчас, конечно, ведает отдельный класс. Если вдруг понадобится отдавать данные не в JSON, а в XML, то нужно будет просто добавить новый сериализатор.
Что касается прав доступа, то их проверка находится в ведении контроллера, отдающего сущность клиенту. Он может, например, обнулить секретные поля.
oxidmod
23.05.2016 21:59ну вот сущности незачем знать о том, что клиенту удобно))
возможно сериализатор не совсем то слово. просто некий сервис который умеет представить сущность в том виде, в котором она нужна клиенту.Avenger911
23.05.2016 22:14сущности незачем знать о том, что клиенту удобно))
ну по правде говоря, да =)
Такая архитектура, конечно, более гибкая, но придётся делать по преобразователю для каждой сущности и клиента...
parpalak
23.05.2016 22:46+1Если подумать, то один сериализатор может быть тривиальным. У нас это клиентский сериализатор :)
Для понимания контекста замечу, что речь идет о REST API для одностраничного приложения на JS. Большинство операций — CRUD. Часть данных сохраняется в БД, часть уходит в API других сервисов, например, в биллинг. Кроме валидации особой бизнес-логики у нас нет.
Бывший коллега придумал интерфейс сущности с двумя методами сериализации:
EntityInterface::toStoreParams()
иEntityInterface::toClientModel()
. Я не вижу проблемы в существовании самих этих методов. У нас такое приложение, что каждая сущность передается на клиент и почти каждая сохраняется в БД.
В этом подходе сериализация в JSON для REST API занимает выделенное положение по сравнению с сериализацией для других целей. Она происходит без дополнительных сервисов. Типичный контроллер содержит код вроде
$userCollection = $userDataMapper->findBy(...); return new JsonResponse([ 'users' => $userCollection->toClientModels() ])
Движение данных в обратную сторону уже идет через дата-маппер:
$user = User::createFromClientModel($request->get('user')); $userDataMapper->insert($user);
В типовом дата-маппере вызывается
toStoreParams()
. Но если требуется более сложная логика по сохранению сущности (запись в две таблицы, запись в очередь и т. д.), мы переопределяем методUserDataMapper::insert()
и сериализуем данные, полученные через геттеры сущности.
samizdam
23.05.2016 23:12В Zend Framework есть такая концепция как Hydrator.
Когда передо мной встала задача отделить представление структуры данных для клиента (то что будет сериализовано в json) от самой модели (в моём случае это были AR, ибо Yii), то я позаимствовал данное словечко и концепцию для организации слоя таких гидраторов. В итоге модели не знают о клиенте, и есть единая точка для манипуляций со структурами для json.4orever
24.05.2016 00:38+1Ну и в поддержку ZF можно посоветовать посмотреть в сторону Apigility для вдохновления, там все красиво с точки зрения архитектуры :)
Fesor
24.05.2016 01:24+1то есть сущность знает, какие её поля нужно отдавать по REST API
То есть элемент бизнес логики знает о UI. Вам не кажется что это не гуд?
parpalak
24.05.2016 09:09А почему вы считаете, что REST API — это UI?
У нас Ember, "MVC" на клиенте и всё такое. API на сервере больше к M относится, а не к V.
oxidmod
24.05.2016 09:26как говорил дедушка Эйнштейн: все в мире относительно.
если смотреть с точки зрения бекенд — программиста, то json, который сервер выплюнет в ответ на запрос браузера — это V из MVC.
для фронта же запрос на серверное АПИ тоже самое что на сервере запрос в бд))parpalak
24.05.2016 10:51V не может быть одновременно и на клиенте, и на сервере.
Вообще, MVC — это шаблон проектирования не любого приложения, а приложения с пользовательским интерфейсом. Об этом часто забывают. У API нет пользовательского интерфейса. Если всё равно внедрять MVC через силу, получится притянутое за уши V и каша в M. Лучше сразу честно признать, что слоев на бекенде REST API много, и они не MVC: https://habrahabr.ru/post/267125/
oxidmod
24.05.2016 11:38просто у вас не одно приложение, а два. На сервере и на клиенте.
Для SPA V — это страничка, которая рендерится.
Для php-приложения V — это json, xml, html либо любое другое представление данных, которое оно отдаст запросившему.parpalak
24.05.2016 13:23Тем не менее, REST API на сервере — это не приложение с пользовательским интерфейсом. Даже название — API — намекает, что это программный интерфейс.
oxidmod
24.05.2016 15:41как ни странно, но АПИ — это часть приложения. У приложения может быть несколько АПИ, которые позволяют другим приложениям (в том числе SPA) взаимодействовать с данным приложением
Fesor
24.05.2016 16:04REST API на сервере — это не приложение с пользовательским интерфейсом
API — Application programming interface. То есть делаем вывод — REST API — это как раз таки интерфейс взаимодействия других програмных средств с приложением. А приложение — оно внутри. Если нет явного разделения — мы говорим о smartui.
Только вот "представление" в контексте бэкэнда — это HTTP запросы/ответы. Приложение получает запрос, конвертирует из представления HTTP во воунтреннее представление, с которым уже может работать модель (приложение), на что оно снова выплевывает нам внутренне представление и мы формируем HTTP представление… слишком много слова "представления" но думаю смысл должен быть понятент.
То есть в рамках бэкэнда у нас есть:
- пассивное представление (HTTP)
- активный контроллер/медиатор/адаптер (фронт контроллер, мидлвэры, обычные контроллеры и их экшены) который конвертирует асинхронные запросы в синхронные вызовы методов модели и в целом формирует пассивное представление.
- модель, само приложение, или скорее та часть ее которая граничит с интерфейсом и предоставляет програмное API для работы с ним. А HTTP API — это всего-лишь адаптер к этому интерфейсу.
Если так смотреть то мы можем говорить только о подходе разделения представления под названием mediating-controller MVC или Model-View-Adapter.
В классическом MVC вьюшки у нас активны, а вот контроллеры отвечают только за маршрутизацию событий в вызовы конкретных методов модели.
В MVP/MVVM у нас вьюшки пассивные а ViewModel/Presenter полностью берут контроль за тем как формируется пассивное view и как пользователь взаимодействует с ним.
И все это — просто частные случаи separated presentation.
Fesor
24.05.2016 15:52-2V не может быть одновременно и на клиенте, и на сервере.
Вы путаете представление и ментальную модель пользователя-человека. Представьте что пользователем приложения может быть другое приложение. И что из таких приложений мы можем выстраивать цепочки.
Вообще, MVC — это шаблон проектирования не любого приложения, а приложения с пользовательским интерфейсом.
мы сейчас про какой MVC? который православный 79-ого года? Если да — причем тут он. Если мы про другие виды separated presentation — то уточните тогда.
parpalak
24.05.2016 19:24Я про тот MVC, о котором написано в Википедии.
Можно сколько угодно заниматься казуистикой и утверждать, что json на выходе REST API — это "представление". Это не отменяет принципиальной разницы между организацией (десктопного) приложения, в котором пользователь нажимает на кнопки и HTTP API. В первом действительно можно сделать активную модель, события и настоящее управление представлением из модели. Во втором жизненный цикл "приложения" ограничен обработкой запроса и формированием ответа, по крайней мере на PHP.
Мало того, что MVC "притянут за уши". В контексте веб-программирования MVC — это вредный паттерн. Он приводит к тому, что у неопытных разработчиков роль модели играет слой работы с БД, роль представления — шаблоны, а бизнес-логика располагается в толстых контроллерах.
Fesor
24.05.2016 19:41-2Можно сколько угодно заниматься казуистикой и утверждать, что json на выходе REST API — это "представление".
Правильно, потому что представлением тут является HTTP) У вас просто еще не возникало задач с версионизацией API, с чего весь разговор и начался. И если смотреть с этой позиции у вас будет две реализации UI (разных версий) для одного приложения. Никакого булшита про MVC и т.д. просто разделение обязанностей.
В целом если у вас настолько просто проэцируются данные сущностей на JSON то почему бы просто не взять монгу и дать http интерфейс в ней клиенту? Тогда бэкэндщики в принципе и не нужны.
Это не отменяет принципиальной разницы между организацией (десктопного) приложения, в котором пользователь нажимает на кнопки и HTTP API.
Вы считаете что пользователь это почему-то человек который жамкает кнопки. Отсюда и весь конфуз.
Во втором жизненный цикл "приложения" ограничен обработкой запроса и формированием ответа, по крайней мере на PHP.
Вот последнее — правильно, потому что PHP обычно умирает. А теперь представьте что у вас нет базы данных, и все данные лежат просто в памяти, и PHP не умирает а так же висит как демон. Просто представьте.
А теперь подумайте, так ли это сильно отличается? У нас данные между запросами лежат в базе, или кэше или любом другом сторадже. Но это деталь реализации слоя хранения данных. Они могут точно так же в памяти лежать — на функциональность это никак не сказывается. Это сказывается только на надежности и стоимости железяк (все данные в память могут не поместиться).
А теперь заменим rest интерфейс на… ну не знаю… граффический интерфейс. Реализация нашей бизнес логики не меняется, меняется только UI layer, с HTTP на обычный GUI.
В контексте веб-программирования MVC — это вредный паттерн.
Это не вредный паттерн. Просто у большинства это просто модное слово, и добрая половина гордо говорит что у них все по MVC хотя по факту у них бизнес логика не то что в контроллеры вытекает, но и иногда в шаблоны те же.
Это все от того что люди не понимают изначальную проблему, которую MVC и другие реализации пытались решить. separated presentation. То есть как организовать взаимодействие различных компонентов системы (приложение как отдельный компонент и GUI как другой) и конвертировать представление из формата модели приложения в то, с которым удобнее работать пользователю.
В целом тут можно много писать, и в принципе можно было бы написать статью. Не про MVC а именно про separated presentation. Скажем описать различные варианты и не давать им названий. Как думаете, имеет смысл?
parpalak
24.05.2016 22:10Вы бы сразу написали про версионирование API :) В подходе, который я описал выше, конечно, не получится сделать версионирование без существенной доработки.
Если говорить про наш случай, то у нас не всё настолько просто, чтобы пользоваться оберткой над монгой, что бы это ни было. Иногда бывает, что поля одной сущности для API хранятся в нескольких таблицах. Иногда за сущностями обращаемся к другим приложениям по их API. Но всё и не настолько сложно, чтобы заботиться о версионировании API или сериализаторах, о которых речь шла выше.
Я понимаю ваши аргументы про MVC, но они меня не убеждают. Я всё равно вижу принципиальную разницу между приложениями, в которых есть пользовательский интерфейс, и в котором его нет. Можно взять для примера графический редактор и консольную программу конвертации графики типа ImageMagick. У них может быть много общего (библиотечного) кода. В первом случае MVC говорит, как организовать код, отвечающий за взаимодействие с пользователем. Во втором случае MVC ничего полезного не говорит. Можно, конечно, формально соотнести фрагменты кода с представлением или с моделью. Но зачем?
Даже Фабиен вот пишет:
I don't like MVC because that's not how the web works. Symfony2 is an HTTP framework; it is a Request/Response framework. That's the big deal. The fundamental principles of Symfony2 are centered around the HTTP specification.
I don't like MVC because the web has evolved a lot in the recent years and some projects are much different than projects we had some years ago. Sometimes, you just need a way to create a REST API. Sometimes, the logic is mostly in the browser and the server is just used to serve data (think backbone.js for instance). And for these projects, you don't need an MVC framework. You need something that handles a Request and returns a Response. You need a framework that implements the HTTP specification. HTTP streaming is yet another example that does not fit well with the MVC pattern.
Напишите статью, если есть материал и желание. Вы хорошо объясняете.
Fesor
24.05.2016 23:29-1Напишите статью, если есть материал и желание. Вы хорошо объясняете.
У меня в черновиках какие-то начинания есть, но после пары месяцев обсуждений такого вопроса есть идея пойти с другого конца… написать статью именно по вопросу представления данных, разобрать несколько подходов но не пользоваться названиями… Ну то есть там полюбому будут описаны и MVC и MVP но названия будут специально опущены. Тогда все становится чуточку проще как мне кажется. Имеет смысл?
mnv
23.05.2016 23:34На мой взгляд
$isClientPropsOriginal
хорошо смотрелась бы статическим свойством вAbstractEntity
. В смысле расходования кофе эффективнее было бы.parpalak
24.05.2016 00:27Со статическим свойством в абстрактном классе тоже на самом деле были проблемы. Если я правильно помню, код там примерно такой, и он работает не так как хотелось бы для кеширования в контексте класса:
<?php class A { protected static $cache; public function printClass () { if (static::$cache === null) { static::$cache = get_class($this); } echo static::$cache, ' ', get_class($this), "\n"; } } class B extends A { } class C extends A { } class D extends C { } class E extends A { public function printClass() { parent::printClass(); } } $b = new B(); $b->printClass(); // B B $c = new C(); $c->printClass(); // B C $d = new D(); $d->printClass(); // B D $e = new E(); $e->printClass(); // B E
LastDragon
24.05.2016 09:04Avenger911
24.05.2016 11:30+2Позднее статическое связывание тут не поможет, придётся определять
protected static $cache;
в каждом дочернем классе (http://stackoverflow.com/a/4577202/2821101).
Лучше уж такой вариант https://habrahabr.ru/post/301478/#comment_9622288.
Zebratuk
24.05.2016 16:29+1Так private невозможно унаследовать => контекст не изменяется. Вполне логично, как мне кажется.
parpalak
24.05.2016 19:28Нелогично то, что для наследования контекста достаточно заменить область видимости private на protected/public. Было бы естественнее менять контекст при переопределении метода.
mnv
24.05.2016 20:10Да, и если этот protected метод понадобится переопределить, то будут новые проблемы.
Avenger911
25.05.2016 00:31+3Ага, мы вернёмся от моего примера 3 к примеру 2.
Я придумал, надо этот метод оставить
protected
, но пометить какfinal
, чтобы никто потом не тянулся к нему грязным ручками.
Fesor
Перефразирую. Статические переменные = глобальное состояние. А работа с глобальным состоянием приводит к побочным эффектам.
Вы мельком упомянули тестируемый код… и тут стоит упомянуть что проще этого доиться когда сначала пишутся тесты. Ну и отсюда всплывает еще куча радостных вещей что далеко не все стоит тестировать юнит тестами. Например query builder-ы мокать никто в здравом уме не будет и большая часть инфраструктурных вещей должны покрываться уже интеграционными тестами (потому что так проще выходит).
А отсюда мы еще и должны вводить какое-то разделение ответственности и вещи становятся намного сложнее.
p.s. В целом статья будет полезна многим)
develop7
s/глобальное/разделяемое/ (состояние)
Fesor
Ну в контексте статических переменных — соглашусь. Доступ к ним ограничен, а стало быть сайд эффекты можно ограничить. Но вот автор схлопотал например.
Avenger911
Спасибо за комментарий! По правде говоря, основным побуждением к написанию этой статьи было желание поделиться необычным поведением static'ов в PHP. А с вашими замечаниями я могу только согласиться.