├── .gitignore ├── phpspec.yml.dist ├── Tests ├── Dummy │ ├── TraitPass.php │ ├── DummyClassOne.php │ └── DummyClassTwo.php ├── Metadata │ └── MetadataTest.php └── Reflection │ └── ClassReflectionTest.php ├── phpunit.xml.dist ├── Factory ├── TranslatableFactoryInterface.php ├── FactoryInterface.php ├── Factory.php └── TranslatableFactory.php ├── Model ├── ResourceInterface.php ├── CodeAwareInterface.php ├── SlugAwareInterface.php ├── VersionedInterface.php ├── ResourceLogEntry.php ├── ArchivableInterface.php ├── TranslationInterface.php ├── TimestampableInterface.php ├── ArchivableTrait.php ├── ToggleableInterface.php ├── ToggleableTrait.php ├── TimestampableTrait.php ├── TranslatableInterface.php ├── AbstractTranslation.php └── TranslatableTrait.php ├── Repository ├── Exception │ └── ExistingResourceException.php ├── RepositoryInterface.php └── InMemoryRepository.php ├── Exception ├── VariantWithNoOptionsValuesException.php ├── UnsupportedMethodException.php ├── RaceConditionException.php ├── UnexpectedTypeException.php ├── DeleteHandlingException.php └── UpdateHandlingException.php ├── Translation ├── TranslatableEntityLocaleAssignerInterface.php ├── Provider │ ├── TranslationLocaleProviderInterface.php │ └── ImmutableTranslationLocaleProvider.php └── TranslatableEntityLocaleAssigner.php ├── Generator ├── RandomnessGeneratorInterface.php └── RandomnessGenerator.php ├── ResourceActions.php ├── spec ├── Fixtures │ └── SampleBookResourceInterface.php ├── Repository │ ├── Exception │ │ └── ExistingResourceExceptionSpec.php │ └── InMemoryRepositorySpec.php ├── Exception │ ├── UnsupportedMethodExceptionSpec.php │ ├── UnexpectedTypeExceptionSpec.php │ ├── DeleteHandlingExceptionSpec.php │ ├── UpdateHandlingExceptionSpec.php │ └── RaceConditionExceptionSpec.php ├── Factory │ ├── FactorySpec.php │ └── TranslatableFactorySpec.php ├── Translation │ ├── Provider │ │ └── ImmutableTranslationLocaleProviderSpec.php │ └── TranslatableEntityLocaleAssignerSpec.php ├── Generator │ └── RandomnessGeneratorSpec.php ├── Model │ └── AbstractTranslationSpec.php └── Metadata │ ├── RegistrySpec.php │ └── MetadataSpec.php ├── Storage └── StorageInterface.php ├── StateMachine ├── StateMachineInterface.php └── StateMachine.php ├── Metadata ├── RegistryInterface.php ├── MetadataInterface.php ├── Registry.php └── Metadata.php ├── LICENSE ├── composer.json ├── README.md ├── Reflection └── ClassReflection.php └── Annotation ├── SyliusCrudRoutes.php └── SyliusRoute.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | !/vendor/.gitignore 3 | 4 | /bin/ 5 | 6 | /composer.phar 7 | /composer.lock 8 | 9 | /.phpunit.result.cache 10 | -------------------------------------------------------------------------------- /phpspec.yml.dist: -------------------------------------------------------------------------------- 1 | suites: 2 | main: 3 | namespace: Sylius\Component\Resource 4 | psr4_prefix: Sylius\Component\Resource 5 | src_path: . 6 | -------------------------------------------------------------------------------- /Tests/Dummy/TraitPass.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | ./Tests/ 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Factory/TranslatableFactoryInterface.php: -------------------------------------------------------------------------------- 1 | archivedAt; 27 | } 28 | 29 | public function setArchivedAt(?\DateTimeInterface $archivedAt): void 30 | { 31 | $this->archivedAt = $archivedAt; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Exception/RaceConditionException.php: -------------------------------------------------------------------------------- 1 | getCode() : 0, 25 | $previous 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Model/ToggleableInterface.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(\Exception::class); 23 | } 24 | 25 | function it_has_a_message(): void 26 | { 27 | $this->getMessage()->shouldReturn('Given resource already exists in the repository.'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | className = $className; 33 | } 34 | 35 | public function createNew() 36 | { 37 | return new $this->className(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/Exception/UnsupportedMethodExceptionSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('methodName'); 23 | } 24 | 25 | function it_extends_exception(): void 26 | { 27 | $this->shouldHaveType(\Exception::class); 28 | } 29 | 30 | function it_has_a_message(): void 31 | { 32 | $this->getMessage()->shouldReturn('The method "methodName" is not supported.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spec/Factory/FactorySpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(\stdClass::class); 24 | } 25 | 26 | function it_implements_factory_interface(): void 27 | { 28 | $this->shouldHaveType(FactoryInterface::class); 29 | } 30 | 31 | function it_creates_a_new_instance_of_a_resource(): void 32 | { 33 | $this->createNew()->shouldHaveType(\stdClass::class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Model/ToggleableTrait.php: -------------------------------------------------------------------------------- 1 | enabled; 24 | } 25 | 26 | /** 27 | * @param bool $enabled 28 | */ 29 | public function setEnabled(?bool $enabled): void 30 | { 31 | $this->enabled = (bool) $enabled; 32 | } 33 | 34 | public function enable(): void 35 | { 36 | $this->enabled = true; 37 | } 38 | 39 | public function disable(): void 40 | { 41 | $this->enabled = false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spec/Exception/UnexpectedTypeExceptionSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('stringValue', '\ExpectedType'); 23 | } 24 | 25 | function it_extends_invalid_argument_exception(): void 26 | { 27 | $this->shouldHaveType(\InvalidArgumentException::class); 28 | } 29 | 30 | function it_has_a_message(): void 31 | { 32 | $this->getMessage()->shouldReturn('Expected argument of type "\ExpectedType", "string" given.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /StateMachine/StateMachineInterface.php: -------------------------------------------------------------------------------- 1 | createdAt; 27 | } 28 | 29 | public function setCreatedAt(?\DateTimeInterface $createdAt): void 30 | { 31 | $this->createdAt = $createdAt; 32 | } 33 | 34 | public function getUpdatedAt(): ?\DateTimeInterface 35 | { 36 | return $this->updatedAt; 37 | } 38 | 39 | public function setUpdatedAt(?\DateTimeInterface $updatedAt): void 40 | { 41 | $this->updatedAt = $updatedAt; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spec/Exception/DeleteHandlingExceptionSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(\Exception::class); 23 | } 24 | 25 | function it_has_a_message(): void 26 | { 27 | $this->getMessage()->shouldReturn('Ups, something went wrong during deleting a resource, please try again.'); 28 | } 29 | 30 | function it_has_a_flash(): void 31 | { 32 | $this->getFlash()->shouldReturn('something_went_wrong_error'); 33 | } 34 | 35 | function it_has_an_api_response_code(): void 36 | { 37 | $this->getApiResponseCode()->shouldReturn(500); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/Exception/UpdateHandlingExceptionSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(\Exception::class); 23 | } 24 | 25 | function it_has_a_message(): void 26 | { 27 | $this->getMessage()->shouldReturn('Ups, something went wrong during updating a resource, please try again.'); 28 | } 29 | 30 | function it_has_a_flash(): void 31 | { 32 | $this->getFlash()->shouldReturn('something_went_wrong_error'); 33 | } 34 | 35 | function it_has_an_api_response_code(): void 36 | { 37 | $this->getApiResponseCode()->shouldReturn(400); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2021 Paweł Jędrzejewski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Translation/Provider/ImmutableTranslationLocaleProvider.php: -------------------------------------------------------------------------------- 1 | definedLocalesCodes = $definedLocalesCodes; 26 | $this->defaultLocaleCode = $defaultLocaleCode; 27 | } 28 | 29 | public function getDefinedLocalesCodes(): array 30 | { 31 | return $this->definedLocalesCodes; 32 | } 33 | 34 | public function getDefaultLocaleCode(): string 35 | { 36 | return $this->defaultLocaleCode; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Model/TranslatableInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function getTranslations(): Collection; 25 | 26 | public function getTranslation(?string $locale = null): TranslationInterface; 27 | 28 | public function hasTranslation(TranslationInterface $translation): bool; 29 | 30 | public function addTranslation(TranslationInterface $translation): void; 31 | 32 | public function removeTranslation(TranslationInterface $translation): void; 33 | 34 | public function setCurrentLocale(string $locale): void; 35 | 36 | public function setFallbackLocale(string $locale): void; 37 | } 38 | -------------------------------------------------------------------------------- /spec/Exception/RaceConditionExceptionSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(UpdateHandlingException::class); 24 | } 25 | 26 | function it_has_a_message(): void 27 | { 28 | $this->getMessage()->shouldReturn('Operated entity was previously modified.'); 29 | } 30 | 31 | function it_has_a_flash(): void 32 | { 33 | $this->getFlash()->shouldReturn('race_condition_error'); 34 | } 35 | 36 | function it_has_an_api_response_code(): void 37 | { 38 | $this->getApiResponseCode()->shouldReturn(409); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Exception/DeleteHandlingException.php: -------------------------------------------------------------------------------- 1 | flash = $flash; 32 | $this->apiResponseCode = $apiResponseCode; 33 | } 34 | 35 | public function getFlash(): string 36 | { 37 | return $this->flash; 38 | } 39 | 40 | public function getApiResponseCode(): int 41 | { 42 | return $this->apiResponseCode; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Exception/UpdateHandlingException.php: -------------------------------------------------------------------------------- 1 | flash = $flash; 32 | $this->apiResponseCode = $apiResponseCode; 33 | } 34 | 35 | public function getFlash(): string 36 | { 37 | return $this->flash; 38 | } 39 | 40 | public function getApiResponseCode(): int 41 | { 42 | return $this->apiResponseCode; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spec/Translation/Provider/ImmutableTranslationLocaleProviderSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(['pl_PL', 'en_US'], 'pl_PL'); 24 | } 25 | 26 | function it_implements_translation_locale_provider_interface(): void 27 | { 28 | $this->shouldImplement(TranslationLocaleProviderInterface::class); 29 | } 30 | 31 | function it_returns_defined_locales_codes(): void 32 | { 33 | $this->getDefinedLocalesCodes()->shouldReturn(['pl_PL', 'en_US']); 34 | } 35 | 36 | function it_returns_the_default_locale_code(): void 37 | { 38 | $this->getDefaultLocaleCode()->shouldReturn('pl_PL'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Translation/TranslatableEntityLocaleAssigner.php: -------------------------------------------------------------------------------- 1 | translationLocaleProvider = $translationLocaleProvider; 26 | } 27 | 28 | public function assignLocale(TranslatableInterface $translatableEntity): void 29 | { 30 | $localeCode = $this->translationLocaleProvider->getDefaultLocaleCode(); 31 | 32 | $translatableEntity->setCurrentLocale($localeCode); 33 | $translatableEntity->setFallbackLocale($localeCode); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /StateMachine/StateMachine.php: -------------------------------------------------------------------------------- 1 | getPossibleTransitions() as $transition) { 23 | $config = $this->config['transitions'][$transition]; 24 | if (in_array($fromState, $config['from'], true)) { 25 | return $transition; 26 | } 27 | } 28 | 29 | return null; 30 | } 31 | 32 | public function getTransitionToState(string $toState): ?string 33 | { 34 | foreach ($this->getPossibleTransitions() as $transition) { 35 | $config = $this->config['transitions'][$transition]; 36 | if ($toState === $config['to']) { 37 | return $transition; 38 | } 39 | } 40 | 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Metadata/MetadataInterface.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 29 | $this->localeProvider = $localeProvider; 30 | } 31 | 32 | /** 33 | * @throws UnexpectedTypeException 34 | */ 35 | public function createNew() 36 | { 37 | $resource = $this->factory->createNew(); 38 | 39 | if (!$resource instanceof TranslatableInterface) { 40 | throw new UnexpectedTypeException($resource, TranslatableInterface::class); 41 | } 42 | 43 | $resource->setCurrentLocale($this->localeProvider->getDefaultLocaleCode()); 44 | $resource->setFallbackLocale($this->localeProvider->getDefaultLocaleCode()); 45 | 46 | return $resource; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Model/AbstractTranslation.php: -------------------------------------------------------------------------------- 1 | translatable; 27 | 28 | // Return typehint should account for null value. 29 | Assert::notNull($translatable); 30 | 31 | return $translatable; 32 | } 33 | 34 | public function setTranslatable(?TranslatableInterface $translatable): void 35 | { 36 | if ($translatable === $this->translatable) { 37 | return; 38 | } 39 | 40 | $previousTranslatable = $this->translatable; 41 | $this->translatable = $translatable; 42 | 43 | if (null !== $previousTranslatable) { 44 | $previousTranslatable->removeTranslation($this); 45 | } 46 | 47 | if (null !== $translatable) { 48 | $translatable->addTranslation($this); 49 | } 50 | } 51 | 52 | public function getLocale(): ?string 53 | { 54 | return $this->locale; 55 | } 56 | 57 | public function setLocale(?string $locale): void 58 | { 59 | $this->locale = $locale; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /spec/Translation/TranslatableEntityLocaleAssignerSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($translationLocaleProvider); 26 | } 27 | 28 | function it_implements_traslatable_entity_locale_assigner_interface(): void 29 | { 30 | $this->shouldImplement(TranslatableEntityLocaleAssignerInterface::class); 31 | } 32 | 33 | function it_should_assign_current_and_default_locale_to_given_translatable_entity( 34 | TranslationLocaleProviderInterface $translationLocaleProvider, 35 | TranslatableInterface $translatableEntity 36 | ): void { 37 | $translationLocaleProvider->getDefaultLocaleCode()->willReturn('en_US'); 38 | 39 | $translatableEntity->setCurrentLocale('en_US')->shouldBeCalled(); 40 | $translatableEntity->setFallbackLocale('en_US')->shouldBeCalled(); 41 | 42 | $this->assignLocale($translatableEntity); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Metadata/Registry.php: -------------------------------------------------------------------------------- 1 | metadata; 24 | } 25 | 26 | public function get(string $alias): MetadataInterface 27 | { 28 | if (!array_key_exists($alias, $this->metadata)) { 29 | throw new \InvalidArgumentException(sprintf('Resource "%s" does not exist.', $alias)); 30 | } 31 | 32 | return $this->metadata[$alias]; 33 | } 34 | 35 | public function getByClass(string $className): MetadataInterface 36 | { 37 | foreach ($this->metadata as $metadata) { 38 | if ($className === $metadata->getClass('model')) { 39 | return $metadata; 40 | } 41 | } 42 | 43 | throw new \InvalidArgumentException(sprintf('Resource with model class "%s" does not exist.', $className)); 44 | } 45 | 46 | public function add(MetadataInterface $metadata): void 47 | { 48 | $this->metadata[$metadata->getAlias()] = $metadata; 49 | } 50 | 51 | public function addFromAliasAndConfiguration(string $alias, array $configuration): void 52 | { 53 | $this->add(Metadata::fromAliasAndConfiguration($alias, $configuration)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Generator/RandomnessGenerator.php: -------------------------------------------------------------------------------- 1 | digits = implode(range(0, 9)); 25 | 26 | $this->uriSafeAlphabet = 27 | implode(range(0, 9)) 28 | . implode(range('a', 'z')) 29 | . implode(range('A', 'Z')) 30 | . implode(['-', '_', '~']) 31 | ; 32 | } 33 | 34 | public function generateUriSafeString(int $length): string 35 | { 36 | return $this->generateStringOfLength($length, $this->uriSafeAlphabet); 37 | } 38 | 39 | public function generateNumeric(int $length): string 40 | { 41 | return $this->generateStringOfLength($length, $this->digits); 42 | } 43 | 44 | public function generateInt(int $min, int $max): int 45 | { 46 | return random_int($min, $max); 47 | } 48 | 49 | private function generateStringOfLength(int $length, string $alphabet): string 50 | { 51 | $alphabetMaxIndex = strlen($alphabet) - 1; 52 | $randomString = ''; 53 | 54 | for ($i = 0; $i < $length; ++$i) { 55 | $index = random_int(0, $alphabetMaxIndex); 56 | $randomString .= $alphabet[$index]; 57 | } 58 | 59 | return $randomString; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sylius/resource", 3 | "type": "library", 4 | "description": "Basic resource interfaces for PHP applications.", 5 | "keywords": [ 6 | "shop", 7 | "ecommerce", 8 | "resource", 9 | "api", 10 | "sylius", 11 | "doctrine" 12 | ], 13 | "homepage": "http://sylius.com", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Paweł Jędrzejewski", 18 | "homepage": "http://pjedrzejewski.com" 19 | }, 20 | { 21 | "name": "Sylius project", 22 | "homepage": "http://sylius.com" 23 | }, 24 | { 25 | "name": "Community contributions", 26 | "homepage": "http://github.com/Sylius/Sylius/contributors" 27 | } 28 | ], 29 | "require": { 30 | "php": "^8.0", 31 | "doctrine/collections": "^1.6", 32 | "doctrine/inflector": "^1.4 || ^2.0", 33 | "gedmo/doctrine-extensions": "^2.4.12 || ^3.0", 34 | "pagerfanta/core": "^3.0", 35 | "symfony/event-dispatcher": "^4.4 || ^5.4", 36 | "symfony/property-access": "^4.4 || ^5.4", 37 | "winzou/state-machine": "^0.4" 38 | }, 39 | "require-dev": { 40 | "behat/transliterator": "^1.3", 41 | "phpspec/phpspec": "^7.2", 42 | "phpunit/phpunit": "^9.5" 43 | }, 44 | "extra": { 45 | "branch-alias": { 46 | "dev-master": "1.9-dev" 47 | } 48 | }, 49 | "autoload": { 50 | "psr-4": { 51 | "Sylius\\Component\\Resource\\": "" 52 | } 53 | }, 54 | "autoload-dev": { 55 | "psr-4": { 56 | "Sylius\\Component\\Resource\\spec\\": "spec/", 57 | "Sylius\\Component\\Resource\\Tests\\": "Tests" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Resource Component 2 | ================== 3 | 4 | Resource management layer for Sylius and PHP apps. 5 | 6 | Sylius 7 | ------ 8 | 9 |

10 | 11 | 12 | 13 | 14 | Sylius Logo. 15 | 16 | 17 |

18 | 19 | Sylius is an Open Source eCommerce solution built from decoupled components with powerful API and the highest quality code. [Read more on sylius.com](http://sylius.com). 20 | 21 | Documentation 22 | ------------- 23 | 24 | Documentation is available on [**docs.sylius.com**](http://docs.sylius.com/en/latest/components_and_bundles/components/Resource/index.html). 25 | 26 | Contributing 27 | ------------ 28 | 29 | [This page](http://docs.sylius.com/en/latest/contributing/index.html) contains all the information about contributing to Sylius. 30 | 31 | Follow Sylius' Development 32 | -------------------------- 33 | 34 | If you want to keep up with the updates and latest features, follow us on the following channels: 35 | 36 | * [Official Blog](https://sylius.com/blog) 37 | * [Sylius on Twitter](https://twitter.com/Sylius) 38 | * [Sylius on Facebook](https://facebook.com/SyliusEcommerce) 39 | 40 | Bug tracking 41 | ------------ 42 | 43 | Sylius uses [GitHub issues](https://github.com/Sylius/Sylius/issues). 44 | If you have found bug, please create an issue. 45 | 46 | MIT License 47 | ----------- 48 | 49 | License can be found [here](https://github.com/Sylius/Sylius/blob/master/LICENSE). 50 | 51 | Authors 52 | ------- 53 | 54 | The component was originally created by [Paweł Jędrzejewski](http://pjedrzejewski.com). 55 | See the list of [contributors](https://github.com/Sylius/Resource/contributors). 56 | -------------------------------------------------------------------------------- /spec/Generator/RandomnessGeneratorSpec.php: -------------------------------------------------------------------------------- 1 | shouldImplement(RandomnessGeneratorInterface::class); 24 | } 25 | 26 | function it_generates_random_uri_safe_string_of_length(): void 27 | { 28 | $length = 9; 29 | 30 | $this->generateUriSafeString($length)->shouldBeString(); 31 | $this->generateUriSafeString($length)->shouldHaveLength($length); 32 | } 33 | 34 | function it_generates_random_numeric_string_of_length(): void 35 | { 36 | $length = 12; 37 | 38 | $this->generateNumeric($length)->shouldBeString(); 39 | $this->generateNumeric($length)->shouldBeNumeric(); 40 | $this->generateNumeric($length)->shouldHaveLength($length); 41 | } 42 | 43 | function it_generates_random_int_in_range(): void 44 | { 45 | $min = 12; 46 | $max = 2000000; 47 | 48 | $this->generateInt($min, $max)->shouldBeInt(); 49 | $this->generateInt($min, $max)->shouldBeInRange($min, $max); 50 | } 51 | 52 | public function getMatchers(): array 53 | { 54 | return [ 55 | 'haveLength' => function ($subject, $length) { 56 | return $length === strlen($subject); 57 | }, 58 | 'beInRange' => function ($subject, $min, $max) { 59 | return $subject >= $min && $subject <= $max; 60 | }, 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/Metadata/MetadataTest.php: -------------------------------------------------------------------------------- 1 | withPluralRules(new Ruleset( 41 | new Transformations(), 42 | new Patterns(), 43 | new Substitutions(new Substitution(new Word('taxon'), new Word('taxons'))) 44 | )); 45 | $inflector = $factory->build(); 46 | 47 | Metadata::setInflector($inflector); 48 | 49 | $metadata = Metadata::fromAliasAndConfiguration('app.taxon', ['driver' => 'whatever']); 50 | 51 | Assert::assertSame('taxons', $metadata->getPluralName()); 52 | } 53 | 54 | /** @test */ 55 | public function it_uses_a_default_inflector(): void 56 | { 57 | $metadata = Metadata::fromAliasAndConfiguration('app.taxon', ['driver' => 'whatever']); 58 | 59 | Assert::assertSame('taxa', $metadata->getPluralName()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /spec/Model/AbstractTranslationSpec.php: -------------------------------------------------------------------------------- 1 | beAnInstanceOf('spec\Sylius\Component\Resource\Model\ConcreteTranslation'); 27 | } 28 | 29 | function it_is_a_translation(): void 30 | { 31 | $this->shouldImplement(TranslationInterface::class); 32 | } 33 | 34 | function its_translatable_is_mutable(TranslatableInterface $translatable): void 35 | { 36 | $this->setTranslatable($translatable); 37 | $this->getTranslatable()->shouldReturn($translatable); 38 | } 39 | 40 | function its_detaches_from_its_translatable_correctly( 41 | TranslatableInterface $translatable1, 42 | TranslatableInterface $translatable2 43 | ): void { 44 | $translatable1->addTranslation(Argument::type(AbstractTranslation::class)); 45 | $this->setTranslatable($translatable1); 46 | 47 | $translatable1->removeTranslation(Argument::type(AbstractTranslation::class)); 48 | $translatable2->addTranslation(Argument::type(AbstractTranslation::class)); 49 | $this->setTranslatable($translatable2); 50 | } 51 | 52 | function its_locale_is_mutable(): void 53 | { 54 | $this->setLocale('en'); 55 | $this->getLocale()->shouldReturn('en'); 56 | } 57 | } 58 | 59 | class ConcreteTranslation extends AbstractTranslation 60 | { 61 | } 62 | -------------------------------------------------------------------------------- /Reflection/ClassReflection.php: -------------------------------------------------------------------------------- 1 | files()->in($path)->name('*.php')->sortByName(true); 35 | 36 | foreach ($finder as $file) { 37 | $fileContent = (string) file_get_contents((string) $file->getRealPath()); 38 | 39 | preg_match('/namespace (.+);/', $fileContent, $matches); 40 | 41 | $namespace = $matches[1] ?? null; 42 | 43 | if (!preg_match('/class +([^{ ]+)/', $fileContent, $matches)) { 44 | // no class found 45 | continue; 46 | } 47 | 48 | $className = trim($matches[1]); 49 | 50 | if (null !== $namespace) { 51 | yield $namespace . '\\' . $className; 52 | } else { 53 | yield $className; 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * @return \ReflectionAttribute[] 60 | */ 61 | public static function getClassAttributes(string $className, string $attributeName): array 62 | { 63 | $reflectionClass = new \ReflectionClass($className); 64 | 65 | return $reflectionClass->getAttributes($attributeName); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /spec/Factory/TranslatableFactorySpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($factory, $localeProvider); 28 | } 29 | 30 | function it_implements_translatable_factory_interface(): void 31 | { 32 | $this->shouldImplement(TranslatableFactoryInterface::class); 33 | } 34 | 35 | function it_throws_an_exception_if_resource_is_not_translatable(FactoryInterface $factory, \stdClass $resource): void 36 | { 37 | $factory->createNew()->willReturn($resource); 38 | 39 | $this 40 | ->shouldThrow(UnexpectedTypeException::class) 41 | ->during('createNew') 42 | ; 43 | } 44 | 45 | function it_creates_translatable_and_sets_locales( 46 | FactoryInterface $factory, 47 | TranslationLocaleProviderInterface $localeProvider, 48 | TranslatableInterface $resource 49 | ): void { 50 | $localeProvider->getDefaultLocaleCode()->willReturn('pl_PL'); 51 | 52 | $factory->createNew()->willReturn($resource); 53 | 54 | $resource->setCurrentLocale('pl_PL')->shouldBeCalled(); 55 | $resource->setFallbackLocale('pl_PL')->shouldBeCalled(); 56 | 57 | $this->createNew()->shouldReturn($resource); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/Reflection/ClassReflectionTest.php: -------------------------------------------------------------------------------- 1 | assertContains(DummyClassOne::class, $resources); 35 | $this->assertContains(DummyClassTwo::class, $resources); 36 | } 37 | 38 | /** @test */ 39 | public function it_returns_resource_classes_from_a_directory(): void 40 | { 41 | $resources = ClassReflection::getResourcesByPath(__DIR__ . '/../Dummy'); 42 | 43 | $this->assertContains(DummyClassOne::class, $resources); 44 | $this->assertContains(DummyClassTwo::class, $resources); 45 | } 46 | 47 | /** @test */ 48 | public function it_excludes_traits(): void 49 | { 50 | $resources = ClassReflection::getResourcesByPath(__DIR__ . '/../Dummy'); 51 | 52 | $this->assertNotContains(TraitPass::class, $resources); 53 | } 54 | 55 | /** @test */ 56 | public function it_returns_class_attributes(): void 57 | { 58 | $this->assertCount(1, ClassReflection::getClassAttributes(BookWithCriteria::class, SyliusCrudRoutes::class)); 59 | $this->assertCount(1, ClassReflection::getClassAttributes(ShowBook::class, SyliusRoute::class)); 60 | $this->assertCount(2, ClassReflection::getClassAttributes(ShowBookWithPriority::class, SyliusRoute::class)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Annotation/SyliusCrudRoutes.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 67 | $this->path = $path; 68 | $this->identifier = $identifier; 69 | $this->criteria = $criteria; 70 | $this->filterable = $filterable; 71 | $this->form = $form; 72 | $this->serializationVersion = $serializationVersion; 73 | $this->section = $section; 74 | $this->redirect = $redirect; 75 | $this->templates = $templates; 76 | $this->grid = $grid; 77 | $this->permission = $permission; 78 | $this->except = $except; 79 | $this->only = $only; 80 | $this->vars = $vars; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Annotation/SyliusRoute.php: -------------------------------------------------------------------------------- 1 | name = $name; 70 | $this->path = $path; 71 | $this->methods = $methods; 72 | $this->controller = $controller; 73 | $this->template = $template; 74 | $this->repository = $repository; 75 | $this->criteria = $criteria; 76 | $this->serializationGroups = $serializationGroups; 77 | $this->serializationVersion = $serializationVersion; 78 | $this->requirements = $requirements; 79 | $this->options = $options; 80 | $this->host = $host; 81 | $this->schemes = $schemes; 82 | $this->vars = $vars; 83 | $this->priority = $priority; 84 | $this->form = $form; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /spec/Metadata/RegistrySpec.php: -------------------------------------------------------------------------------- 1 | shouldImplement(RegistryInterface::class); 25 | } 26 | 27 | function it_returns_all_resources_metadata(MetadataInterface $metadata1, MetadataInterface $metadata2): void 28 | { 29 | $metadata1->getAlias()->willReturn('app.product'); 30 | $metadata2->getAlias()->willReturn('app.order'); 31 | 32 | $this->add($metadata1); 33 | $this->add($metadata2); 34 | 35 | $this->getAll()->shouldReturn(['app.product' => $metadata1, 'app.order' => $metadata2]); 36 | } 37 | 38 | function it_throws_an_exception_if_resource_is_not_registered(): void 39 | { 40 | $this 41 | ->shouldThrow(\InvalidArgumentException::class) 42 | ->during('get', ['foo.bar']) 43 | ; 44 | } 45 | 46 | function it_returns_specific_metadata(MetadataInterface $metadata): void 47 | { 48 | $metadata->getAlias()->willReturn('app.shipping_method'); 49 | 50 | $this->add($metadata); 51 | 52 | $this->get('app.shipping_method')->shouldReturn($metadata); 53 | } 54 | 55 | function it_throws_an_exception_if_resource_is_not_registered_with_class(): void 56 | { 57 | $this 58 | ->shouldThrow(\InvalidArgumentException::class) 59 | ->during('getByClass', ['App\Model\OrderItem']) 60 | ; 61 | } 62 | 63 | function it_returns_specific_metadata_by_model_class(MetadataInterface $metadata1, MetadataInterface $metadata2): void 64 | { 65 | $metadata1->getAlias()->willReturn('app.product'); 66 | $metadata1->getClass('model')->willReturn('App\Model\Product'); 67 | 68 | $metadata2->getAlias()->willReturn('app.order'); 69 | $metadata2->getClass('model')->willReturn('App\Model\Order'); 70 | 71 | $this->add($metadata1); 72 | $this->add($metadata2); 73 | 74 | $this->getByClass('App\Model\Order')->shouldReturn($metadata2); 75 | } 76 | 77 | function it_adds_metadata_from_configuration_array(): void 78 | { 79 | $this->addFromAliasAndConfiguration('app.product', [ 80 | 'driver' => 'doctrine/orm', 81 | 'classes' => [ 82 | 'model' => 'App\Model\Product', 83 | ], 84 | ]); 85 | 86 | $this->get('app.product')->shouldHaveType(MetadataInterface::class); 87 | $this->getByClass('App\Model\Product')->shouldHaveType(MetadataInterface::class); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Model/TranslatableTrait.php: -------------------------------------------------------------------------------- 1 | translations = new ArrayCollection(); 47 | } 48 | 49 | public function getTranslation(?string $locale = null): TranslationInterface 50 | { 51 | $locale = $locale ?: $this->currentLocale; 52 | if (null === $locale) { 53 | throw new \RuntimeException('No locale has been set and current locale is undefined.'); 54 | } 55 | 56 | if (isset($this->translationsCache[$locale])) { 57 | return $this->translationsCache[$locale]; 58 | } 59 | 60 | $translation = $this->translations->get($locale); 61 | if (null !== $translation) { 62 | $this->translationsCache[$locale] = $translation; 63 | 64 | return $translation; 65 | } 66 | 67 | if ($locale !== $this->fallbackLocale) { 68 | if (isset($this->translationsCache[$this->fallbackLocale])) { 69 | return $this->translationsCache[$this->fallbackLocale]; 70 | } 71 | 72 | $fallbackTranslation = $this->translations->get($this->fallbackLocale); 73 | if (null !== $fallbackTranslation) { 74 | $this->translationsCache[$this->fallbackLocale] = $fallbackTranslation; 75 | 76 | return $fallbackTranslation; 77 | } 78 | } 79 | 80 | $translation = $this->createTranslation(); 81 | $translation->setLocale($locale); 82 | 83 | $this->addTranslation($translation); 84 | 85 | $this->translationsCache[$locale] = $translation; 86 | 87 | return $translation; 88 | } 89 | 90 | /** 91 | * @return Collection|TranslationInterface[] 92 | */ 93 | public function getTranslations(): Collection 94 | { 95 | return $this->translations; 96 | } 97 | 98 | public function hasTranslation(TranslationInterface $translation): bool 99 | { 100 | return isset($this->translationsCache[$translation->getLocale()]) || $this->translations->containsKey($translation->getLocale()); 101 | } 102 | 103 | public function addTranslation(TranslationInterface $translation): void 104 | { 105 | if (!$this->hasTranslation($translation)) { 106 | $this->translationsCache[$translation->getLocale()] = $translation; 107 | 108 | $this->translations->set($translation->getLocale(), $translation); 109 | $translation->setTranslatable($this); 110 | } 111 | } 112 | 113 | public function removeTranslation(TranslationInterface $translation): void 114 | { 115 | if ($this->translations->removeElement($translation)) { 116 | unset($this->translationsCache[$translation->getLocale()]); 117 | 118 | $translation->setTranslatable(null); 119 | } 120 | } 121 | 122 | public function setCurrentLocale(string $currentLocale): void 123 | { 124 | $this->currentLocale = $currentLocale; 125 | } 126 | 127 | public function setFallbackLocale(string $fallbackLocale): void 128 | { 129 | $this->fallbackLocale = $fallbackLocale; 130 | } 131 | 132 | /** 133 | * Create resource translation model. 134 | */ 135 | abstract protected function createTranslation(): TranslationInterface; 136 | } 137 | -------------------------------------------------------------------------------- /spec/Metadata/MetadataSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedThrough('fromAliasAndConfiguration', [ 24 | 'app.product', 25 | [ 26 | 'driver' => 'doctrine/orm', 27 | 'templates' => 'AppBundle:Resource', 28 | 'classes' => [ 29 | 'model' => 'AppBundle\Model\Resource', 30 | 'form' => [ 31 | 'default' => 'AppBundle\Form\Type\ResourceType', 32 | 'choice' => 'AppBundle\Form\Type\ResourceChoiceType', 33 | 'autocomplete' => 'AppBundle\Type\ResourceAutocompleteType', 34 | ], 35 | ], 36 | ], 37 | ]); 38 | } 39 | 40 | function it_implements_metadata_interface(): void 41 | { 42 | $this->shouldImplement(MetadataInterface::class); 43 | } 44 | 45 | function it_has_alias(): void 46 | { 47 | $this->getAlias()->shouldReturn('app.product'); 48 | } 49 | 50 | function it_has_application_name(): void 51 | { 52 | $this->getApplicationName()->shouldReturn('app'); 53 | } 54 | 55 | function it_has_resource_name(): void 56 | { 57 | $this->getName()->shouldReturn('product'); 58 | } 59 | 60 | function it_humanizes_simple_names(): void 61 | { 62 | $this->getHumanizedName()->shouldReturn('product'); 63 | } 64 | 65 | function it_humanizes_more_complex_names(): void 66 | { 67 | $this->beConstructedThrough('fromAliasAndConfiguration', ['app.product_option', ['driver' => 'doctrine/orm']]); 68 | 69 | $this->getHumanizedName()->shouldReturn('product option'); 70 | } 71 | 72 | function it_has_plural_resource_name(): void 73 | { 74 | $this->getPluralName()->shouldReturn('products'); 75 | } 76 | 77 | function it_has_driver(): void 78 | { 79 | $this->getDriver()->shouldReturn('doctrine/orm'); 80 | } 81 | 82 | function it_has_templates_namespace(): void 83 | { 84 | $this->getTemplatesNamespace()->shouldReturn('AppBundle:Resource'); 85 | } 86 | 87 | function it_has_access_to_specific_config_parameter(): void 88 | { 89 | $this->getParameter('driver')->shouldReturn('doctrine/orm'); 90 | } 91 | 92 | function it_checks_if_specific_parameter_exists(): void 93 | { 94 | $this->hasParameter('foo')->shouldReturn(false); 95 | $this->hasParameter('driver')->shouldReturn(true); 96 | } 97 | 98 | function it_throws_an_exception_when_parameter_does_not_exist(): void 99 | { 100 | $this 101 | ->shouldThrow(\InvalidArgumentException::class) 102 | ->during('getParameter', ['foo']) 103 | ; 104 | } 105 | 106 | function it_has_access_to_specific_classes(): void 107 | { 108 | $this->getClass('model')->shouldReturn('AppBundle\Model\Resource'); 109 | } 110 | 111 | function it_throws_an_exception_when_class_does_not_exist(): void 112 | { 113 | $this 114 | ->shouldThrow(\InvalidArgumentException::class) 115 | ->during('getClass', ['foo']) 116 | ; 117 | } 118 | 119 | function it_checks_if_specific_class_exists(): void 120 | { 121 | $this->hasClass('bar')->shouldReturn(false); 122 | $this->hasClass('model')->shouldReturn(true); 123 | } 124 | 125 | function it_generates_service_id(): void 126 | { 127 | $this->getServiceId('factory')->shouldReturn('app.factory.product'); 128 | $this->getServiceId('repository')->shouldReturn('app.repository.product'); 129 | $this->getServiceId('form.type')->shouldReturn('app.form.type.product'); 130 | } 131 | 132 | function it_generates_permission_code(): void 133 | { 134 | $this->getPermissionCode('show')->shouldReturn('app.product.show'); 135 | $this->getPermissionCode('create')->shouldReturn('app.product.create'); 136 | $this->getPermissionCode('custom')->shouldReturn('app.product.custom'); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Metadata/Metadata.php: -------------------------------------------------------------------------------- 1 | name = $name; 39 | $this->applicationName = $applicationName; 40 | 41 | $this->driver = $parameters['driver']; 42 | $this->templatesNamespace = array_key_exists('templates', $parameters) ? $parameters['templates'] : null; 43 | 44 | $this->parameters = $parameters; 45 | } 46 | 47 | public static function fromAliasAndConfiguration(string $alias, array $parameters): self 48 | { 49 | [$applicationName, $name] = self::parseAlias($alias); 50 | 51 | return new self($name, $applicationName, $parameters); 52 | } 53 | 54 | public static function setInflector(InflectorObject $inflector): void 55 | { 56 | self::$inflectorInstance = $inflector; 57 | } 58 | 59 | private static function getInflector(): InflectorObject 60 | { 61 | if (self::$inflectorInstance === null) { 62 | $inflectorFactory = InflectorFactory::create(); 63 | 64 | self::$inflectorInstance = $inflectorFactory->build(); 65 | } 66 | 67 | return self::$inflectorInstance; 68 | } 69 | 70 | public function getAlias(): string 71 | { 72 | return $this->applicationName . '.' . $this->name; 73 | } 74 | 75 | public function getApplicationName(): string 76 | { 77 | return $this->applicationName; 78 | } 79 | 80 | public function getName(): string 81 | { 82 | return $this->name; 83 | } 84 | 85 | public function getHumanizedName(): string 86 | { 87 | return strtolower(trim((string) preg_replace(['/([A-Z])/', '/[_\s]+/'], ['_$1', ' '], $this->name))); 88 | } 89 | 90 | public function getPluralName(): string 91 | { 92 | return self::getInflector()->pluralize($this->name); 93 | } 94 | 95 | public function getDriver(): string 96 | { 97 | return $this->driver; 98 | } 99 | 100 | public function getTemplatesNamespace(): ?string 101 | { 102 | return $this->templatesNamespace; 103 | } 104 | 105 | public function getParameter(string $name) 106 | { 107 | if (!$this->hasParameter($name)) { 108 | throw new \InvalidArgumentException(sprintf('Parameter "%s" is not configured for resource "%s".', $name, $this->getAlias())); 109 | } 110 | 111 | return $this->parameters[$name]; 112 | } 113 | 114 | public function hasParameter(string $name): bool 115 | { 116 | return array_key_exists($name, $this->parameters); 117 | } 118 | 119 | public function getParameters(): array 120 | { 121 | return $this->parameters; 122 | } 123 | 124 | public function getClass(string $name): string 125 | { 126 | if (!$this->hasClass($name)) { 127 | throw new \InvalidArgumentException(sprintf('Class "%s" is not configured for resource "%s".', $name, $this->getAlias())); 128 | } 129 | 130 | return $this->parameters['classes'][$name]; 131 | } 132 | 133 | public function hasClass(string $name): bool 134 | { 135 | return isset($this->parameters['classes'][$name]); 136 | } 137 | 138 | public function getServiceId(string $serviceName): string 139 | { 140 | return sprintf('%s.%s.%s', $this->applicationName, $serviceName, $this->name); 141 | } 142 | 143 | public function getPermissionCode(string $permissionName): string 144 | { 145 | return sprintf('%s.%s.%s', $this->applicationName, $this->name, $permissionName); 146 | } 147 | 148 | private static function parseAlias(string $alias): array 149 | { 150 | if (false === strpos($alias, '.')) { 151 | throw new \InvalidArgumentException(sprintf('Invalid alias "%s" supplied, it should conform to the following format ".".', $alias)); 152 | } 153 | 154 | return explode('.', $alias); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Repository/InMemoryRepository.php: -------------------------------------------------------------------------------- 1 | interface = $interface; 50 | $this->accessor = PropertyAccess::createPropertyAccessor(); 51 | $this->arrayObject = new ArrayObject(); 52 | } 53 | 54 | /** 55 | * @throws ExistingResourceException 56 | * @throws UnexpectedTypeException 57 | */ 58 | public function add(ResourceInterface $resource): void 59 | { 60 | if (!$resource instanceof $this->interface) { 61 | throw new UnexpectedTypeException($resource, $this->interface); 62 | } 63 | 64 | if (in_array($resource, $this->findAll(), true)) { 65 | throw new ExistingResourceException(); 66 | } 67 | 68 | $this->arrayObject->append($resource); 69 | } 70 | 71 | public function remove(ResourceInterface $resource): void 72 | { 73 | $newResources = array_filter($this->findAll(), static function ($object) use ($resource) { 74 | return $object !== $resource; 75 | }); 76 | 77 | $this->arrayObject->exchangeArray($newResources); 78 | } 79 | 80 | public function find($id): ?object 81 | { 82 | return $this->findOneBy(['id' => $id]); 83 | } 84 | 85 | public function findAll(): array 86 | { 87 | return $this->arrayObject->getArrayCopy(); 88 | } 89 | 90 | public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array 91 | { 92 | $results = $this->findAll(); 93 | 94 | if (!empty($criteria)) { 95 | $results = $this->applyCriteria($results, $criteria); 96 | } 97 | 98 | if (!empty($orderBy)) { 99 | $results = $this->applyOrder($results, $orderBy); 100 | } 101 | 102 | return array_slice($results, $offset ?? 0, $limit); 103 | } 104 | 105 | /** 106 | * @throws \InvalidArgumentException 107 | */ 108 | public function findOneBy(array $criteria): ?ResourceInterface 109 | { 110 | if (empty($criteria)) { 111 | throw new \InvalidArgumentException('The criteria array needs to be set.'); 112 | } 113 | 114 | $results = $this->applyCriteria($this->findAll(), $criteria); 115 | 116 | /** @var ResourceInterface|false $result */ 117 | $result = reset($results); 118 | if ($result !== false) { 119 | return $result; 120 | } 121 | 122 | return null; 123 | } 124 | 125 | public function getClassName(): string 126 | { 127 | return $this->interface; 128 | } 129 | 130 | public function createPaginator(array $criteria = [], array $sorting = []): iterable 131 | { 132 | $resources = $this->findAll(); 133 | 134 | if (!empty($sorting)) { 135 | $resources = $this->applyOrder($resources, $sorting); 136 | } 137 | 138 | if (!empty($criteria)) { 139 | $resources = $this->applyCriteria($resources, $criteria); 140 | } 141 | 142 | return new Pagerfanta(new ArrayAdapter($resources)); 143 | } 144 | 145 | /** 146 | * @param object[] $resources 147 | * 148 | * @return object[]|array 149 | */ 150 | private function applyCriteria(array $resources, array $criteria): array 151 | { 152 | foreach ($this->arrayObject as $object) { 153 | foreach ($criteria as $criterion => $value) { 154 | if ($value !== $this->accessor->getValue($object, $criterion)) { 155 | $key = array_search($object, $resources); 156 | unset($resources[$key]); 157 | } 158 | } 159 | } 160 | 161 | return $resources; 162 | } 163 | 164 | /** 165 | * @param object[] $resources 166 | * 167 | * @return object[] 168 | */ 169 | private function applyOrder(array $resources, array $orderBy): array 170 | { 171 | $results = $resources; 172 | 173 | $arguments = []; 174 | foreach ($orderBy as $property => $order) { 175 | $sortable = []; 176 | 177 | foreach ($results as $key => $object) { 178 | $sortable[$key] = $this->accessor->getValue($object, $property); 179 | } 180 | 181 | $arguments[] = $sortable; 182 | 183 | if (RepositoryInterface::ORDER_ASCENDING === $order) { 184 | $arguments[] = \SORT_ASC; 185 | } elseif (RepositoryInterface::ORDER_DESCENDING === $order) { 186 | $arguments[] = \SORT_DESC; 187 | } else { 188 | throw new \InvalidArgumentException('Unknown order.'); 189 | } 190 | } 191 | 192 | $arguments[] = &$results; 193 | 194 | /** 195 | * Doing PHP magic, it works this way 196 | * 197 | * @psalm-suppress InvalidPassByReference 198 | * @psalm-suppress PossiblyInvalidArgument 199 | */ 200 | array_multisort(...$arguments); 201 | 202 | return $results; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /spec/Repository/InMemoryRepositorySpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(SampleBookResourceInterface::class); 31 | } 32 | 33 | function it_throws_unexpected_type_exception_when_constructing_without_resource_interface(): void 34 | { 35 | $this->beConstructedWith(\stdClass::class); 36 | 37 | $this->shouldThrow(UnexpectedTypeException::class)->duringInstantiation(); 38 | } 39 | 40 | function it_implements_repository_interface(): void 41 | { 42 | $this->shouldImplement(RepositoryInterface::class); 43 | } 44 | 45 | function it_throws_invalid_argument_exception_when_adding_wrong_resource_type(ResourceInterface $resource): void 46 | { 47 | $this->shouldThrow(\InvalidArgumentException::class)->during('add', [$resource]); 48 | } 49 | 50 | function it_adds_an_object(SampleBookResourceInterface $monocle): void 51 | { 52 | $monocle->getId()->willReturn(2); 53 | 54 | $this->add($monocle); 55 | $this->findOneBy(['id' => 2])->shouldReturn($monocle); 56 | } 57 | 58 | function it_throws_existing_resource_exception_on_adding_a_resource_which_is_already_in_repository(SampleBookResourceInterface $bike): void 59 | { 60 | $this->add($bike); 61 | $this->shouldThrow(ExistingResourceException::class)->during('add', [$bike]); 62 | } 63 | 64 | function it_removes_a_resource(SampleBookResourceInterface $shirt): void 65 | { 66 | $shirt->getId()->willReturn(5); 67 | 68 | $this->add($shirt); 69 | $this->remove($shirt); 70 | 71 | $this->findOneBy(['id' => 5])->shouldReturn(null); 72 | } 73 | 74 | function it_finds_object_by_id(SampleBookResourceInterface $monocle): void 75 | { 76 | $monocle->getId()->willReturn(2); 77 | 78 | $this->add($monocle); 79 | $this->find(2)->shouldReturn($monocle); 80 | } 81 | 82 | function it_returns_null_if_cannot_find_object_by_id(): void 83 | { 84 | $this->find(2)->shouldReturn(null); 85 | } 86 | 87 | function it_returns_all_objects_when_finding_by_an_empty_parameter_array( 88 | SampleBookResourceInterface $book, 89 | SampleBookResourceInterface $shirt 90 | ): void { 91 | $book->getId()->willReturn(10); 92 | $book->getName()->willReturn('Book'); 93 | 94 | $shirt->getId()->willReturn(5); 95 | $shirt->getName()->willReturn('Shirt'); 96 | 97 | $this->add($book); 98 | $this->add($shirt); 99 | 100 | $this->findBy([])->shouldReturn([$book, $shirt]); 101 | } 102 | 103 | function it_finds_many_objects_by_multiple_criteria_orders_a_limit_and_an_offset( 104 | SampleBookResourceInterface $firstBook, 105 | SampleBookResourceInterface $secondBook, 106 | SampleBookResourceInterface $thirdBook, 107 | SampleBookResourceInterface $fourthBook, 108 | SampleBookResourceInterface $wrongIdBook, 109 | SampleBookResourceInterface $wrongNameBook 110 | ): void { 111 | $id = 80; 112 | $name = 'Book'; 113 | 114 | $firstBook->getId()->willReturn($id); 115 | $secondBook->getId()->willReturn($id); 116 | $thirdBook->getId()->willReturn($id); 117 | $fourthBook->getId()->willReturn($id); 118 | $wrongNameBook->getId()->willReturn($id); 119 | $wrongIdBook->getId()->willReturn(100); 120 | 121 | $firstBook->getName()->willReturn($name); 122 | $secondBook->getName()->willReturn($name); 123 | $thirdBook->getName()->willReturn($name); 124 | $fourthBook->getName()->willReturn($name); 125 | $wrongIdBook->getName()->willReturn($name); 126 | $wrongNameBook->getName()->willReturn('Tome'); 127 | 128 | $firstBook->getRating()->willReturn(3); 129 | $secondBook->getRating()->willReturn(2); 130 | $thirdBook->getRating()->willReturn(2); 131 | $fourthBook->getRating()->willReturn(4); 132 | 133 | $firstBook->getTitle()->willReturn('World War Z'); 134 | $secondBook->getTitle()->willReturn('World War Z'); 135 | $thirdBook->getTitle()->willReturn('Call of Cthulhu'); 136 | $fourthBook->getTitle()->willReturn('Art of War'); 137 | 138 | $this->add($firstBook); 139 | $this->add($secondBook); 140 | $this->add($thirdBook); 141 | $this->add($fourthBook); 142 | $this->add($wrongIdBook); 143 | $this->add($wrongNameBook); 144 | 145 | $this->findBy( 146 | $criteria = [ 147 | 'name' => $name, 148 | 'id' => $id, 149 | ], 150 | $orderBy = [ 151 | 'rating' => RepositoryInterface::ORDER_ASCENDING, 152 | 'title' => RepositoryInterface::ORDER_DESCENDING, 153 | ], 154 | $limit = 2, 155 | $offset = 1 156 | )->shouldReturn([$thirdBook, $firstBook]); 157 | } 158 | 159 | function it_throws_invalid_argument_exception_when_finding_one_object_with_empty_parameter_array(): void 160 | { 161 | $this->shouldThrow(\InvalidArgumentException::class)->during('findOneBy', [[]]); 162 | } 163 | 164 | function it_finds_one_object_by_parameter(SampleBookResourceInterface $book, SampleBookResourceInterface $shirt): void 165 | { 166 | $book->getName()->willReturn('Book'); 167 | $shirt->getName()->willReturn('Shirt'); 168 | 169 | $this->add($book); 170 | $this->add($shirt); 171 | 172 | $this->findOneBy(['name' => 'Book'])->shouldReturn($book); 173 | } 174 | 175 | function it_returns_first_result_while_finding_one_by_parameters( 176 | SampleBookResourceInterface $book, 177 | SampleBookResourceInterface $secondBook 178 | ): void { 179 | $book->getName()->willReturn('Book'); 180 | $secondBook->getName()->willReturn('Book'); 181 | 182 | $this->add($book); 183 | $this->add($secondBook); 184 | 185 | $this->findOneBy(['name' => 'Book'])->shouldReturn($book); 186 | } 187 | 188 | function it_finds_all_objects_in_memory(SampleBookResourceInterface $book, SampleBookResourceInterface $shirt): void 189 | { 190 | $this->add($book); 191 | $this->add($shirt); 192 | 193 | $this->findAll()->shouldReturn([$book, $shirt]); 194 | } 195 | 196 | function it_return_empty_array_when_memory_is_empty(): void 197 | { 198 | $this->findAll()->shouldReturn([]); 199 | } 200 | 201 | function it_creates_paginator(): void 202 | { 203 | $this->createPaginator()->shouldHaveType(Pagerfanta::class); 204 | } 205 | 206 | function it_returns_stated_class_name(): void 207 | { 208 | $this->getClassName()->shouldReturn(SampleBookResourceInterface::class); 209 | } 210 | } 211 | --------------------------------------------------------------------------------