Контрактное программирование в PHP

    Контрактное программирование В реальной жизни мы повсюду сталкиваемся с различными контрактами: при устройстве на работу, при выполнении работ, при подписании взаимных соглашений и многими другими. Юридическая сила контрактов гарантирует нам защиту интересов и не допускает их нарушения без последствий, что дает нам уверенность в том, что те пункты, которые описаны в контракте — будут выполнены. Эта уверенность помогает нам планировать время, планировать расходы, а также планировать необходимые ресурсы. А что если и программный код будет описываться контрактами? Интересно? Тогда добро пожаловать под кат!

    Введение


    Сама идея контрактного программирования возникла в 90-х годах у Бертрана Мейера при разработке объектно-ориентированного языка программирования Eiffel. Суть идеи Бертрана была в том, что нужно было иметь инструмент для описания формальной верификации и формальной спецификации кода. Такой инструмент давал бы конкретные ответы: «метод обязуется сделать свою работу, если вы выполните условия, необходимые для его вызова». И контракты как нельзя лучше подходили для данной роли, потому что позволяли описать что будет получено от системы (спецификация) в случае соблюдения предусловий (верификация). С тех пор появилось множество реализаций данной методики программирования как на уровне конкретного языка, так и в виде отдельных библиотек, позволяющих задавать контракты и проводить их верификацию с помощью внешнего кода. К сожалению, в PHP нет поддержки контрактного программирования на уровне самого языка, поэтому реализация может быть выполнена только с помощью сторонних библиотек.

    Контракты в коде


    Так как контрактное программирование было разработано для объектно-ориентированного языка, то не сложно догадаться, что основными рабочими элементами для контрактов являются классы, методы и свойства.

    Предусловия

    Самым простым вариантом контракта являются предусловия — требования, которые должны быть выполнены перед конкретным действием. В рамках ООП все действия описываются методами в классах, поэтому предусловия применяются к методами, а их проверка происходит в момент вызова метода, но до выполнения самого тела метода. Очевидное использование — проверка валидности переданных параметров в метод, их структуры и корректности. То есть с помощью предусловий мы описываем в контракте все то, с чем мы точно работать не будем. Это же здорово!

    Чтобы не быть голословным, давайте рассмотрим пример:

    class BankAccount
    {
        protected $balance = 0.0;
    
        /**
         * Deposits fixed amount of money to the account
         *
         * @param float $amount
         */
        public function deposit($amount)
        {
            if ($amount <= 0 || !is_numeric($amount)) {
                throw new \InvalidArgumentException("Invalid amount of money");
            }
            $this->balance += $amount;
        }
    }
    


    Мы видим, что метод пополнения баланса в неявном виде требует числового значения величины суммы пополнения, которая также должна быть строго больше нуля, в противном случае будет выброшено исключение. Это типичный вариант предусловия в коде. Однако он имеет несколько минусов: мы вынуждены искать глазами эти проверки и, находясь в другом классе, не можем быстро оценить наличие/отсутствие таких проверок. Также, без наличия явного контракта, нам придется помнить о том, что в коде класса есть необходимые проверки входящих аргументов и нам не надо волноваться за них. Еще один фактор: эти проверки выполняются всегда, как в режиме разработки, так и боевом режиме работы приложения, что незначительно влияет в отрицательную сторону на скорость работы приложения.

    В плане реализации предусловий, в PHP существует специальная конструкция для проверки утверждений — assert(). Большое ее преимущество в том, что проверки можно отключать в боевом режиме, заменяя весь код команды на единственный NOP. Давайте посмотрим на то, как можно описать предусловие с помощью данной конструкции:

    class BankAccount
    {
        protected $balance = 0.0;
    
        /**
         * Deposits fixed amount of money to the account
         *
         * @param float $amount
         */
        public function deposit($amount)
        {
            assert('$amount>0 && is_numeric($amount); /* Invalid amount of money /*');
    
            $this->balance += $amount;
        }
    }
    

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

    Постусловия

    Следующая категория контрактов — постусловия. Как можно догадаться из названия, данный тип проверки выполняется после того, как было выполнено тело метода, но до момента возврата управления в вызывающий код. Для нашего метода deposit из примера мы можем сформировать следующее постусловие: баланс счета после вызова метода должен равняться предыдущему значению баланса плюс величина пополнения. Осталось дело за малым — описать все это в виде утверждения в коде. Но вот здесь нас поджидает первое разочарование: как же сформировать это требование в коде, ведь мы сперва изменим баланс в теле самого метода, а потом попытаемся проверить утверждение, где нужно старое значение баланса. Здесь может помочь клонирование объекта перед выполнением кода и проверка пост-условий:

    class BankAccount
    {
        protected $balance = 0.0;
    
        /**
         * Deposits fixed amount of money to the account
         *
         * @param float $amount
         */
        public function deposit($amount)
        {
            $__old = clone $this;
            assert('$amount>0 && is_numeric($amount); /* Invalid amount of money /*');
    
            $this->balance += $amount;
            assert('$this->balance == $__old->balance+$amount; /* Contract violation /*');
        }
    }
    


    Еще одно разочарование поджидает нас при описании постусловий для методов, возвращающих значение:

    class BankAccount
    {
        protected $balance = 0.0;
    
        /**
         * Returns current balance
         */
        public function getBalance()
        {
            return $this->balance;
        }
    }
    

    Как здесь описать контрактное условие, что метод должен возвращать текущий баланс? Так как пост-условие выполняется после тела метода, то мы наткнемся на return раньше, чем сработает наша проверка. Поэтому придется изменить код метода, чтобы сохранить результат в переменную $__result и сравнить потом с $this->balance:

    class BankAccount
    {
        protected $balance = 0.0;
    
        /**
         * Returns current balance
         */
        public function getBalance()
        {
            $__result = $this->balance;
            assert('$__result == $this->balance; /* Contract violation /*');
            
            return $__result;
        }
    }
    


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

    Инварианты

    Нам осталось рассмотреть еще один важный тип контрактов: инварианты. Инварианты — это специальные условия, которые описывают целостное состояние объекта. Важной особенностью инвариантов является то, что они проверяются всегда после вызова любого публичного метода в классе и после вызова конструктора. Так как контракт определяет состояние объекта, а публичные методы — единственная возможность изменить состояние извне, то мы получаем полную спецификацию объекта. Для нашего примера хорошим инвариантом может быть условие: баланс счета никогда не должен быть меньше нуля. Однако, с инвариантами в PHP дело обстоит еще хуже чем с постусловиями: нет никакой возможности легко добавить проверку во все публичные методы класса, чтобы после вызова любого публичного метода можно было проверить необходимое условие в инварианте. Также нет возможности обращаться к предыдущему состоянию объекта $__old и возвращаемому результату $__result. Без инвариантов нет контрактов, поэтому долгое время не было никаких средств и методик для реализации данного функционала.

    Новые возможности


    Встречайте, PhpDeal — экспериментальный DbC-фреймворк для контрактного программирования в PHP.
    После того, как был разработан фреймворк Go! AOP для аспектно-ориентированного программирования в PHP, у меня в голове крутились мысли насчет автоматической валидации параметров, проверки условий и много-много другого. Триггером к созданию проекта для контрактного программирования послужило обсуждение на PHP.Internals . Удивительно, но с помощью АОП задача решалась всего в пару действий: нужно было описать аспект, который будет перехватывать выполнение методов, помеченных с помощью контрактных аннотаций, и выполнять нужные проверки до или после вызова метода.

    Давайте посмотрим на то, как можно использовать контракты с помощью этого фреймворка:

    use PhpDeal\Annotation as Contract;
    
    /**
     * Simple trade account class
     * @Contract\Invariant("$this->balance > 0")
     */
    class Account implements AccountContract
    {
    
        /**
         * Current balance
         *
         * @var float
         */
        protected $balance = 0.0;
    
        /**
         * Deposits fixed amount of money to the account
         *
         * @param float $amount
         *
         * @Contract\Verify("$amount>0 && is_numeric($amount)")
         * @Contract\Ensure("$this->balance == $__old->balance+$amount")
         */
        public function deposit($amount)
        {
            $this->balance += $amount;
        }
    
        /**
         * Returns current balance
         *
         * @Contract\Ensure("$__result == $this->balance")
         * @return float
         */
        public function getBalance()
        {
            return $this->balance;
        }
    }
    


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

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

    Постусловия задаются аннотацией, имеющей стандартное название Ensure в терминах контрактного программирования. Код имеет аналогичную область видимости, что и сам метод, помимо этого, доступны переменные $__old с состоянием объекта до выполнения метода и переменная $__result, содержащая в себе то значение, которое было возвращено из данного метода.

    Благодаря использованию АОП стало возможным реализовать даже инварианты — они элегантно описываются в виде аннотаций Invariant в док-блоке класса и ведут себя аналогично постусловиям, но для всех методов.

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

    use PhpDeal\Annotation as Contract;
    
    /**
     * Simple trade account contract
     */
    interface AccountContract
    {
        /**
         * Deposits fixed amount of money to the account
         *
         * @param float $amount
         *
         * @Contract\Verify("$amount>0 && is_numeric($amount)")
         * @Contract\Ensure("$this->balance == $__old->balance+$amount")
         */
        public function deposit($amount);
    
        /**
         * Returns current balance
         *
         * @Contract\Ensure("$__result == $this->balance")
         *
         * @return float
         */
        public function getBalance();
    }
    


    Дальше начинается самое интересное: при создании класса и определении нужного метода любая современная IDE переносит все аннотации из описания метода в интерфейсе в сам класс. А это позволяет движку PhpDeal их находить и обеспечивать автоматическую проверку контрактов в каждом конкретном классе, реализующем данный интерфейс. Для желающих пощупать все собственными руками — можно скачать проект с гитхаба, установить все зависимости с помощью композера, настроить локальный веб-сервер на эту папку и потом просто открыть в браузере код из папки demo

    Заключение


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

    Благодарю за внимание!

    Ссылки по теме:
    1. Wikipedia — контрактное программирование
    2. Фреймворк PhpDeal для контрактного программирования в PHP
    3. Фреймворк Go! AOP для аспектно-ориентированного программирования в PHP

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 63

      –3
      Видимо я что-то не понимаю, но зачем в примерах такая пост-проверка?
      Да и assert нельзя использовать в таком плане, одна строчка в конфиге, и логика приложения терпит крах от отсутствия проверок.
      Что полезно — так это пост-валидация обьекта при вызове методов. Что должно быть реализуемо с использованием магической функции __call и Reflection API для проверки «публичности» вызваного метода.
        +1
        Ассерты не должны использоваться как окончательные проверки, я об этом в статье как раз писал. Но они используются в момент разработки, т.е. если написать код с соблюдением контрактов, то утверждается, что в боевой среде эти проверки могут быть отключены — и приложение будет работать так, как задумывалось. Но, это не касается проверки параметров, передаваемых от клиента, об этом я тоже написал в статье. Контракты описывают взаимодействие исключительно в коде и обеспечивают корректность при разработке.
          +1
          Можете пояснить примеры с проверками?
          Непонятны конкретно следующие:
          * @Contract\Ensure("$this->balance == $__old->balance+$amount")
          * @Contract\Ensure("$__result == $this->balance")
          , потому как не могу понять их смысл. Если в случае с
           * @Contract\Verify("$amount>0 && is_numeric($amount)")
          понятно назначение этого контракта (проверить правильность параметра, соответствие бизнес-модели), то в приведенных выше я не понимаю, зачем они нужны. Иными словами, как может случиться, что
          $old = $value;
          $value += $add;
          $value == $old + $add; // зачем проверять это?


          Просмотрел ещё раз, но так и не увидел в тексте замечания о том, что:
          Ассерты не должны использоваться как окончательные проверки
          Пока не дочитаешь до конца — это не очевидно, и может сбить с толку.
            +1
            Для первого вашего вопроса: здесь я выбрал сознательно простой пример. Поэтому да, как настоящий контракт это не стоит рассматривать, я хотел донести лишь идею использования. В настоящих системах этот контракт будет выглядеть значительно сложнее.

            Что касательно второго:
            … Хочу обратить внимание на то, что предусловия в рамках контрактов служат для проверки логки работы программы и не отвечают за валидность параметров, переданных от клиента. Контракты отвечают только за взаимодействие внутри самой системы. Поэтому пользовательский ввод должен всегда фильтроваться с помощью фильтров, так как утверждения могут быть отключены.


            Согласен, что про ассерты я явно не написал, так как думал, что это известный факт. Так как и контракты и ассерты используются только на этапе разработки и девелоперских окружениях.
        +2
        Скажите, а есть ли какие-то преимущества при написании ассертов в «eval-стиле», когда код передается как строка?

        assert('$amount>0 && is_numeric($amount);');
        

        На мой взгляд, это как-то не эстетично и мы теряем возможность проверки синтаксиса. Лично я предпочитаю такую запись:

        assert($amount>0 && is_numeric($amount));
        

        Могу предположить, что Вы используете такую запись для того, чтобы видеть свой комментарий (Invalid amount of money).
        Начиная с версии 5.4.8 вы можете передавать описание ассерта вторым параметром — ua2.php.net/assert
          +4
          Преимущества написания ассертов в eval-стиле дает возможность PHP не интерпретировать код в том случае, когда ассерты отключены. Когда же код в ассерте написан явно — он все время парсится и выполняется движком, даже когда нет потребности в проверке контрактов.
            0
            В конкретном примере без кавычек легко обойтись, ну а если у вас в проверке есть вызов метода, который очень ресурсоемкий, например проверка что на вход подали сортированный массив выливается в проверку на идентичность этого массива и сортированного, что суть ресурсоемкая операция и в дев режиме вы еще можете это терпеть, а на лайве под серьезной нагрузкой этого лучше не делать.
            П.С. сам ярый противник eval-style, но ассерты это единственное место где по кодстайлу мы у себя его разрешили.
            0
            Буду сволочью, но насколько быстро это работает,
            и как насчет использования TypeHintов для проверки, например, а также других менее хитрых возможностей и путей (в том числе, если так нужна своя типизация и централизация проверок — ее и создать, ООП это позволяет)?

            А так выглядит симпатично и интересно. Если еще и сделано не на уровне Reflection и прочей радости, а расширениями к ПХП и тд, чтобы быстро работало — то вообще гуд.
              0
              Assertions should be used as a debugging feature only.

              Мануал как бы намекает что это для тех случаев, когда производительность не на самом первом месте.
                +1
                Так как PhpDeal основан на базе аспектов, то там все очень неплохо оптимизировано: кэш проксей, кэш аннотаций, поддержка опкод-кэшеров в боевом режиме, кэширование инстансов рефлексии под методы.
                Что касательно тайпхинта — использовать ее однозначно, как и в случае с интерфейсами. Контракты не заменяют тайпхинтинг, а позволяют дополнительно описывать условия верификации при вызове метода.

                Сделано это на уровне userland-а, однако работать это будет быстро, не разочарую вас. Если эта идея будет полезна, то есть возможность еще улучшить код, выпилив единственные eval-ы, которые сейчас используются для проверок в составе контрактов. Также есть замечательная мысль — добавить туда поддержку спецификаций на Gherkin-е. Тогда нативная документация к методу станет и контрактом к нему же:

                class BankAccount
                {
                    /**
                     * Deposits fixed amount of money to the account
                     *
                     * @param float $amount
                     *
                     * @Contract\Verify("Given some precondition and some other precondition")
                     * @Contract\Ensure("When some action by the actor and some other action, then some result is returned")
                     */
                    public function deposit($amount) {}
                }
                
                +6
                Обратите внимание вот на что: из года в год всё большая и большая ответственность в PHP перекладывается на комментарии — по сути, изобретаются средства для привнесения новой функциональности в язык минимальными усилиями.

                Такими темпами ситуация однажды станет напоминать ситуацию в макросами в C, где многие цокают: «Это что за макрос в 800 строк? Ай-яй-яй, макрос на макросе сидит и макросом погоняет!», а другие им отвечают: «А что делать? По-другому — никак!»

                Собственно, отсюда вопрос: не кажется ли вам, что перенос логики исполнения в комментарии — это перебор?
                  +2
                  Да, ожидал этого вопроса :) Он уже многократно задавался, задается и будет задаваться. Ну что же, мое мнение таково, что рано или поздно аннотации будут реализованы на уровне ядра PHP, все к этому идет. Посмотрите на все новые фреймоврки: Laravel, Symfony2, Doctrine2, FLOW3. Везде есть аннотации, они удобны, они дают необходимый инструмент для описания мета-информации. Главное — правильно кэшировать и не анализировать их в рантайме.

                  Так что мое мнение: метаинформация в комментариях — это путь, от которого мы вряд ли откажемся. Стоит ожидать дальнейшего развития на уровне ядра PHP.
                    0
                    Я бы поспорил с тем, что логические конструкции можно называть метаинформацией, но, в целом, ответ про неизбежность понятен %)
                    Правда не уверен, что что-то кроме кеширования и диспетчеризации возможно будет реализовать на уровне ядра, в частности из-за, упомянутого вами зоопарка подходов.
                      0
                      До тех пор, пока это не в ядре PHP о аннотациях можно забыть. Даже сейчас в больших проектах мы избегаем использование аннотаций в пользу XML или YML

                      Если посмтреть ядро тех же самых Doctrine и Symfony, то там используется XML.
                        0
                        Простите, а в чем разница для боевой среды? Что в случае использования yml или xml, что в случае использования аннотаций, так или иначе в Symfony информация об этом кэшируется, т.е. непосредственное чтение этой информации происходит всего лишь при первом запуске приложения, а дальше она себе спокойненько валяется в кэше в одном и том же виде. Это конечно если рассуждать о скорости работы, а остальные причины, на мой взгляд, чисто идеологические.
                          –1
                          Уже где-то об этом разговаривали.
                          По умолчанию ничего не кешируется.
                          habrahabr.ru/post/196604/#comment_6825282
                            0
                            Я конечно не в курсе на счет yml, т.к. проектов под рукой на которых посмотреть можно было бы нет, а искусственно делать пока нет времени, но вот то что аннотации кэшируются по умолчанию, это 100%. Вот Вам пример, в каком виде этот кэш лежит image.

                            Более того, если вы измените аннотации и продолжите использовать prod окружение без сброса кэша, то эти изменения тоже никак не отразятся на работе приложения, что еще раз подтверждает их закэшированность. Кстати, тот же принцип работает и на config.yml например, попробуйте сменить реквизиты доступа к БД в prod окружении — приложение продолжит работать по старому подключению до сброса кэша. А вот в dev окружении да, по умолчанию ничего не кэшируется — ни аннотации, ни конфиги. Но Вы не запускаете большие проекты в работу на dev окружении?
                              0
                              Это было давно и естественно я всё тестировал на prod окружении и даже не с 1 запуска.
                              Возможно уже починили, но я бы, всё-равно через профайлер прогнал.
                                0
                                Вам не совсем правильно сказали. Оно не кешируется в дев-режиме, но оно компилируется в идентичный PHP-код. Т.е. не важно, yaml, xml или аннотации — сначала всё равно идёт компиляция этих параметров в php-код.
                          0
                          Ну вот мне, например, выносить логику приложения в конфиги не нравится. Чтобы понять всю логику, приходится смотреть и в код какого-нибудь класса, и в конфиг, который в yaml или xml не всегда доходчиво реализован.
                            0
                            Обычно понимать всю логику сразу не нужно. Идёт движение по уровням абстракций…
                              0
                              Так аннотации — это и есть уровень абстракции. По сути, мы просто указываем, какой сервис вызвать и с какими параметрами. Т.е. делаем то же самое, что делали бы в коде метода. Конечно, есть отличия в том, как это делается в коде и в аннотациях, но по мне — это намного ближе к обычному подходу по сравнению с конфигами.
                          +1
                          Ну что же, мое мнение таково, что рано или поздно аннотации будут реализованы на уровне ядра PHP, все к этому идет.


                          Скорее поздно чем рано. Или исключительно в хип-хопе.

                          Год назад была уже в internals дискуссия. Ну и в октябре на PHP Framework Days я у Расмуса спрашивал про аннотации на уровне языка. Он сказал, что во-первых пока так и не нашли удобный всем синтаксис аннотаций, во-вторых никто не вызывался их реализовывать, в-третьих далеко не все вендоры библиотек, поддерживающих аннотации, готовы будут перейти на нативные аннотации PHP.
                          +3
                          На мой взгляд к этому надо просто осторожно подходить. В симфони, например, есть несколько аннотаций, которые достаточно удобны и я бы ни за что не хотел их каждый раз руками писать.
                          У меня в коде проекта есть такой контроллер:
                          /**
                           * @Route("/{id}/comments")
                           * @ParamConverter("cafe", class="AppUserBundle:User")
                           * @Template()
                           */
                          public function indexCommentsAction(User $user)
                          {
                              return ['user' => $user];
                          }
                          

                          Без использования аннотаций тут пришлось бы написать немного шаблонного кода:
                          public function indexCommentsAction($id)
                          {
                              $user = $this->getDoctrine()->getManager()->getRepository('AppUserBundle:User')->findOneById($id);
                              if ($user === null) {
                                  throw $this->createNotFoundException('There is no users with id = ' . $id);
                              }
                          
                              return $this->render('AppCommentBundle:Index:index.html.twig', ['user' => $user]);
                          }
                          

                          Ну и зачем это нужно, если удобнее это все завернуть в простую аннотацию. Особенно когда понимаешь, как она реально работает.
                            +1
                            Кстати, к слову сказать, Ваш код сработает и без @ParamConverter, по крайней мере в 2.4 версии точно, но по моему и раньше было. Т.е. когда вы используете один входящий параметр со строгой типизацией, этот тип является сущностью доктрины и в роуте присутствует явно названный id, то такое приведение выполняется по умолчанию. Смысл использовать @ParamConverter есть, когда у вас более одного параметра или же приведение используется не по идентификатору например, ну и т.д.
                              0
                              Ухты! Спасибо.
                              Жалко темлейты по умолчанию не резолвит, если возвращается массив
                                0
                                Ага, и роуты по умолчанию не генерит, на основе указанных параметров. Можно реализовать и направить pull request, авось и включат в новые версии.
                                  0
                                  Не, спасибо. Я в симфони уже отправил PR в swiftmailerbundle, который не принимают пол года. Ишшуе можно написать. У контрибьютеров и ментейнеров там своя обособленная туса
                                0
                                made my day :)
                                0
                                Можно в противовес привести аргумент плохой читаемости и повышенного уровня входа для понимания этого кода, но давайте примем, что читающий — компетентный symfony-разработчик, владеющий всем спектром возможностей фреймворка.

                                Первая проблема в примере — размазывается хранение путей роутинга, для поиска нужного контроллера требуется проверить настройку роутинга, найти в нем ответственный контроллер, поискать по коду контроллера конфигурацию пути. Это занимает больше времени и сил, чем пройтись по routing.yml (даже если конфигурация разделена).

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

                                Третяя проблема вытекает из предыдущих двух, но стоит особняком — это рефакторинг.

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

                                P.S. переход на аннотации и конфиги размывает код. Да взять хотя бы конфигурацию сервисов в симфони через конфиг. Это удобно, пока их пара. Когда файл разрастается на 500 строк, читать его становится невозможно. Проверка синтаксиса опосредованная, IDE поддерживает минимум, отладка в случае неявного бага занимает вечность.
                                  0
                                  попробуйте phpstorm + symfony plugin + php-annotation plugin
                                    0
                                    Я насчёт роутинга как раз склоняюсь к указанию ресурсов (контроллеров) в конфиге и описании роутов в самом классе. Слабые связи и разделение ответственности. Плюс более удобное и быстрое переопределение роутов.
                                  +1
                                  не кажется ли вам, что перенос логики исполнения в комментарии — это перебор?

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

                                    Вопрос-то был не об аспектах в принципе, а о конкретной реализации.

                                    К слову, ситуация, когда в динамические языки начинают вносить элементы статики в последнее время всё шире распространяется, и это тоже весьма интересная и спорная тема.

                                    Не кажется, потому что по сути это декларативное объявление для программиста.

                                    А при чём тут программист? Цель прогаммы [созданной программистом], я надеюсь это не вызывает сомнений, — быть исполненой машиной. С этой точки зрения, в каком стиле это объявление реализовано — вовсе не важно. И лишь в целях гарантирования надёжности (или стремления к ней) изобретались и изобретаются различные подходы. То, что, например, IDE, или компилятор, подсказывают/указывает программистам, исходя из кода (интерфейсов, типов данных и пр.), что можно, а что нельзя и упрощают этим жизнь, по мне, так это приятное следствие из стремления решить вышеозначенную задачу.

                                    Вы же машину программируете, а не себя или товарища по цеху. Или вы о чём-то другом?
                                      0
                                      Контрактное программирование — это скорее именно «программирование» себя или товарища по цеху, обладающее приятным следствием — возможностью автоматической проверки «формальных, точных и верифицируемых спецификаций программных интерфейсов» (с) вики. Даже размещение контракта в этом смысле в комментариях символично — это документирование интерфейса прежде всего, а не инструкции машины. Что благодаря какому-то коду (времени исполнения как в данной реализации, а может быть и времени статического анализа как в IDE при работе с phpdoc) можно обнаруживать несоответствие реализации контракту не в ручном режиме, а в автоматическом — приятное следствие наличия формальной, точной и верифицируемой спецификации.
                                        0
                                        Таким образом, развивая мысль, можно говорить о том, что интерфейсы в PHP — это тоже программирование себя/товарища? (Обратите внимание, что в разбираемом языке интерфейсы как раз в поддержду [рудиментарную] контрактов). А если шагнуть ещё дальше, то и сигнатуры функций станут тем же — они ведь тоже для формализации и верификации. И компиляторы, опять же, о том же.
                                        В любом случае, мне понятна ваша позиция ;)
                                          +1
                                          Именно. Никаких исполняемых инструкций интерфейсы не несут, содержат лишь информацию для проверок времени трансляции. Убрать implements из объявлений — рабочая программа ни на йоту не изменит своей функциональности. А добавить — рабочая может перестать работать (скажем, в интерфейс добавить метод, который нигде не реализован, но нигде и не вызывается). Просто механизм декларации программистом своих намерений и последующей проверки их выполнения. Если убрать проверки из транслятора (оставив синтаксис), то ничего не изменится в рабочей программе.

                                          Сигнатуры функции осуществляют две функции — первая, да, декларация, но вторая это уже непосредственные команды инициализации переменных значениями из стэка (в широком смысле слова) вызовов.
                                  +3
                                  Повторю то что уже написали выше, но это те ощущения, которые у меня возникли сразу после прочтения статьи.

                                  — Слишком сильно влияет на производительность в худшую сторону
                                  — Абсолютно никакой поддержки IDE (очень жирный минус, катастрофический. Т.е. нет ни подсветки ни рефакторинга
                                  — Выигрыш от соглашения размещать эти проверки сразу в начали метода, на мой взгляд — никакой (у вас большой метод, значит метод не правильный, декомпозируйте задачу, которую он решает и делегируйте решение частей задачи другим классам, в некоторых случаях — методам)

                                  Ну и ещё небольшая опасность. Естественно появится пласт программистов PHP, которые начнут делать все проверки с помощью комментариев и совсем забудут про строгую типизацию и все из неё вытекающее.
                                    0
                                    Слишком сильно влияет на производительность в худшую сторону
                                    Вы уже проверили? :)
                                    Абсолютно никакой поддержки IDE
                                    Здесь да, согласен. Без кофмортной подсветки синтаксиса крайне неудобно работать. Можно только помечтать о том, чтобы написать плагин к шторму.
                                    0
                                    Абсолютно никакой поддержки IDE

                                    Ну, PhpDoc тоже не сразу стали ИДЕ поддерживать, а сейчас попробуйте найти без не’.

                                    И это есть продвинутая статическая типизация по сути. Вернее тайп-хинтинг, некоторые ошибки которого можно поймать и статическим анализом кода (в т. ч. и в ИДЕ).
                                    +4
                                    Вспомнил, как лет 15 назад познакомился с языком Eiffel, всплакнул.
                                      +2
                                      Скажите, а чем эти проверки отличаются от аннотаций? И от валидации через аннотации в частности?
                                        0
                                        Уточните свой вопрос :) Данное решение не подразумевает наличие какого-либо фреймворка кроме моего, также не требуется наличие DI-контейнера, для которого нужна генерация специальных проксей.
                                        А так, да, эти проверки — и есть аннотации :)) И от валидации через аннотации ничем не отличается
                                          0
                                          Семантикой отличается, даже если код проверок идентичный :)
                                          +3
                                          Аннотации — способ декларации свойств сущности. Контракт в данном виде один из видов свойств.

                                          Контракт и валидация решают разные задачи даже если в некоторых реализациях они состоят из побайтно идентичного кода. Нарушение контракта означает ошибку логики, ошибку этапа разработки, непредвиденное разработчиком поведение. Всё что должно сделать приложение если заметило нарушение контракта — это умереть, фатал еррор, исполнение невозможно. Близкий аналог — это нарушение тайп-хинтинга. Собственно тайп-хинтинг является простейшим неотключаемым предусловием. Валидация же означает проверку входных данных приложения от пользователя, с файлов, баз и т. п., реакцию на неё разработчик предусматривает.
                                            0
                                            Допустим, есть у нас контракт:

                                            @Contract\Verify("$amount>0 && is_numeric($amount)")
                                            

                                            и где-то ранее, получив значение $amount от пользователя, дополнительно его валидируем. Выходит, что в этом случае 2 раза проверяется одно условие? Контракт лишний или валидация лишняя?
                                              +1
                                              Не лишнее ни то, ни другое. В типичном MVC приложении мы валидируем (постоянно, в том числе в продакшене) значение, введённое пользователем, на соответствие бизнес-правилам в контроллере (уровень абстракции — пользовательские сценарии), а контрактом проверяем (как правило только на этапе разработки и тестирования), что контроллер вызывает модель согласно её контракту (уровень абстракции — интеграция частей системы).

                                              Семантически контракт проверяет, что а) валидация работает и б) в системе нет вызовов модели с недопустимыми значениями минуя валидацию. Если получаем ошибку валидации, то это предусмотренное поведение, пускай и не основное. Если получаем ошибку контракта, то это непредусмотренное поведение, по сути аналогичное ошибке типизации типа NPE.
                                          +2
                                          * @Contract\Ensure("$this->balance == $__old->balance+$amount")

                                          Простите, ну как-то наверное не очень правильно писать this->balance в интерфейсе. Получается, что затачиваемся на конкретную реализацию, где есть такое поле. Например, какую-нибудь проксю за этим интерфейсом уже не напишешь. :) А в ином случае выделение сущности интерфейса сомнительно на мой взгляд.
                                            0
                                            Да, тут есть некоторое расхождение, но я не призываю к использованию :) просто поделился тем, что выглядит интересно с моей точки зрения. Как это можно использовать — еще не до конца понятно даже мне, но есть ощущение, что из этого может получиться со временем что-то интересное.
                                              0
                                              В данном примере, это больше похоже на мини unit test, проверяющий логику работы самого метода, хотя формально описывает некое постусловие контракта. Тогда зачем нам такой контракт?
                                                0
                                                Скорее надо писать там $this->getBalance() == $__old->getBalance() Или контракт делать в трєйте реализующем интерфейс, в общем обход синтаксиса.

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

                                                  Еще один важный момент: в контрактах-инвариантах нельзя использовать проверки на основе публичных методов, так как это приведет к рекурсии (инварианты проверяются после вызова публичного метода)
                                                0
                                                Интересная статья. Спасибо автору!
                                                  0
                                                  Пожалуйста:) Такие отзывы — лучший стимул для меня к созданию нового :))
                                                  0
                                                  Довольно интересное решение. Давно использую контракты в PHP (и if и assert), но до такого еще идея не дошла.
                                                  Один вопрос, не нашел ответа, где система вызывает ту или иную проверку до и после выполнения метода?
                                                    +1
                                                    Для этого нужно разобраться в том, как работает аспектный фреймворк Go! :) Если кратко — то он умеет вплетать советы в код конкретных классов до их загузки в память PHP (Load-Time Weaving). И метод становится композитом из оригинального метода и советов к нему. Причем сам код об этом практически ничего не знает.
                                                      0
                                                      А насколько это тяжеловесно?
                                                        +1
                                                        Тяжеловесно только при первом проходе, все остальные запуски очень быстрые. Чтобы оценить скорость — могу дать ссылку для кода ZF2, в котором перехватываются все методы (публичные, защищенные, статические) и пишется имя класса, метода и аргументы.
                                                        Быстро или медленно — решать вам :) хост — обычный бесплатный VPS
                                                          0
                                                          Не думали о внедрении кода прямо в файл или о компиляции PHP в некий байт-код, дабы тяжеловесности небыло вовсе?
                                                            +2
                                                            Именно так все и происходит, кэш генерится в виде отдельных файлов-классов, которые и заменяют оригинальные. Тяжеловестность есть из-за того, что приходится поработать над статическим анализом кода и его обработкой с помощью рефлексии. Но также есть консольная команда прогрева кэша, которая может выполняться в момент деплоя. Она значительно ускоряет старт приложения с аспектами
                                                              0
                                                              Вроде понятно. Спасибо.
                                                    0
                                                    Оставлю ссылочку для желающих: PhpStorm IDE Integration. При установленном плагине у вас будет работать подсветка синтаксиса PHP в аннотациях контрактов

                                                    Only users with full accounts can post comments. Log in, please.