Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Product images deletion fix #9906

Merged
merged 4 commits into from
Nov 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ Feature: Prevent deletion of purchased product
When I try to delete the "Toyota GT86 model" product
Then I should be notified that this product is in use and cannot be deleted
And this product should still exist in the product catalog

@ui
Scenario: Purchased product images should be kept
Given the store has a product "Lamborghini Gallardo Model"
And this product has an image "lamborghini.jpg" with "thumbnail" type
And there is a customer "jane.doe@gmail.com" that placed an order "#00000026"
And the customer bought a single "Lamborghini Gallardo Model"
And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment
When I try to delete the "Lamborghini Gallardo Model" product
Then I should be notified that this product is in use and cannot be deleted
And this product should still exist in the product catalog
And the product "Lamborghini Gallardo Model" should still have an accessible image
8 changes: 8 additions & 0 deletions src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,14 @@ public function thisProductShouldHaveAnImageWithType($type)
Assert::true($currentPage->isImageWithTypeDisplayed($type));
}

/**
* @Then /^the (product "[^"]+") should still have an accessible image$/
*/
public function productShouldStillHaveAnAccessibleImage(ProductInterface $product): void
{
Assert::true($this->indexPage->hasProductAccessibleImage($product->getCode()));
}

/**
* @Then /^(?:this product|it)(?:| also) should not have any images with "([^"]*)" type$/
*/
Expand Down
28 changes: 28 additions & 0 deletions src/Sylius/Behat/Page/Admin/Product/IndexPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,30 @@

namespace Sylius\Behat\Page\Admin\Product;

use Behat\Mink\Session;
use Sylius\Behat\Page\Admin\Crud\IndexPage as CrudIndexPage;
use Sylius\Behat\Service\Accessor\TableAccessorInterface;
use Sylius\Behat\Service\Checker\ImageExistenceCheckerInterface;
use Symfony\Component\Routing\RouterInterface;

final class IndexPage extends CrudIndexPage implements IndexPageInterface
{
/** @var ImageExistenceCheckerInterface */
private $imageExistenceChecker;

public function __construct(
Session $session,
array $parameters,
RouterInterface $router,
TableAccessorInterface $tableAccessor,
string $routeName,
ImageExistenceCheckerInterface $imageExistenceChecker
) {
parent::__construct($session, $parameters, $router, $tableAccessor, $routeName);

$this->imageExistenceChecker = $imageExistenceChecker;
}

/**
* {@inheritdoc}
*/
Expand All @@ -25,6 +45,14 @@ public function filterByTaxon($taxonName)
$this->getElement('taxon_filter', ['%taxon%' => $taxonName])->click();
}

public function hasProductAccessibleImage(string $productCode): bool
{
$productRow = $this->getTableAccessor()->getRowWithFields($this->getElement('table'), ['code' => $productCode]);
$imageUrl = $productRow->find('css', 'img')->getAttribute('src');

return $this->imageExistenceChecker->doesImageWithUrlExist($imageUrl, 'sylius_admin_product_thumbnail');
}

/**
* {@inheritdoc}
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Sylius/Behat/Page/Admin/Product/IndexPageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ interface IndexPageInterface extends CrudIndexPageInterface
* @param string $taxonName
*/
public function filterByTaxon($taxonName);

public function hasProductAccessibleImage(string $productCode): bool;
}
5 changes: 5 additions & 0 deletions src/Sylius/Behat/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@

<service id="sylius.behat.table_accessor" class="Sylius\Behat\Service\Accessor\TableAccessor" public="false" />

<service id="sylius.behat.checker.image_existence" class="Sylius\Behat\Service\Checker\ImageExistenceChecker">
<argument type="service" id="__symfony__.sylius.liip.filter_service" />
<argument type="string">%__symfony__.sylius_core.public_dir%</argument>
</service>

<service id="sylius.behat.paypal_api_mocker" class="Sylius\Behat\Service\Mocker\PaypalApiMocker" public="false">
<argument type="service" id="sylius.behat.mocker" />
<argument type="service" id="sylius.behat.response_loader" />
Expand Down
1 change: 1 addition & 0 deletions src/Sylius/Behat/Resources/config/services/contexts/ui.xml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
<argument type="service" id="sylius.behat.page.admin.product.index_per_taxon" />
<argument type="service" id="sylius.behat.current_page_resolver" />
<argument type="service" id="sylius.behat.notification_checker" />
<argument type="service" id="__symfony__.liip_imagine.cache.manager" />
<tag name="fob.context_service" />
</service>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</service>
<service id="sylius.behat.page.admin.product.index" class="%sylius.behat.page.admin.product.index.class%" parent="sylius.behat.page.admin.crud.index" public="false">
<argument type="string">sylius_admin_product_index</argument>
<argument type="service" id="sylius.behat.checker.image_existence" />
</service>
<service id="sylius.behat.page.admin.product.index_per_taxon" class="%sylius.behat.page.admin.product.index_per_taxon.class%" parent="sylius.behat.page.admin.crud.index" public="false">
<argument type="string">sylius_admin_product_per_taxon_index</argument>
Expand Down
40 changes: 40 additions & 0 deletions src/Sylius/Behat/Service/Checker/ImageExistenceChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\Behat\Service\Checker;

use Liip\ImagineBundle\Service\FilterService;

final class ImageExistenceChecker implements ImageExistenceCheckerInterface
{
/** @var FilterService */
private $filterService;

/** @var string */
private $mediaRootPath;

public function __construct(FilterService $filterService, string $mediaRootPath)
{
$this->filterService = $filterService;
$this->mediaRootPath = $mediaRootPath;
}

public function doesImageWithUrlExist(string $imageUrl, string $liipImagineFilter): bool
{
$imageUrl = str_replace($liipImagineFilter . '/', '', substr($imageUrl, strpos($imageUrl, $liipImagineFilter), strlen($imageUrl)));

$browserImagePath = $this->filterService->getUrlOfFilteredImage($imageUrl, $liipImagineFilter);

return file_exists($this->mediaRootPath . parse_url($browserImagePath, PHP_URL_PATH));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\Behat\Service\Checker;

interface ImageExistenceCheckerInterface
{
public function doesImageWithUrlExist(string $imageUrl, string $liipImagineImagineFilter): bool;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@

namespace Sylius\Bundle\CoreBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Liip\ImagineBundle\Imagine\Filter\FilterManager;
use Sylius\Component\Core\Model\ImageInterface;
use Sylius\Component\Core\Uploader\ImageUploaderInterface;

/**
* @internal
Zales0123 marked this conversation as resolved.
Show resolved Hide resolved
*/
final class ImagesRemoveListener
{
/** @var ImageUploaderInterface */
Expand All @@ -30,20 +34,35 @@ final class ImagesRemoveListener
/** @var FilterManager */
private $filterManager;

/** @var string[] */
private $imagesToDelete = [];

public function __construct(ImageUploaderInterface $imageUploader, CacheManager $cacheManager, FilterManager $filterManager)
{
$this->imageUploader = $imageUploader;
$this->cacheManager = $cacheManager;
$this->filterManager = $filterManager;
}

public function postRemove(LifecycleEventArgs $event): void
public function onFlush(OnFlushEventArgs $event): void
{
$image = $event->getEntity();
foreach ($event->getEntityManager()->getUnitOfWork()->getScheduledEntityDeletions() as $entityDeletion) {
if (!$entityDeletion instanceof ImageInterface) {
continue;
}

if (!in_array($entityDeletion->getPath(), $this->imagesToDelete)) {
$this->imagesToDelete[] = $entityDeletion->getPath();
}
}
}

if ($image instanceof ImageInterface) {
$this->imageUploader->remove($image->getPath());
$this->cacheManager->remove($image->getPath(), array_keys($this->filterManager->getFilterConfiguration()->all()));
public function postFlush(PostFlushEventArgs $event): void
{
foreach ($this->imagesToDelete as $key => $imagePath) {
$this->imageUploader->remove($imagePath);
Zales0123 marked this conversation as resolved.
Show resolved Hide resolved
$this->cacheManager->remove($imagePath, array_keys($this->filterManager->getFilterConfiguration()->all()));
unset($this->imagesToDelete[$key]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ parameters:
sylius.cache:
type: file_system
sylius.uploader.filesystem: sylius_image
sylius_core.public_dir: "%kernel.project_dir%/web"
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
<argument type="service" id="sylius.image_uploader" />
<argument type="service" id="liip_imagine.cache.manager" />
<argument type="service" id="liip_imagine.filter.manager" />
<tag name="doctrine.event_listener" event="postRemove" lazy="true" />
<tag name="doctrine.event_listener" event="onFlush" lazy="true" />
<tag name="doctrine.event_listener" event="postFlush" lazy="true" />
</service>
<service id="sylius.listener.order_recalculation" class="Sylius\Bundle\CoreBundle\EventListener\OrderRecalculationListener">
<argument type="service" id="sylius.order_processing.order_processor" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<service id="sylius.test.factory.promotion" class="Sylius\Component\Core\Test\Factory\TestPromotionFactory">
<argument type="service" id="sylius.factory.promotion" />
</service>
</services>

<service id="sylius.liip.filter_service" alias="liip_imagine.service.filter" public="true" />
</services>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@

namespace spec\Sylius\Bundle\CoreBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\UnitOfWork;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Liip\ImagineBundle\Imagine\Filter\FilterConfiguration;
use Liip\ImagineBundle\Imagine\Filter\FilterManager;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\CoreBundle\EventListener\ImagesRemoveListener;
use Sylius\Component\Core\Model\ImageInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Uploader\ImageUploaderInterface;

final class ImagesRemoveListenerSpec extends ObjectBehavior
Expand All @@ -35,36 +38,80 @@ function it_is_initializable(): void
$this->shouldHaveType(ImagesRemoveListener::class);
}

function it_removes_file_on_post_remove_event(
function it_saves_scheduled_entity_deletions_images_paths(
OnFlushEventArgs $event,
EntityManagerInterface $entityManager,
UnitOfWork $unitOfWork,
ImageInterface $image,
ProductInterface $product
): void {
$event->getEntityManager()->willReturn($entityManager);
$entityManager->getUnitOfWork()->willReturn($unitOfWork);
$unitOfWork->getScheduledEntityDeletions()->willReturn([$image, $product]);

$image->getPath()->shouldBeCalled();

$this->onFlush($event);
}

function it_removes_saved_images_paths(
ImageUploaderInterface $imageUploader,
CacheManager $cacheManager,
FilterManager $filterManager,
LifecycleEventArgs $event,
OnFlushEventArgs $onFlushEvent,
PostFlushEventArgs $postFlushEvent,
EntityManagerInterface $entityManager,
UnitOfWork $unitOfWork,
ImageInterface $image,
ProductInterface $product,
FilterConfiguration $filterConfiguration
): void {
$event->getEntity()->willReturn($image);
$onFlushEvent->getEntityManager()->willReturn($entityManager);
$entityManager->getUnitOfWork()->willReturn($unitOfWork);
$unitOfWork->getScheduledEntityDeletions()->willReturn([$image, $product]);

$image->getPath()->willReturn('image/path');

$filterManager->getFilterConfiguration()->willReturn($filterConfiguration);
$filterConfiguration->all()->willReturn(['sylius_small' => 'thumbnalis']);
$this->onFlush($onFlushEvent);

$imageUploader->remove('image/path')->shouldBeCalled();
$cacheManager->remove('image/path', ['sylius_small'])->shouldBeCalled();

$this->postRemove($event);
$filterManager->getFilterConfiguration()->willReturn($filterConfiguration);
$filterConfiguration->all()->willReturn(['test' => 'Test']);

$cacheManager->remove('image/path', ['test'])->shouldBeCalled();

$this->postFlush($postFlushEvent);
}

function it_does_nothing_if_entity_is_not_image(
function it_removes_saved_images_paths_from_both_filesystem_and_service_property(
ImageUploaderInterface $imageUploader,
CacheManager $cacheManager,
FilterManager $filterManager,
LifecycleEventArgs $event
OnFlushEventArgs $onFlushEvent,
PostFlushEventArgs $postFlushEvent,
EntityManagerInterface $entityManager,
UnitOfWork $unitOfWork,
ImageInterface $image,
ProductInterface $product,
FilterConfiguration $filterConfiguration
): void {
$event->getEntity()->willReturn(new \stdClass());
$filterManager->getFilterConfiguration()->shouldNotBeCalled();
$imageUploader->remove(Argument::any())->shouldNotBeCalled();
$cacheManager->remove(Argument::any(), Argument::any())->shouldNotBeCalled();
$onFlushEvent->getEntityManager()->willReturn($entityManager);
$entityManager->getUnitOfWork()->willReturn($unitOfWork);
$unitOfWork->getScheduledEntityDeletions()->willReturn([$image, $product]);

$image->getPath()->willReturn('image/path');

$this->onFlush($onFlushEvent);

$imageUploader->remove('image/path')->shouldBeCalledOnce();

$filterManager->getFilterConfiguration()->willReturn($filterConfiguration);
$filterConfiguration->all()->willReturn(['test' => 'Test']);

$cacheManager->remove('image/path', ['test'])->shouldBeCalledOnce();

$this->postRemove($event);
$this->postFlush($postFlushEvent);
$this->postFlush($postFlushEvent);
}
}