├── .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 |
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 |
--------------------------------------------------------------------------------