Эта статья была рецензирована Кристофером Питтом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Пост о разработке и тестировании новых функций Sylius был введением в три типа тестов, которые используются в Sylius — PHPUnit , Phpspec и Behat .
В этой части мы расширим некоторые основные классы, чтобы указать состояние инвентаря с цветовой кодировкой. Сначала мы разберемся с серверной частью. В следующем посте мы будем использовать Behat и протестировать визуальные изменения. Пожалуйста, следуйте инструкциям в предыдущем посте, чтобы запустить рабочий экземпляр.
У Sylius есть отличное решение для управления запасами . Тем не менее, всегда есть место для настройки или два. Если вы посмотрите на список продуктов (admin / products), там нет информации о наличии товара. Рассматривая варианты продукта, мы видим данные инвентаризации, отслеживаются ли они или нет, а также общее количество товаров на складе, если отслеживаются. Было бы неплохо увидеть такую информацию и на странице с перечнем продуктов. Кроме того, уровень запасов — «все или ничего» — например, зеленая метка гласит «10 Доступно в наличии» или красная «0 Доступно в наличии». Как насчет чего-то промежуточного, скажем, желтой метки «3 Доступно в наличии», чтобы указать, что запас низкий? Затем администратор магазина может решить, что пора пополнить.
Расширить ProductVariant и Модели продуктов
Мы хотим расширить поведение моделей ProductVariant
и Product
предоставляемых Sylius, чтобы мы могли видеть дополнительную информацию о наличии на складе при просмотре продуктов.
Создать пакет
Сначала мы создаем файл src/AppBundle/AppBundle.php
и регистрируем его в app/AppKernel.php
.
<?php // src/AppBundle/AppBundle.php namespace AppBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class AppBundle extends Bundle { }
<?php // app/AppKernel.php public function registerBundles() { $bundles = [ // ... new AppBundle\AppBundle(), ]; }
Далее мы информируем автозагрузчик о новом пакете, добавляя его в раздел «автозагрузка» composer.json
.
{ // ... "autoload": { "psr-4": { // ... "AppBundle\\": "src/AppBundle" } } // ... }
Затем мы запускаем composer dump-autoload
для повторного генерирования информации автозагрузчика. Теперь мы можем начать писать некоторые тесты SpecBDD.
Написание SpecBDD тестов
Наши тесты будут сосредоточены на том, как наши классы Product
и ProductVariant
помогают нам достичь целей, которые мы поставили для реализации функций, которые мы описали выше. Phpspec должен знать о наших тестах. Мы открываем phpspec.yml.dist
и добавляем следующее в «suites»:
AppBundle: { namespace: AppBundle\Entity, psr4_prefix: AppBundle\Entity, spec_path: src/AppBundle/Entity, src_path: src/AppBundle/Entity }
Возможно, было бы хорошо очистить кеш сейчас: php bin/console cache:clear
. Следующее, что нужно сделать, — это иметь спецификации, в которых мы пишем ожидаемое поведение наших классов, и Phpspec может помочь нам с этим с самого начала. Команда description является отправной точкой и создает класс спецификации:
php bin/phpspec desc AppBundle/Entity/ProductVariant
Результат:
Specification for AppBundle\Entity\ProductVariant created in src/AppBundle/Entity/spec/ProductVariantSpec.php
Давайте запустим это.
php bin/phpspec run src/AppBundle/Entity/spec/ProductVariantSpec.php
Класс ProductVariant
, описанный в спецификации, не существует, но Phpspec предложит создать его для нас. После того, как он был создан, мы можем повторить два шага выше и получить наш класс Product
.
php bin/phpspec desc AppBundle/Entity/Product php bin/phpspec run src/AppBundle/Entity/spec/ProductSpec.php
У Sylius уже есть эти два класса, поэтому нам нужно расширить их в нашем AppBundle. Поскольку Sylius часто использует интерфейсы, мы тоже создаем их.
<?php // src/AppBundle/Entity/ProductInterface.php namespace AppBundle\Entity; use Sylius\Component\Core\Model\ProductInterface as BaseProductInterface; interface ProductInterface extends BaseProductInterface { }
<?php // src/AppBundle/Entity/ProductVariantInterface.php namespace AppBundle\Entity; use Sylius\Component\Core\Model\ProductVariantInterface as BaseProductVariantInterface; interface ProductVariantInterface extends BaseProductVariantInterface { }
<?php // src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Sylius\Component\Core\Model\Product as BaseProduct; use AppBundle\Entity\ProductInterface; class Product extends BaseProduct implements ProductInterface { }
Давайте обновим сгенерированный класс ProductVariant
с $reorderLevel
свойства $reorderLevel
мы хотим представить.
<?php // src/AppBundle/Entity/ProductVariant.php namespace AppBundle\Entity; use AppBundle\Entity\ProductVariantInterface as ProductVariantInterface; use Sylius\Component\Core\Model\ProductVariant as BaseProductVariant; class ProductVariant extends BaseProductVariant implements ProductVariantInterface { const REORDER_LEVEL = 5; /** * @var int */ private $reorderLevel; }
Переопределение классов Sylius
Давайте проинформируем Sylius о наших классах, чтобы они использовались вместо тех, которые предоставляются Sylius. Мы добавляем следующее в app/config/config.yml
:
sylius_product: resources: product: classes: model: AppBundle\Entity\Product product_variant: classes: model: AppBundle\Entity\ProductVariant
Мы добавили свойство $reorderLevel
умолчанию 5 в класс ProductVariant
. Так как это будет отличаться от варианта к варианту, нам нужно изменить таблицу product_variant
чтобы мы могли также хранить эту информацию. Поскольку Sylius использует Doctrine ORM, это путь. Чтобы расширить существующее определение таблицы, давайте создадим src/AppBundle/Resources/config/doctrine/ProductVariant.orm.yml
и добавим следующее:
AppBundle\Entity\ProductVariant: type: entity table: sylius_product_variant fields: reorderLevel: column: reorder_level type: integer nullable: true
Ключом для поля является имя свойства модели. Мы должны указать имя столбца, в противном случае Doctrine по умолчанию использует имя свойства «reorderLevel». Если свойство является одним словом, например, «уровень», то столбец может быть исключен из этой конфигурации.
Далее, давайте создадим src/AppBundle/Resources/config/doctrine/Product.orm.yml
и добавим это:
AppBundle\Entity\Product: type: entity table: sylius_product
участияAppBundle\Entity\Product: type: entity table: sylius_product
Он пуст, но мы говорим, что наша расширенная модель Product все еще хранит данные в неизмененной таблице.
Обновление базы данных
Давайте запустим следующее:
php bin/console doctrine:migrations:diff
Эта команда генерирует класс миграции в app/migrations
. Обычно это последний, так как имя включает метку времени. Он содержит операторы SQL для изменения таблицы product_variant
. В реальных проектах сгенерированного кода может быть недостаточно, но вы можете отредактировать файл в соответствии с вашими требованиями. Для миграции:
php bin/console doctrine:migrations:migrate
Если по какой-то причине это не помогло, попробуйте следующее:
php bin/console doctrine:schema:update --force
Написание большего количества тестов SpecBDD
Теперь, когда мы настроили наши модели и базу данных, давайте вернемся к нашим тестам. Наши первые два примера должны теперь дать зеленые результаты, потому что классы были созданы. Давайте подтвердим с помощью:
php bin/phpspec run -fpretty --verbose src/AppBundle/Entity/spec/ProductVariantSpec.php
<?php // src/AppBundle/Entity/spec/ProductVariantSpec.php namespace spec\AppBundle\Entity; use AppBundle\Entity\ProductVariant; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class ProductVariantSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType(ProductVariant::class); } }
$this
представляет специальный объект, который будет вести себя точно так же, как наш класс ProductVariant
. Однако он будет иметь только те свойства и методы, которые мы указали в примерах. Давайте добавим метод в класс spec.
function it_has_reorder_level_by_default() { $this->getReorderLevel()->shouldReturn(ProductVariant::REORDER_LEVEL); }
Мы бы хотели, чтобы наш ProductVariant
по ProductVariant
имел значение уровня переупорядочения 5. Помните, что наш класс ProductVariant
по-прежнему пуст. Давайте попробуем запустить спецификацию. Тест не проходит хорошо, с подсказкой:
Do you want me to create `AppBundle\Entity\ProductVariant::getReorderLevel()` for you?
Если мы подтвердим, метод getReorderLevel()
в классе. Ожидается, что getReorderLevel()
найдет getReorderLevel()
который возвращает целое число, представляющее порог запасов, ниже которого запас должен быть пополнен. Все, что нам нужно сделать в методе, это вернуть свойство $reorderLevel
.
Обычно return $this->reorderLevel;
было бы достаточно, но у нас уже есть продукты в нашем магазине, поэтому нам нужно найти способ получить значение уровня повторного заказа по умолчанию. Один из способов сделать это:
/** * @return int */ public function getReorderLevel() { return !empty($this->reorderLevel) ? $this->reorderLevel : self::REORDER_LEVEL; }
Тест проходит. Мы можем обновить ProductVariantInterface
с помощью public function getReorderLevel();
, Далее мы хотим убедиться, что можем установить уровень переупорядочения. Вот пример:
function it_has_reorder_level() { $this->setReorderLevel(10); $this->getReorderLevel()->shouldReturn(10); }
Когда мы запустим его, Phpspec узнает, что метода setReorderLevel()
и предложит его создать. Обновите созданный метод следующим образом:
/** * @param int $reorderLevel */ public function setReorderLevel($reorderLevel) { $this->reorderLevel = $reorderLevel; }
Мы запускаем его, и он весь зеленый. Не забудьте обновить интерфейс тоже. Теперь мы хотим проверить, можно ли переупорядочить элемент, т. Е. Должен ли он быть помечен как достаточно низкий, чтобы его можно было пополнить. Он должен отслеживаться, а количество в наличии должно быть ниже или равно уровню повторного заказа:
function it_is_reorderable_when_tracked_and_available_stock_is_at_reorder_level() { $this->setTracked(true); $this->setOnHand(3); $this->setReorderLevel(4); $this->isReorderable()->shouldReturn(true); }
Когда мы запускаем спецификацию, он находит все методы, кроме isReorderable()
которые он предлагает создать для нас в классе ProductVariant
.
В методе нам нужно проверить, что вариант отслеживается, а количество в наличии меньше или равно уровню повторного заказа.
public function isReorderable() { return $this->isTracked() && ($this->getOnHand() <= $this->getReorderLevel()); }
Все тесты проходят. Давайте обновим интерфейс с помощью public function isReorderable();
, Возможно, вы также захотите добавить тест, когда доступный запас превышает уровень повторного заказа: он не должен переупорядочиваться.
function it_is_not_reorderable_when_tracked_and_available_stock_is_greater_than_reorder_level() { $this->setTracked(true); $this->setOnHand(10); $this->setReorderLevel(4); $this->isReorderable()->shouldReturn(false); }
Запуск спецификации теперь должен дать нам что-то вроде этого:
Мы закончили с ProductVariant
. Мы сделаем нечто подобное с вариантами, которые есть в продукте, но немного по-другому. Нам нужен продукт для каждого примера, который мы пишем, и было бы утомительно создавать продукт с минимум двумя вариантами для каждого.
В Phpspec мы можем написать метод let()
который letGo()
перед каждым примером, и letGo()
после его запуска. В src/AppBundle/Entity/spec/ProductSpec.php
мы добавим следующее, не забывая импортировать ProductVariantInterface
с use AppBundle\Entity\ProductVariantInterface as VariantInterface;
на вершине класса.
function let(VariantInterface $firstVariant, VariantInterface $secondVariant) { $firstVariant->setOnHand(4); $firstVariant->getOnHand()->willReturn(4); $firstVariant->setReorderLevel(4); $firstVariant->getReorderLevel()->willReturn(4); $firstVariant->isTracked()->willReturn(true); $firstVariant->isReorderable()->willReturn(true); $firstVariant->setProduct($this)->shouldBeCalled(); $this->addVariant($firstVariant); $secondVariant->setOnHand(10); $secondVariant->getOnHand()->willReturn(10); $secondVariant->setReorderLevel(3); $secondVariant->getReorderLevel()->willReturn(3); $secondVariant->isTracked()->willReturn(true); $secondVariant->isReorderable()->willReturn(false); $secondVariant->setProduct($this)->shouldBeCalled(); $this->addVariant($secondVariant); }
Теперь мы можем приступить к написанию примеров и проверке, чтобы наши тесты были зелеными на каждом этапе пути:
php bin/phpspec run -fpretty --verbose src/AppBundle/Entity/spec/ProductSpec.php
Первое, что мы хотим гарантировать, это то, что имеющееся в наличии количество продукта представляет собой сумму имеющихся в наличии вариантов продукта. В ProductSpec.php
мы добавляем этот пример:
function it_gets_products_on_hand_as_sum_of_product_variants_on_hand() { $this->getVariants()->shouldHaveCount(2); $this->getOnHand()->shouldReturn(14); }
Конечно, когда вы запускаете его, класс Product
не имеет getOnHand()
поэтому мы просим Phpspec создать его для нас, прежде чем мы его реализуем.
public function getOnHand() { $onHand = 0; foreach ($this->getVariants() as $variant) { if ($variant->isTracked()) { $onHand += $variant->getOnHand(); } } return $this->onHand = $onHand; }
Мы не должны забывать добавить public function getOnHand();
в ProductInterface
, тоже.
Наконец, продукт переупорядочен, когда по крайней мере один из его вариантов переупорядочен. Пример выглядит следующим образом:
function it_recognizes_when_to_reorder_stock() { $this->isReorderable()->shouldReturn(true); }
Опять же, мы позволяем Phpsepc создать метод isReorderable()
. Давайте обновим ProductInterface
с помощью public function isReorderable();
, Полные реализации должны быть:
public function isReorderable() { $variants = $this->getVariants(); $trackedVariants = $variants->filter(function (ProductVariantInterface $variant) { return $variant->isReorderable() === true; }); return $this->reorderable = count($trackedVariants) > 0; }
Мы также должны иметь возможность проверить, отслеживается ли продукт по аналогичной логике — по крайней мере, один из его вариантов должен быть отслеживаемым. Пример Phpspec для этого прост, поскольку оба варианта, которые мы получили, отслеживаются.
function it_recognizes_product_as_tracked_if_at_least_one_variant_is_tracked() { $this->isTracked()->shouldReturn(true); }
Теперь мы запустим спецификацию и обновим сгенерированный isTracked()
:
public function isTracked() { $variants = $this->getVariants(); $trackedVariants = $variants->filter( function (ProductVariantInterface $variant) { return $variant->isTracked() === true; } ); return $this->tracked = count($trackedVariants) > 0; }
Все хорошо. Возможно, нам следует добавить пример, чтобы убедиться, что isTracked()
возвращает false
если ни один из вариантов не отслеживается. Оба варианта в нашем методе let()
уже отслеживаются. Однако, если мы передадим две переменные с соответствующими именами, как в методе let()
, мы можем переопределить поведение объектов в нашем новом примере.
function it_recognizes_product_as_not_tracked_if_no_variant_is_tracked( VariantInterface $firstVariant, VariantInterface $secondVariant ) { $firstVariant->isTracked()->willReturn(false); $secondVariant->isTracked()->willReturn(false); $this->isTracked()->shouldReturn(false); }
Мы также можем немного поиграть с логическими значениями. Например, если мы изменим $this->isTracked()->shouldReturn(false);
$this->isTracked()->shouldReturn(true);
мы получаем сообщение как:
AppBundle/Entity/Product recognizes product as not tracked if no variant is tracked expected true, but got false.
Давайте изменим его обратно на false
, поэтому мы продолжаем со всех зеленых. Это завершает наши тесты для классов Product
и ProductVariant
. Позже нам понадобится несколько вещей, таких как getOnHold()
который проверяет аналогичный метод на наличие вариантов. Мы должны получить класс следующим образом:
<?php // src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Sylius\Component\Core\Model\Product as BaseProduct; use AppBundle\Entity\ProductVariantInterface as ProductVariantInterface; class Product extends BaseProduct implements ProductInterface { /** * @var int */ protected $onHold = 0; /** * @var int */ protected $onHand = 0; /** * @var bool */ protected $tracked = false; /** * @var bool */ protected $reorderable = false; public function isTracked() { $variants = $this->getVariants(); $trackedVariants = $variants->filter( function (ProductVariantInterface $variant) { return $variant->isTracked() === true; } ); return $this->tracked = count($trackedVariants) > 0; } public function getOnHand() { $onHand = 0; foreach ($this->getVariants() as $variant) { if ($variant->isTracked() === true) { $onHand += $variant->getOnHand(); } } return $this->onHand = $onHand; } public function getOnHold() { $onHold = 0; foreach ($this->getVariants() as $variant) { if ($variant->isTracked()) { $onHold += $variant->getOnHold(); } } return $this->onHold = $onHold; } public function isReorderable() { $variants = $this->getVariants(); $trackedVariants = $variants->filter( function (ProductVariantInterface $variant) { return $variant->isReorderable() === true; } ); return $this->reorderable = count($trackedVariants) > 0; } }
Вывод
Теперь мы узнали, как правильно расширить основные функциональные возможности Sylius — путем тщательного тестирования во время разработки. Мы добавили некоторые новые функции, позволили Phpspec генерировать для них заглушки и сделали все тесты с красного на зеленый, прежде чем продолжить нашу разработку.
В заключительной части этой серии «Расширение Sylius» мы будем работать с Behat и протестировать визуальный вывод изменений, вызванных нашими функциями. Будьте на связи!