Pull to refresh

Comments 63

Видимо я что-то не понимаю, но зачем в примерах такая пост-проверка?
Да и assert нельзя использовать в таком плане, одна строчка в конфиге, и логика приложения терпит крах от отсутствия проверок.
Что полезно — так это пост-валидация обьекта при вызове методов. Что должно быть реализуемо с использованием магической функции __call и Reflection API для проверки «публичности» вызваного метода.
Ассерты не должны использоваться как окончательные проверки, я об этом в статье как раз писал. Но они используются в момент разработки, т.е. если написать код с соблюдением контрактов, то утверждается, что в боевой среде эти проверки могут быть отключены — и приложение будет работать так, как задумывалось. Но, это не касается проверки параметров, передаваемых от клиента, об этом я тоже написал в статье. Контракты описывают взаимодействие исключительно в коде и обеспечивают корректность при разработке.
Можете пояснить примеры с проверками?
Непонятны конкретно следующие:
* @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; // зачем проверять это?


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

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


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

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

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

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

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

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

Мануал как бы намекает что это для тех случаев, когда производительность не на самом первом месте.
Так как 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) {}
}
Обратите внимание вот на что: из года в год всё большая и большая ответственность в PHP перекладывается на комментарии — по сути, изобретаются средства для привнесения новой функциональности в язык минимальными усилиями.

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

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

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

Если посмтреть ядро тех же самых Doctrine и Symfony, то там используется XML.
Простите, а в чем разница для боевой среды? Что в случае использования yml или xml, что в случае использования аннотаций, так или иначе в Symfony информация об этом кэшируется, т.е. непосредственное чтение этой информации происходит всего лишь при первом запуске приложения, а дальше она себе спокойненько валяется в кэше в одном и том же виде. Это конечно если рассуждать о скорости работы, а остальные причины, на мой взгляд, чисто идеологические.
Я конечно не в курсе на счет yml, т.к. проектов под рукой на которых посмотреть можно было бы нет, а искусственно делать пока нет времени, но вот то что аннотации кэшируются по умолчанию, это 100%. Вот Вам пример, в каком виде этот кэш лежит image.

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


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

Год назад была уже в internals дискуссия. Ну и в октябре на PHP Framework Days я у Расмуса спрашивал про аннотации на уровне языка. Он сказал, что во-первых пока так и не нашли удобный всем синтаксис аннотаций, во-вторых никто не вызывался их реализовывать, в-третьих далеко не все вендоры библиотек, поддерживающих аннотации, готовы будут перейти на нативные аннотации PHP.
На мой взгляд к этому надо просто осторожно подходить. В симфони, например, есть несколько аннотаций, которые достаточно удобны и я бы ни за что не хотел их каждый раз руками писать.
У меня в коде проекта есть такой контроллер:
/**
 * @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]);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Articles