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


Баг


Итак, когда после обеда я подошёл к своему коллеге Роману 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.


Почему же баг в коде нашего проекта раньше никак не давал о себе знать? Видимо, сущности редко создавались разнотипными группами, а если и создавались — то рефакторили их одновременно. А вот при прогоне тестов в один запуск скрипта попали два объекта, работающие через разные механизмы, и один из них испортил другому состояние.


Выводы


(ведь в каждой серьёзной статье должны быть выводы)


  1. Статические переменные — зло.
    Ну то есть как и любое другое зло в программировании, они требуют осторожного и вдумчивого подхода. Конечно, можно критиковать нас за использование скрытого состояния, но при аккуратном применении это позволяет писать достаточно эффективный код. Однако за static'ами могут скрываться подводные камни, один из которых я вам продемонстрировал. Поэтому
  2. Пишите юнит-тесты.
    Никто не поручится, что скрытый косяк в вашем коде не вылезет на свет после очередного рефакторинга. Так что пишите тестируемый код и покрывайте его тестами. Если бы подобный описанному мной баг возник в боевом коде, а не в тестах, на его отладку вполне мог бы уйти весь день, а не полтора-два часа.
  3. Не бойтесь влезть в дебри.
    Даже такая простая штука, как статические переменные, может послужить поводом для того, чтобы глубоко погрузиться в системную документацию и исходники PHP. И даже что-то в них понять.

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


P.S.: Благодарю Романа parpalak за ценные советы при подготовке материала.

Поделиться с друзьями
-->

Комментарии (49)


  1. Fesor
    23.05.2016 16:43
    +3

    Статические переменные — зло.

    Перефразирую. Статические переменные = глобальное состояние. А работа с глобальным состоянием приводит к побочным эффектам.


    Пишите юнит-тесты.

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


    А отсюда мы еще и должны вводить какое-то разделение ответственности и вещи становятся намного сложнее.


    p.s. В целом статья будет полезна многим)


    1. develop7
      23.05.2016 17:24
      +1

      s/глобальное/разделяемое/ (состояние)


      1. Fesor
        24.05.2016 01:23

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


    1. Avenger911
      23.05.2016 17:40

      Спасибо за комментарий! По правде говоря, основным побуждением к написанию этой статьи было желание поделиться необычным поведением static'ов в PHP. А с вашими замечаниями я могу только согласиться.


  1. 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);
    }


    1. Avenger911
      23.05.2016 17:31

      Вы правы, обычно if, конечно, читабельнее, чем тернарный оператор. Но я привёл одну строчку, а в полном коде в этом месте уже стоит выше проверка

      if ($isClientPropsOriginal === null) {

      }


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


  1. aol-nnov
    23.05.2016 17:41

    > 1. Статические переменные — зло.
    > 1. Пишите юнит-тесты.
    > 1. Не бойтесь влезть в дебри.

    а у вас -ус отклеился- для статьи юнит тесты не написаны! ;)


    1. Avenger911
      23.05.2016 17:46
      +1

      Будем считать ваш комментарий первым таким тестом =)

      А вообще, мне только с третьей попытки удалось заставить ХабраМаркдаун отобразить нумерованный список как надо.


  1. crmMaster
    23.05.2016 19:11
    -3

    Гуглить «позднее/раннее статическое связывание». В статье описано нормальное поведение статических полей при наследовании.


    1. Avenger911
      23.05.2016 20:10
      +4

      В статье речь идёт о статических переменных внутри метода класса, а не о статических методах/полях классов.


  1. AlexLeonov
    23.05.2016 19:28

    Спасибо за статью, однако мне показалось, что ничего нового. Ну кроме постоянного смешивания понятий «статическая локальная переменная» и «статическое свойство»

    То, что методы (обычные, не статические!) связаны с классом, где они написаны, а не с объектом и про вызове в них просто прокидывается $this — мне думалось, что общеизвестно…

    Впрочем, может это всё просто показалось при беглом чтении.


    1. Avenger911
      23.05.2016 20:17

      Я старался везде подчёркивать, что речь идёт именно о статической локальной переменной, а статические свойства классов вообще не использовал.

      То, что методы (обычные, не статические!) связаны с классом, где они написаны, а не с объектом и про вызове в них просто прокидывается $this — мне думалось, что общеизвестно…

      Вы правы, но на деле всё получается несколько сложнее. Контекст статических переменных (и некоторая другая информация) создаётся отдельно для каждого класса, даже если сам метод в нём не объявлен (см. мой пример 1).


  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
    


  1. oxidmod
    23.05.2016 20:43

    извините не по теме, но что за странная сущность, которая знает о своих клиентах?


    1. Avenger911
      23.05.2016 21:04

      «Клиент» здесь — это браузер пользователя, то есть сущность знает, какие её поля нужно отдавать по REST API (ну и какие записать в базу — тоже знает)


      1. oxidmod
        23.05.2016 21:05

        а если появится еще другой клиент для сущности?


        1. Avenger911
          23.05.2016 21:25

          … то этот вопрос надо будет обдумать :-)

          Вообще я так с ходу даже не могу придумать, кто третий может прийти в наше уютненькое приложение с фронтендом и бэкендом. Но да, в существующей архитектуре знания сущности умножатся. Это не кажется мне большой проблемой, учитывая что 90% полей передаются как есть из базы, не требуя дополнительного описания, а для оставшихся мы сейчас используем симпатичные аннотации. Механизм из примера с возвратом массива — это старый вариант, который мы ещё не везде переписали. Если же клиенты размножатся до проблемного количества, придётся задуматься над рефакторингом.

          А какую архитектуру вы предлагаете?


          1. oxidmod
            23.05.2016 21:30

            ну это трудно назвать архитектурой.
            сущность имеет геттеры которые возвращают поля как есть в бд.
            завести сериализаторы который умеют конвертировать сущность в нужны вид.
            их можно унаследовать от общего интерфейса, чтобы для остального кода было без разницы какой именно сериализатор использовать.
            Новый клиент — новый сериализатор, который гарантировано не порушит работу уже написанных. И + 2 строки где-то в фабричном методе, который возвращает нужный серилизатор.


            1. oxidmod
              23.05.2016 21:35
              +1

              уже по истечению время редактирования пришла еще одна мысль в голову. Клиент то может быть тот же. Но в зависимости от учетной записи может разниться список доступной информации. Такое знание в сущности уж точно не к месту будет.


            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, то нужно будет просто добавить новый сериализатор.


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


              1. oxidmod
                23.05.2016 21:59

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


                1. Avenger911
                  23.05.2016 22:14

                  сущности незачем знать о том, что клиенту удобно))

                  ну по правде говоря, да =)


                  Такая архитектура, конечно, более гибкая, но придётся делать по преобразователю для каждой сущности и клиента...


                1. 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() и сериализуем данные, полученные через геттеры сущности.


                1. samizdam
                  23.05.2016 23:12

                  В Zend Framework есть такая концепция как Hydrator.
                  Когда передо мной встала задача отделить представление структуры данных для клиента (то что будет сериализовано в json) от самой модели (в моём случае это были AR, ибо Yii), то я позаимствовал данное словечко и концепцию для организации слоя таких гидраторов. В итоге модели не знают о клиенте, и есть единая точка для манипуляций со структурами для json.


                  1. 4orever
                    24.05.2016 00:38
                    +1

                    Ну и в поддержку ZF можно посоветовать посмотреть в сторону Apigility для вдохновления, там все красиво с точки зрения архитектуры :)


      1. Fesor
        24.05.2016 01:24
        +1

        то есть сущность знает, какие её поля нужно отдавать по REST API

        То есть элемент бизнес логики знает о UI. Вам не кажется что это не гуд?


        1. parpalak
          24.05.2016 09:09

          А почему вы считаете, что REST API — это UI?


          У нас Ember, "MVC" на клиенте и всё такое. API на сервере больше к M относится, а не к V.


          1. oxidmod
            24.05.2016 09:26

            как говорил дедушка Эйнштейн: все в мире относительно.
            если смотреть с точки зрения бекенд — программиста, то json, который сервер выплюнет в ответ на запрос браузера — это V из MVC.
            для фронта же запрос на серверное АПИ тоже самое что на сервере запрос в бд))


            1. parpalak
              24.05.2016 10:51

              V не может быть одновременно и на клиенте, и на сервере.


              Вообще, MVC — это шаблон проектирования не любого приложения, а приложения с пользовательским интерфейсом. Об этом часто забывают. У API нет пользовательского интерфейса. Если всё равно внедрять MVC через силу, получится притянутое за уши V и каша в M. Лучше сразу честно признать, что слоев на бекенде REST API много, и они не MVC: https://habrahabr.ru/post/267125/


              1. oxidmod
                24.05.2016 11:38

                просто у вас не одно приложение, а два. На сервере и на клиенте.
                Для SPA V — это страничка, которая рендерится.
                Для php-приложения V — это json, xml, html либо любое другое представление данных, которое оно отдаст запросившему.


                1. parpalak
                  24.05.2016 13:23

                  Тем не менее, REST API на сервере — это не приложение с пользовательским интерфейсом. Даже название — API — намекает, что это программный интерфейс.


                  1. oxidmod
                    24.05.2016 15:41

                    как ни странно, но АПИ — это часть приложения. У приложения может быть несколько АПИ, которые позволяют другим приложениям (в том числе SPA) взаимодействовать с данным приложением


                  1. Fesor
                    24.05.2016 16:04

                    REST 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.


              1. Fesor
                24.05.2016 15:52
                -2

                V не может быть одновременно и на клиенте, и на сервере.

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


                Вообще, MVC — это шаблон проектирования не любого приложения, а приложения с пользовательским интерфейсом.

                мы сейчас про какой MVC? который православный 79-ого года? Если да — причем тут он. Если мы про другие виды separated presentation — то уточните тогда.


                1. parpalak
                  24.05.2016 19:24

                  Я про тот MVC, о котором написано в Википедии.


                  Можно сколько угодно заниматься казуистикой и утверждать, что json на выходе REST API — это "представление". Это не отменяет принципиальной разницы между организацией (десктопного) приложения, в котором пользователь нажимает на кнопки и HTTP API. В первом действительно можно сделать активную модель, события и настоящее управление представлением из модели. Во втором жизненный цикл "приложения" ограничен обработкой запроса и формированием ответа, по крайней мере на PHP.


                  Мало того, что MVC "притянут за уши". В контексте веб-программирования MVC — это вредный паттерн. Он приводит к тому, что у неопытных разработчиков роль модели играет слой работы с БД, роль представления — шаблоны, а бизнес-логика располагается в толстых контроллерах.


                  1. 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. Скажем описать различные варианты и не давать им названий. Как думаете, имеет смысл?


                    1. 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.

                      Напишите статью, если есть материал и желание. Вы хорошо объясняете.


                      1. Fesor
                        24.05.2016 23:29
                        -1

                        Напишите статью, если есть материал и желание. Вы хорошо объясняете.

                        У меня в черновиках какие-то начинания есть, но после пары месяцев обсуждений такого вопроса есть идея пойти с другого конца… написать статью именно по вопросу представления данных, разобрать несколько подходов но не пользоваться названиями… Ну то есть там полюбому будут описаны и MVC и MVP но названия будут специально опущены. Тогда все становится чуточку проще как мне кажется. Имеет смысл?


                        1. parpalak
                          25.05.2016 00:08

                          Да, было бы интересно прочитать.


  1. mnv
    23.05.2016 23:34

    На мой взгляд $isClientPropsOriginal хорошо смотрелась бы статическим свойством в AbstractEntity. В смысле расходования кофе эффективнее было бы.


    1. 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


      1. LastDragon
        24.05.2016 09:04

        1. Avenger911
          24.05.2016 11:30
          +2

          Позднее статическое связывание тут не поможет, придётся определять protected static $cache; в каждом дочернем классе (http://stackoverflow.com/a/4577202/2821101).


          Лучше уж такой вариант https://habrahabr.ru/post/301478/#comment_9622288.


          1. LastDragon
            24.05.2016 11:55

            Я и не говорил что поможет — код выше работает как и должен :)


          1. mnv
            24.05.2016 16:56

            Да, либо так, суть одна


  1. Zebratuk
    24.05.2016 16:29
    +1

    Так private невозможно унаследовать => контекст не изменяется. Вполне логично, как мне кажется.


    1. parpalak
      24.05.2016 19:28

      Нелогично то, что для наследования контекста достаточно заменить область видимости private на protected/public. Было бы естественнее менять контекст при переопределении метода.


      1. mnv
        24.05.2016 20:10

        Да, и если этот protected метод понадобится переопределить, то будут новые проблемы.


        1. Avenger911
          25.05.2016 00:31
          +3

          Ага, мы вернёмся от моего примера 3 к примеру 2.


          Я придумал, надо этот метод оставить protected, но пометить как final, чтобы никто потом не тянулся к нему грязным ручками.