diff --git a/lib/API/JSON/v1/DynamicSegments.php b/lib/API/JSON/v1/DynamicSegments.php index 5286967265..28415dc831 100644 --- a/lib/API/JSON/v1/DynamicSegments.php +++ b/lib/API/JSON/v1/DynamicSegments.php @@ -2,22 +2,20 @@ namespace MailPoet\API\JSON\v1; +use InvalidArgumentException; use MailPoet\API\JSON\Endpoint as APIEndpoint; use MailPoet\API\JSON\Error; use MailPoet\API\JSON\Response; use MailPoet\API\JSON\ResponseBuilders\DynamicSegmentsResponseBuilder; use MailPoet\Config\AccessControl; -use MailPoet\DynamicSegments\Exceptions\ErrorSavingException; -use MailPoet\DynamicSegments\Exceptions\InvalidSegmentTypeException; use MailPoet\DynamicSegments\Mappers\DBMapper; -use MailPoet\DynamicSegments\Mappers\FormDataMapper; use MailPoet\DynamicSegments\Persistence\Loading\SingleSegmentLoader; -use MailPoet\DynamicSegments\Persistence\Saver; use MailPoet\Entities\SegmentEntity; use MailPoet\Listing\BulkActionController; use MailPoet\Listing\Handler; -use MailPoet\Models\Model; use MailPoet\Segments\DynamicSegments\DynamicSegmentsListingRepository; +use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException; +use MailPoet\Segments\DynamicSegments\SegmentSaveController; use MailPoet\Segments\SegmentsRepository; use MailPoet\WP\Functions as WPFunctions; @@ -27,12 +25,6 @@ class DynamicSegments extends APIEndpoint { 'global' => AccessControl::PERMISSION_MANAGE_SEGMENTS, ]; - /** @var FormDataMapper */ - private $mapper; - - /** @var Saver */ - private $saver; - /** @var SingleSegmentLoader */ private $dynamicSegmentsLoader; @@ -51,24 +43,25 @@ class DynamicSegments extends APIEndpoint { /** @var DynamicSegmentsResponseBuilder */ private $segmentsResponseBuilder; + /** @var SegmentSaveController */ + private $saveController; + public function __construct( BulkActionController $bulkAction, Handler $handler, DynamicSegmentsListingRepository $dynamicSegmentsListingRepository, DynamicSegmentsResponseBuilder $segmentsResponseBuilder, SegmentsRepository $segmentsRepository, - $mapper = null, - $saver = null, + SegmentSaveController $saveController, $dynamicSegmentsLoader = null ) { $this->bulkAction = $bulkAction; $this->listingHandler = $handler; - $this->mapper = $mapper ?: new FormDataMapper(); - $this->saver = $saver ?: new Saver(); $this->dynamicSegmentsLoader = $dynamicSegmentsLoader ?: new SingleSegmentLoader(new DBMapper()); $this->dynamicSegmentsListingRepository = $dynamicSegmentsListingRepository; $this->segmentsResponseBuilder = $segmentsResponseBuilder; $this->segmentsRepository = $segmentsRepository; + $this->saveController = $saveController; } public function get($data = []) { @@ -92,38 +85,34 @@ class DynamicSegments extends APIEndpoint { public function save($data) { try { - $dynamicSegment = $this->mapper->mapDataToDB($data); - $this->saver->save($dynamicSegment); - - return $this->successResponse($data); - } catch (InvalidSegmentTypeException $e) { + $segment = $this->saveController->save($data); + return $this->successResponse($this->segmentsResponseBuilder->build($segment)); + } catch (InvalidFilterException $e) { return $this->errorResponse([ Error::BAD_REQUEST => $this->getErrorString($e), ], [], Response::STATUS_BAD_REQUEST); - } catch (ErrorSavingException $e) { - $statusCode = Response::STATUS_UNKNOWN; - if ($e->getCode() === Model::DUPLICATE_RECORD) { - $statusCode = Response::STATUS_CONFLICT; - } - return $this->errorResponse([$statusCode => $e->getMessage()], [], $statusCode); + } catch (InvalidArgumentException $e) { + return $this->badRequest([ + Error::BAD_REQUEST => __('Another record already exists. Please specify a different "name".', 'mailpoet'), + ]); } } - private function getErrorString(InvalidSegmentTypeException $e) { + private function getErrorString(InvalidFilterException $e) { switch ($e->getCode()) { - case InvalidSegmentTypeException::MISSING_TYPE: + case InvalidFilterException::MISSING_TYPE: return WPFunctions::get()->__('Segment type is missing.', 'mailpoet'); - case InvalidSegmentTypeException::INVALID_TYPE: + case InvalidFilterException::INVALID_TYPE: return WPFunctions::get()->__('Segment type is unknown.', 'mailpoet'); - case InvalidSegmentTypeException::MISSING_ROLE: + case InvalidFilterException::MISSING_ROLE: return WPFunctions::get()->__('Please select user role.', 'mailpoet'); - case InvalidSegmentTypeException::MISSING_ACTION: + case InvalidFilterException::MISSING_ACTION: return WPFunctions::get()->__('Please select email action.', 'mailpoet'); - case InvalidSegmentTypeException::MISSING_NEWSLETTER_ID: + case InvalidFilterException::MISSING_NEWSLETTER_ID: return WPFunctions::get()->__('Please select an email.', 'mailpoet'); - case InvalidSegmentTypeException::MISSING_PRODUCT_ID: + case InvalidFilterException::MISSING_PRODUCT_ID: return WPFunctions::get()->__('Please select category.', 'mailpoet'); - case InvalidSegmentTypeException::MISSING_CATEGORY_ID: + case InvalidFilterException::MISSING_CATEGORY_ID: return WPFunctions::get()->__('Please select product.', 'mailpoet'); default: return WPFunctions::get()->__('An error occurred while saving data.', 'mailpoet'); diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index 0913eaccb1..4224d55173 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -263,6 +263,8 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\UserRole::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory::class)->setPublic(true); + $container->autowire(\MailPoet\Segments\DynamicSegments\SegmentSaveController::class)->setPublic(true); + $container->autowire(\MailPoet\Segments\DynamicSegments\FilterDataMapper::class)->setPublic(true); // Services $container->autowire(\MailPoet\Services\Bridge::class)->setPublic(true); $container->autowire(\MailPoet\Services\AuthorizedEmailsController::class)->setPublic(true); diff --git a/lib/Entities/DynamicSegmentFilterEntity.php b/lib/Entities/DynamicSegmentFilterEntity.php index 4fcc591134..ee311c0d1c 100644 --- a/lib/Entities/DynamicSegmentFilterEntity.php +++ b/lib/Entities/DynamicSegmentFilterEntity.php @@ -53,4 +53,8 @@ class DynamicSegmentFilterEntity { public function setSegment(SegmentEntity $segment) { $this->segment = $segment; } + + public function setFilterData(DynamicSegmentFilterData $filterData) { + $this->filterData = $filterData; + } } diff --git a/lib/Segments/DynamicSegments/SegmentSaveController.php b/lib/Segments/DynamicSegments/SegmentSaveController.php new file mode 100644 index 0000000000..cb86a77beb --- /dev/null +++ b/lib/Segments/DynamicSegments/SegmentSaveController.php @@ -0,0 +1,40 @@ +segmentsRepository = $segmentsRepository; + $this->filterDataMapper = $filterDataMapper; + } + + public function save(array $data = []): SegmentEntity { + $id = isset($data['id']) ? (int)$data['id'] : null; + $name = $data['name'] ?? ''; + $description = $data['description'] ?? ''; + $filterData = $this->filterDataMapper->map($data); + + $this->checkSegmentUniqueName($name, $id); + + return $this->segmentsRepository->createOrUpdate($name, $description, SegmentEntity::TYPE_DYNAMIC, $filterData, $id); + } + + private function checkSegmentUniqueName(string $name, ?int $id): void { + if (!$this->segmentsRepository->isNameUnique($name, $id)) { + throw new InvalidArgumentException("Segment with name: '{$name}' already exists."); + } + } +} diff --git a/lib/Segments/SegmentSaveController.php b/lib/Segments/SegmentSaveController.php index 1cbcd17add..ca4bee00bc 100644 --- a/lib/Segments/SegmentSaveController.php +++ b/lib/Segments/SegmentSaveController.php @@ -29,7 +29,7 @@ class SegmentSaveController { $this->checkSegmentUniqueName($name, $id); - return $this->segmentsRepository->createOrUpdate($name, $description, $id); + return $this->segmentsRepository->createOrUpdate($name, $description, SegmentEntity::TYPE_DEFAULT, null, $id); } public function duplicate(SegmentEntity $segmentEntity): SegmentEntity { diff --git a/lib/Segments/SegmentsRepository.php b/lib/Segments/SegmentsRepository.php index 6d43b20d04..e8edea6ac8 100644 --- a/lib/Segments/SegmentsRepository.php +++ b/lib/Segments/SegmentsRepository.php @@ -4,6 +4,8 @@ namespace MailPoet\Segments; use DateTime; use MailPoet\Doctrine\Repository; +use MailPoet\Entities\DynamicSegmentFilterData; +use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\SegmentEntity; use MailPoet\Entities\SubscriberSegmentEntity; use MailPoet\NotFoundException; @@ -58,6 +60,8 @@ class SegmentsRepository extends Repository { public function createOrUpdate( string $name, string $description = '', + string $type = SegmentEntity::TYPE_DEFAULT, + ?DynamicSegmentFilterData $filterData = null, ?int $id = null ): SegmentEntity { if ($id) { @@ -68,9 +72,21 @@ class SegmentsRepository extends Repository { $segment->setName($name); $segment->setDescription($description); } else { - $segment = new SegmentEntity($name, SegmentEntity::TYPE_DEFAULT, $description); + $segment = new SegmentEntity($name, $type, $description); $this->persist($segment); } + + if ($filterData instanceof DynamicSegmentFilterData) { + // So far we allow only one filter + $filterEntity = $segment->getDynamicFilters()->first(); + if (!$filterEntity instanceof DynamicSegmentFilterEntity) { + $filterEntity = new DynamicSegmentFilterEntity($segment, $filterData); + $segment->getDynamicFilters()->add($filterEntity); + $this->entityManager->persist($filterEntity); + } else { + $filterEntity->setFilterData($filterData); + } + } $this->flush(); return $segment; } diff --git a/tests/integration/API/JSON/v1/DynamicSegmentsTest.php b/tests/integration/API/JSON/v1/DynamicSegmentsTest.php index 7b833eb7b8..8f6d3a9d71 100644 --- a/tests/integration/API/JSON/v1/DynamicSegmentsTest.php +++ b/tests/integration/API/JSON/v1/DynamicSegmentsTest.php @@ -3,11 +3,8 @@ namespace MailPoet\API\JSON\v1; use Codeception\Stub; -use Codeception\Stub\Expected; use MailPoet\API\JSON\ResponseBuilders\DynamicSegmentsResponseBuilder; use MailPoet\DI\ContainerWrapper; -use MailPoet\DynamicSegments\Exceptions\ErrorSavingException; -use MailPoet\DynamicSegments\Exceptions\InvalidSegmentTypeException; use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\SegmentEntity; @@ -15,8 +12,8 @@ use MailPoet\Listing\BulkActionController; use MailPoet\Listing\Handler; use MailPoet\Models\DynamicSegment; use MailPoet\Models\DynamicSegmentFilter; -use MailPoet\Models\Model; use MailPoet\Segments\DynamicSegments\DynamicSegmentsListingRepository; +use MailPoet\Segments\DynamicSegments\SegmentSaveController; use MailPoet\Segments\SegmentsRepository; class DynamicSegmentsTest extends \MailPoetTest { @@ -37,6 +34,8 @@ class DynamicSegmentsTest extends \MailPoetTest { private $responseBuilder; /** @var SegmentsRepository */ private $segmentsRepository; + /** @var SegmentSaveController */ + private $saveController; public function _before() { $this->bulkAction = ContainerWrapper::getInstance()->get(BulkActionController::class); @@ -44,6 +43,7 @@ class DynamicSegmentsTest extends \MailPoetTest { $this->listingRepository = ContainerWrapper::getInstance()->get(DynamicSegmentsListingRepository::class); $this->responseBuilder = ContainerWrapper::getInstance()->get(DynamicSegmentsResponseBuilder::class); $this->segmentsRepository = ContainerWrapper::getInstance()->get(SegmentsRepository::class); + $this->saveController = ContainerWrapper::getInstance()->get(SegmentSaveController::class); } public function testGetReturnsResponse() { @@ -54,9 +54,7 @@ class DynamicSegmentsTest extends \MailPoetTest { $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, - null, - null, - null + $this->saveController ); $response = $endpoint->get(['id' => $segment->getId()]); expect($response)->isInstanceOf('\MailPoet\API\JSON\SuccessResponse'); @@ -65,59 +63,47 @@ class DynamicSegmentsTest extends \MailPoetTest { } public function testGetReturnsError() { - $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, null, null, null); + $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $this->saveController); $response = $endpoint->get(['id' => 5]); expect($response)->isInstanceOf('\MailPoet\API\JSON\ErrorResponse'); expect($response->status)->equals(self::SEGMENT_NOT_FOUND_RESPONSE_CODE); } public function testSaverSavesData() { - $mapper = Stub::makeEmpty('\MailPoet\DynamicSegments\Mappers\FormDataMapper', ['mapDataToDB' => Expected::once(function () { - $dynamicSegment = DynamicSegment::create(); - $dynamicSegment->hydrate([ - 'name' => 'name', - 'description' => 'description', - ]); - return $dynamicSegment; - })]); - $saver = Stub::makeEmpty('\MailPoet\DynamicSegments\Persistence\Saver', ['save' => Expected::once()]); - - $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $mapper, $saver); - $response = $endpoint->save([]); + $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $this->saveController); + $response = $endpoint->save([ + 'name' => 'Test dynamic', + 'description' => 'description dynamic', + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => 'editor', + ]); expect($response)->isInstanceOf('\MailPoet\API\JSON\SuccessResponse'); expect($response->status)->equals(self::SUCCESS_RESPONSE_CODE); + expect($response->data['name'])->equals('Test dynamic'); } - public function testSaverReturnsErrorOnInvalidData() { - $mapper = Stub::makeEmpty('\MailPoet\DynamicSegments\Mappers\FormDataMapper', ['mapDataToDB' => Expected::once(function () { - throw new InvalidSegmentTypeException(); - })]); - $saver = Stub::makeEmpty('\MailPoet\DynamicSegments\Persistence\Saver', ['save' => Expected::never()]); - - $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $mapper, $saver); - $response = $endpoint->save([]); + public function testSaverReturnsErrorOnInvalidFilterData() { + $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $this->saveController); + $response = $endpoint->save([ + 'name' => 'Test dynamic', + ]); expect($response)->isInstanceOf('\MailPoet\API\JSON\ErrorResponse'); expect($response->status)->equals(self::INVALID_DATA_RESPONSE_CODE); + expect($response->errors[0]['message'])->equals('Segment type is missing.'); } - public function testSaverReturnsErrorOnSave() { - $mapper = Stub::makeEmpty('\MailPoet\DynamicSegments\Mappers\FormDataMapper', ['mapDataToDB' => Expected::once(function () { - $dynamicSegment = DynamicSegment::create(); - $dynamicSegment->hydrate([ - 'name' => 'name', - 'description' => 'description', - ]); - return $dynamicSegment; - })]); - $saver = Stub::makeEmpty('\MailPoet\DynamicSegments\Persistence\Saver', ['save' => Expected::once(function () { - throw new ErrorSavingException('Error saving data', Model::DUPLICATE_RECORD); - })]); - - $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $mapper, $saver); - $response = $endpoint->save([]); + public function testSaverReturnsErrorOnDuplicateRecord() { + $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $this->saveController); + $data = [ + 'name' => 'Test dynamic', + 'description' => 'description dynamic', + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => 'editor', + ]; + $endpoint->save($data); + $response = $endpoint->save($data); expect($response)->isInstanceOf('\MailPoet\API\JSON\ErrorResponse'); - expect($response->status)->equals(self::SERVER_ERROR_RESPONSE_CODE); - expect($response->errors[0]['message'])->equals('Error saving data'); + expect($response->status)->equals(self::INVALID_DATA_RESPONSE_CODE); } public function testItCanTrashASegment() { @@ -132,7 +118,7 @@ class DynamicSegmentsTest extends \MailPoetTest { }, ]); - $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, null, null, $loader); + $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $this->saveController, $loader); $response = $endpoint->trash(['id' => $dynamicSegment->id]); expect($response->status)->equals(self::SUCCESS_RESPONSE_CODE); @@ -158,7 +144,7 @@ class DynamicSegmentsTest extends \MailPoetTest { }, ]); - $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, null, null, $loader); + $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $this->saveController, $loader); $response = $endpoint->restore(['id' => $dynamicSegment->id]); expect($response->status)->equals(self::SUCCESS_RESPONSE_CODE); @@ -187,7 +173,7 @@ class DynamicSegmentsTest extends \MailPoetTest { }, ]); - $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, null, null, $loader); + $endpoint = new DynamicSegments($this->bulkAction, $this->listingHandler, $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, $this->saveController, $loader); $response = $endpoint->delete(['id' => $dynamicSegment->id]); expect($response->status)->equals(self::SUCCESS_RESPONSE_CODE); @@ -218,9 +204,7 @@ class DynamicSegmentsTest extends \MailPoetTest { $this->listingRepository, $this->responseBuilder, $this->segmentsRepository, - null, - null, - null + $this->saveController ); $response = $endpoint->bulkAction([ 'action' => 'trash', @@ -260,4 +244,10 @@ class DynamicSegmentsTest extends \MailPoetTest { $this->entityManager->flush(); return $segment; } + + public function _after() { + parent::_after(); + $this->truncateEntity(SegmentEntity::class); + $this->truncateEntity(DynamicSegmentFilterEntity::class); + } } diff --git a/tests/integration/Segments/DynamicSegments/SegmentSaveControllerTest.php b/tests/integration/Segments/DynamicSegments/SegmentSaveControllerTest.php new file mode 100644 index 0000000000..1a85a00638 --- /dev/null +++ b/tests/integration/Segments/DynamicSegments/SegmentSaveControllerTest.php @@ -0,0 +1,79 @@ +cleanup(); + $this->saveController = $this->diContainer->get(SegmentSaveController::class); + } + + public function testItCanSaveASegment() { + $segmentData = [ + 'name' => 'Test Segment', + 'description' => 'Description', + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => 'editor', + ]; + + $segment = $this->saveController->save($segmentData); + expect($segment->getName())->equals('Test Segment'); + expect($segment->getDescription())->equals('Description'); + expect($segment->getDynamicFilters()->count())->equals(1); + expect($segment->getType())->equals(SegmentEntity::TYPE_DYNAMIC); + $filter = $segment->getDynamicFilters()->first(); + assert($filter instanceof DynamicSegmentFilterEntity); + expect($filter->getFilterData()->getData())->equals([ + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => 'editor', + ]); + } + + public function testItCheckDuplicateSegment() { + $name = 'Test name'; + $this->createSegment($name); + $segmentData = [ + 'name' => $name, + 'description' => 'Description', + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => 'editor', + ]; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Segment with name: 'Test name' already exists."); + $this->saveController->save($segmentData); + } + + public function testItValidatesSegmentFilterData() { + $name = 'Test name'; + $this->createSegment($name); + $segmentData = [ + 'name' => $name, + 'description' => 'Description', + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => null, + ]; + $this->expectException(InvalidFilterException::class); + $this->saveController->save($segmentData); + } + + private function createSegment(string $name): SegmentEntity { + $segment = new SegmentEntity($name, SegmentEntity::TYPE_DEFAULT, 'description'); + $this->entityManager->persist($segment); + $this->entityManager->flush(); + return $segment; + } + + private function cleanup() { + $this->truncateEntity(SegmentEntity::class); + $this->truncateEntity(DynamicSegmentFilterEntity::class); + } +}