Я из любопытства искал то же самое и нашел этот вопрос, поэтому постараюсь дать ответ.
Во-первых, PHP, по замыслу, не совсем кодовый контракт. Вы даже не можете принудительно применять, когда это необходимо, основные типы параметров внутри методов, поэтому я вряд ли верю, что контракты кода будут существовать в PHP однажды.
Давайте посмотрим, что произойдет, если мы сделаем пользовательскую реализацию библиотеки / фреймворка стороннего производителя.
1. Предпосылки
Свобода передачи всего, что мы хотим, методу делает контракты кода (или что-то более или менее похожее на контракты кода) очень ценными, по крайней мере, на предварительных условиях, поскольку защитить методы от неправильных значений в аргументах сложнее, сравнивая к обычным языкам программирования, где типы могут быть реализованы через сам язык.
Было бы удобнее написать:
public function AddProduct($productId, $name, $price, $isCurrentlyInStock)
{
Contracts::Require(__FILE__, __LINE__, is_int($productId), 'The product ID must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_string($name), 'The product name must be a string.');
Contracts::Require(__FILE__, __LINE__, is_int($price), 'The price must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_bool($isCurrentlyInStock), 'The product availability must be an boolean.');
Contracts::Require(__FILE__, __LINE__, $productId > 0 && $productId <= 5873, 'The product ID is out of range.');
Contracts::Require(__FILE__, __LINE__, $price > 0, 'The product price cannot be negative.');
// Business code goes here.
}
вместо:
public function AddProduct($productId, $name, $price, $isCurrentlyInStock)
{
if (!is_int($productId))
{
throw new ArgumentException(__FILE__, __LINE__, 'The product ID must be an integer.');
}
if (!is_int($name))
{
throw new ArgumentException(__FILE__, __LINE__, 'The product name must be a string.');
}
// Continue with four other checks.
// Business code goes here.
}
2. Постусловия: большие проблемы
То, что легко сделать с предварительными условиями, остается невозможным для постусловий. Конечно, вы можете представить что-то вроде:
public function FindLastProduct()
{
$lastProduct = ...
// Business code goes here.
Contracts::Ensure($lastProduct instanceof Product, 'The method was about to return a non-product, when an instance of a Product class was expected.');
return $lastProduct;
}
Единственная проблема заключается в том, что этот подход не имеет ничего общего с контрактами кода, ни на уровне реализации (как в примере с предварительными условиями), ни на уровне кода (поскольку постусловия идут перед фактическим бизнес-кодом, а не между возвратом кода и метода). ).
Это также означает, что если в методе или throw
есть несколько возвратов, постусловие никогда не будет проверяться, если вы не включите $this->Ensure()
перед каждым return
или throw
(кошмар обслуживания!).
3. Инварианты: возможно?
С помощью сеттеров можно эмулировать какие-то контракты кода для свойств. Но сеттеры настолько плохо реализованы в PHP, что это вызовет слишком много проблем, и автозаполнение не будет работать, если сеттеры используются вместо полей.
4. Осуществление
В заключение, PHP не является лучшим кандидатом для контрактов кода, и, поскольку его дизайн настолько плох, у него, вероятно, никогда не будет контрактов кода, если в будущем не будет существенных изменений в дизайне языка.
В настоящее время контракты с псевдокодом2 довольно бесполезны, когда речь идет о постусловиях или инвариантах. С другой стороны, некоторые псевдо-предварительные условия могут быть легко написаны на PHP, что делает проверку аргументов намного более элегантной и короткой.
Вот краткий пример такой реализации:
class ArgumentException extends Exception
{
// Code here.
}
class CodeContracts
{
public static function Require($file, $line, $precondition, $failureMessage)
{
Contracts::Require(__FILE__, __LINE__, is_string($file), 'The source file name must be a string.');
Contracts::Require(__FILE__, __LINE__, is_int($line), 'The source file line must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_string($precondition), 'The precondition must evaluate to a boolean.');
Contracts::Require(__FILE__, __LINE__, is_int($failureMessage), 'The failure message must be a string.');
Contracts::Require(__FILE__, __LINE__, $file != '', 'The source file name cannot be an empty string.');
Contracts::Require(__FILE__, __LINE__, $line >= 0, 'The source file line cannot be negative.');
if (!$precondition)
{
throw new ContractException('The code contract was violated in ' . $file . ':' . $line . ': ' . $failureMessage);
}
}
}
Конечно, исключение может быть заменено подходом log-and-continue / log-and-stop, страницей ошибок и т. Д.
5. Заключение
Глядя на реализацию предварительных договоров, сама идея кажется бесполезной. Почему мы беспокоимся о тех контрактах псевдокода, которые на самом деле сильно отличаются от контрактов кода в обычных языках программирования? Что это приносит нам? Ничего особенного, кроме того факта, что мы можем писать чеки так же, как если бы мы использовали контракты с реальным кодом. И нет никакой причины делать это просто , потому что мы можем .
Почему кодовые контракты существуют на обычных языках? По двум причинам:
- Поскольку они обеспечивают простой способ применения условий, которые должны соответствовать при запуске или завершении блока кода,
- Потому что, когда я использую библиотеку .NET Framework, в которой используются контракты кода, я легко могу узнать в среде IDE, что требуется от метода, и что ожидается от метода, и это без доступа к исходному коду. .
Из того, что я вижу, в реализации контрактов псевдокода в PHP первая причина очень ограничена, а вторая не существует и, вероятно, никогда не будет существовать.
Это означает, что на самом деле простая проверка аргументов является хорошей альтернативой, тем более что PHP хорошо работает с массивами. Вот копия-вставка из старого личного проекта:
class ArgumentException extends Exception
{
private $argumentName = null;
public function __construct($message = '', $code = 0, $argumentName = '')
{
if (!is_string($message)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. String value expected.', 0, 'message');
if (!is_long($code)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. Integer value expected.', 0, 'code');
if (!is_string($argumentName)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. String value expected.', 0, 'argumentName');
parent::__construct($message, $code);
$this->argumentName = $argumentName;
}
public function __toString()
{
return 'exception \'' . get_class($this) . '\' ' . ((!$this->argumentName) ? '' : 'on argument \'' . $this->argumentName . '\' ') . 'with message \'' . parent::getMessage() . '\' in ' . parent::getFile() . ':' . parent::getLine() . '
Stack trace:
' . parent::getTraceAsString();
}
}
class Component
{
public static function CheckArguments($file, $line, $args)
{
foreach ($args as $argName => $argAttributes)
{
if (isset($argAttributes['type']) && (!VarTypes::MatchType($argAttributes['value'], $argAttributes['type'])))
{
throw new ArgumentException(String::Format('Invalid type for argument \'{0}\' in {1}:{2}. Expected type: {3}.', $argName, $file, $line, $argAttributes['type']), 0, $argName);
}
if (isset($argAttributes['length']))
{
settype($argAttributes['length'], 'integer');
if (is_string($argAttributes['value']))
{
if (strlen($argAttributes['value']) != $argAttributes['length'])
{
throw new ArgumentException(String::Format('Invalid length for argument \'{0}\' in {1}:{2}. Expected length: {3}. Current length: {4}.', $argName, $file, $line, $argAttributes['length'], strlen($argAttributes['value'])), 0, $argName);
}
}
else
{
throw new ArgumentException(String::Format('Invalid attributes for argument \'{0}\' in {1}:{2}. Either remove length attribute or pass a string.', $argName, $file, $line), 0, $argName);
}
}
}
}
}
Пример использования:
/// <summary>
/// Determines whether the ending of the string matches the specified string.
/// </summary>
public static function EndsWith($string, $end, $case = true)
{
Component::CheckArguments(__FILE__, __LINE__, array(
'string' => array('value' => $string, 'type' => VTYPE_STRING),
'end' => array('value' => $end, 'type' => VTYPE_STRING),
'case' => array('value' => $case, 'type' => VTYPE_BOOL)
));
$stringLength = strlen($string);
$endLength = strlen($end);
if ($endLength > $stringLength) return false;
if ($endLength == $stringLength && $string != $end) return false;
return (($case) ? substr_compare($string, $end, $stringLength - $endLength) : substr_compare($string, $end, $stringLength - $endLength, $stringLength, true)) == 0;
}
Этого будет недостаточно, если мы хотим проверить предварительные условия, которые не просто зависят от аргументов (например, проверить значение свойства в предварительном условии). Но в большинстве случаев все, что нам нужно, это проверять аргументы, и контракты псевдокода в PHP - не лучший способ сделать это.
Другими словами, если ваша единственная цель - проверить аргументы, контракты с псевдокодом являются излишним. Они могут быть возможны, когда вам нужно что-то большее, например, предварительное условие, которое зависит от свойства объекта. Но в этом последнем случае, вероятно, есть больше PHPy-способов сделать что-то⁴, поэтому единственная причина использовать контракты кода остается: , потому что мы можем .
¹ Мы можем указать, что аргумент должен быть экземпляром класса. Любопытно, что нет способа указать, что аргумент должен быть целым числом или строкой.
² Под псевдокодовыми контрактами я имею в виду, что реализация, представленная выше, очень отличается от реализации контрактов кода в .NET Framework. Реальная реализация будет возможна только при изменении самого языка.
³ Если ссылочная сборка контракта построена или, что еще лучше, если контракты указаны в файле XML.
⁴ Простой if - throw
может добиться цели.