├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── .phpunit.cache └── test-results ├── .styleci.yml ├── LICENSE.md ├── README.md ├── changelog.md ├── composer.json ├── images └── relate_list.png ├── phpunit.xml.dist ├── src ├── Comparisons │ ├── ComparisonResult.php │ └── ListComparator.php ├── Console │ └── Commands │ │ ├── FillRelationshipsCommand.php │ │ └── ListRelationshipsCommand.php ├── EntryRelationship.php ├── Events │ ├── EventStack.php │ ├── UpdatedRelatedEntryEvent.php │ ├── UpdatedRelationshipsEvent.php │ ├── UpdatingRelatedEntryEvent.php │ └── UpdatingRelationshipsEvent.php ├── Listeners │ ├── BaseListener.php │ ├── EntryDeletedListener.php │ ├── EntrySavedListener.php │ ├── EntrySavingListener.php │ ├── TermDeletedListener.php │ ├── TermSavedListener.php │ ├── TermSavingListener.php │ ├── UserDeletedListener.php │ ├── UserSavedListener.php │ └── UserSavingListener.php ├── Processors │ ├── Concerns │ │ ├── GetsFieldValues.php │ │ ├── ProcessesManyToMany.php │ │ ├── ProcessesManyToOne.php │ │ ├── ProcessesOneToMany.php │ │ └── ProcessesOneToOne.php │ ├── FillRelationshipsProcessor.php │ └── RelationshipProcessor.php ├── RelationshipManager.php ├── RelationshipProxy.php ├── ServiceProvider.php └── Support │ └── Facades │ ├── EventStack.php │ └── Relate.php └── tests ├── BaseTestCase.php ├── Factories └── EntryFactory.php ├── ManyToManyDeleteTest.php ├── ManyToManyDisabledDeleteTest.php ├── ManyToManyTest.php ├── ManyToOneDeleteTest.php ├── ManyToOneDisabledDeleteTest.php ├── ManyToOneTest.php ├── OneToManyDeleteTest.php ├── OneToManyDisabledDeleteTest.php ├── OneToManyTest.php ├── OneToOneDeleteTest.php ├── OneToOneDisabledDeleteTest.php ├── OneToOneTest.php ├── PreventSavingStacheItemsToDisk.php ├── RelationshipTestCase.php ├── SetNotationRelationshipsTest.php └── __fixtures__ ├── blueprints ├── articles.yaml ├── authors.yaml ├── books.yaml ├── conferences.yaml ├── employees.yaml ├── positions.yaml ├── sponsors.yaml ├── topics.yaml └── user.yaml ├── config ├── amp.php ├── antlers.php ├── api.php ├── assets.php ├── cp.php ├── editions.php ├── forms.php ├── git.php ├── graphql.php ├── live_preview.php ├── oauth.php ├── protect.php ├── revisions.php ├── routes.php ├── search.php ├── sites.php ├── stache.php ├── static_caching.php ├── system.php └── users.php ├── content ├── assets │ └── .gitkeep ├── collections │ └── .gitkeep ├── globals │ └── .gitkeep ├── structures │ └── .gitkeep └── taxonomies │ └── .gitkeep ├── dev-null └── .gitkeep └── users └── .gitkeep /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JohnathonKoster 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | php-tests: 11 | runs-on: ${{ matrix.os }} 12 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 13 | 14 | strategy: 15 | matrix: 16 | php: [8.2] 17 | laravel: [10.*, 11.*] 18 | stability: [prefer-lowest, prefer-stable] 19 | os: [ubuntu-latest] 20 | include: 21 | - laravel: 10.* 22 | - laravel: 11.* 23 | - os: windows-latest 24 | php: 8.2 25 | laravel: 10.* 26 | stability: prefer-stable 27 | - os: windows-latest 28 | php: 8.2 29 | laravel: 11.* 30 | stability: prefer-stable 31 | 32 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 33 | 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v1 37 | 38 | - name: Setup PHP 39 | uses: shivammathur/setup-php@v2 40 | with: 41 | php-version: ${{ matrix.php }} 42 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 43 | coverage: none 44 | 45 | - name: Set PHP 7.4 Mockery 46 | run: composer require "mockery/mockery >=1.2.3" --no-interaction --no-update 47 | if: matrix.php >= 7.4 && matrix.php <8.0 48 | 49 | - name: Set PHP 8 Mockery 50 | run: composer require "mockery/mockery >=1.3.3" --no-interaction --no-update 51 | if: matrix.php >= 8.0 52 | 53 | - name: Set PHP 8.1 Testbench 54 | run: composer require "orchestra/testbench ^6.22.0" --no-interaction --no-update 55 | if: matrix.laravel == '8.*' && matrix.php >= 8.1 56 | 57 | - name: Install dependencies 58 | run: | 59 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 60 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 61 | 62 | - name: Execute tests 63 | run: vendor/bin/phpunit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | .phpunit.result.cache 5 | .php-cs-fixer.cache 6 | vendor 7 | mix-manifest.json 8 | composer.lock 9 | -------------------------------------------------------------------------------- /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":[],"times":{"Tests\\OneToManyTest::test_one_to_many_term_relationships":0.192,"Tests\\ManyToManyDeleteTest::test_many_to_many_delete":0.161,"Tests\\ManyToManyDeleteTest::test_many_to_many_user_delete":0.103,"Tests\\ManyToManyDeleteTest::test_many_to_many_term_delete":0.171,"Tests\\ManyToManyDisabledDeleteTest::test_many_to_many_deletes_can_be_disabled":0.085,"Tests\\ManyToManyDisabledDeleteTest::test_many_to_many_user_deletes_can_be_disabled":0.073,"Tests\\ManyToManyDisabledDeleteTest::test_many_to_many_term_deletes_can_be_disabled":0.109,"Tests\\ManyToManyTest::test_many_to_many_relationship":0.204,"Tests\\ManyToManyTest::test_many_to_many_user_relationships":0.094,"Tests\\ManyToManyTest::test_many_to_many_term_relationships":0.106,"Tests\\ManyToOneDeleteTest::test_many_to_one_delete":0.169,"Tests\\ManyToOneDeleteTest::test_many_to_one_user_delete":0.146,"Tests\\ManyToOneDeleteTest::test_many_to_one_term_delete":0.112,"Tests\\ManyToOneDisabledDeleteTest::test_many_to_one_delete_disabled":0.118,"Tests\\ManyToOneDisabledDeleteTest::test_many_to_one_user_delete_disabled":0.103,"Tests\\ManyToOneDisabledDeleteTest::test_many_to_one_term_delete_disabled":0.07,"Tests\\ManyToOneTest::test_many_to_one_relationship":0.18,"Tests\\ManyToOneTest::test_many_to_one_user_relationship":0.098,"Tests\\ManyToOneTest::test_many_to_one_term_relationship":0.137,"Tests\\OneToManyDeleteTest::test_one_to_many_delete":0.182,"Tests\\OneToManyDeleteTest::test_one_to_many_user_delete":0.115,"Tests\\OneToManyDeleteTest::test_one_to_many_term_delete":0.132,"Tests\\OneToManyDisabledDeleteTest::test_one_to_many_delete_disabled":0.139,"Tests\\OneToManyDisabledDeleteTest::test_one_to_many_user_delete_disabled":0.076,"Tests\\OneToManyDisabledDeleteTest::test_one_to_many_term_delete_disabled":0.08,"Tests\\OneToManyTest::test_one_to_many_relationships":0.197,"Tests\\OneToManyTest::test_explicit_one_to_many":0.2,"Tests\\OneToManyTest::test_one_to_many_user_relationships":0.18,"Tests\\OneToOneDeleteTest::test_one_to_one_delete":0.07,"Tests\\OneToOneDeleteTest::test_one_to_one_user_delete":0.066,"Tests\\OneToOneDeleteTest::test_one_to_one_term_delete":0.071,"Tests\\OneToOneDisabledDeleteTest::test_one_to_one_delete_disabled":0.047,"Tests\\OneToOneDisabledDeleteTest::test_one_to_one_user_disabled_delete":0.041,"Tests\\OneToOneDisabledDeleteTest::test_one_to_one_term_delete_disabled":0.044,"Tests\\OneToOneTest::test_one_to_one_relationship":0.079,"Tests\\OneToOneTest::test_one_to_one_user_relationship":0.069,"Tests\\OneToOneTest::test_one_to_one_term_relationship":0.072,"Tests\\SetNotationRelationshipsTest::test_it_extracts_collection_names":0.01,"Tests\\SetNotationRelationshipsTest::test_it_extracts_collection_names_without_sets":0.011}} -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | risky: false 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v2.0.0 4 | 5 | - Statamic 4 Support 🎉 6 | 7 | ## v1.2.0 8 | 9 | - Adds support for "term" relationships 10 | - Adds support for collection name "set" notation 11 | 12 | ## v1.1.2 13 | 14 | - Restores functionality of the `relate:fill` command 15 | 16 | ## v1.1.1 17 | 18 | - Adds default values for entity types, to improve backwards compatibility 19 | 20 | ## v1.10 21 | 22 | - Adds support for creating user relationships -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stillat/relationships", 3 | "type": "statamic-addon", 4 | "description": "Provides bi-directional entry relationship automation", 5 | "keywords": [ 6 | "statamic", 7 | "entries" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "John Koster", 13 | "email": "john@stillat.com", 14 | "homepage": "https://stillat.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "statamic/cms": "^4.16 || ^5" 21 | }, 22 | "require-dev": { 23 | "mockery/mockery": "^1.2.3", 24 | "orchestra/testbench": "^7.0 || ^8.0 || ^9" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Stillat\\Relationships\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Tests\\": "tests" 34 | } 35 | }, 36 | "extra": { 37 | "statamic": { 38 | "name": "Relationships", 39 | "description": "Relationships addon" 40 | }, 41 | "laravel": { 42 | "providers": [ 43 | "Stillat\\Relationships\\ServiceProvider" 44 | ] 45 | } 46 | }, 47 | "config": { 48 | "allow-plugins": { 49 | "pixelfear/composer-dist-plugin": true 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /images/relate_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stillat/relationships/af96e50e0b618ff1d8eca6562662409e9d99b2f6/images/relate_list.png -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Comparisons/ComparisonResult.php: -------------------------------------------------------------------------------- 1 | same); 16 | } 17 | 18 | public function getAddedCount() 19 | { 20 | return count($this->added); 21 | } 22 | 23 | public function getRemovedCount() 24 | { 25 | return count($this->removed); 26 | } 27 | 28 | public function hasChanges() 29 | { 30 | return count($this->added) > 0 || count($this->removed) > 0; 31 | } 32 | 33 | public function getEffectedIds() 34 | { 35 | return array_unique(array_merge($this->added, $this->removed)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Comparisons/ListComparator.php: -------------------------------------------------------------------------------- 1 | added = array_values(array_diff($b, $a)); 43 | $results->same = array_values(array_intersect($a, $b)); 44 | $results->removed = array_values(array_diff($a, $b)); 45 | 46 | return $results; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Console/Commands/FillRelationshipsCommand.php: -------------------------------------------------------------------------------- 1 | processor = $processor; 32 | } 33 | 34 | private function setupNormalVerbosity() 35 | { 36 | $this->info('Updating relationships. This may take a while if you have a lot of entries.'); 37 | $this->newLine(); 38 | 39 | $loggedMessages = []; 40 | 41 | Event::listen(UpdatingRelationshipsEvent::class, function (UpdatingRelationshipsEvent $event) use (&$loggedMessages) { 42 | $message = "Updating relationship: {$event->relationship->getDescription()}..."; 43 | 44 | if (! in_array($message, $loggedMessages)) { 45 | $this->info($message); 46 | $loggedMessages[] = $message; 47 | } 48 | }); 49 | 50 | Event::listen(UpdatedRelationshipsEvent::class, function (UpdatedRelationshipsEvent $event) use (&$loggedMessages) { 51 | $message = "Updating relationship: {$event->relationship->getDescription()}... done!"; 52 | 53 | if (! in_array($message, $loggedMessages)) { 54 | $this->info($message); 55 | $loggedMessages[] = $message; 56 | } 57 | }); 58 | } 59 | 60 | private function setupVerboseLogging() 61 | { 62 | $this->info('Updating relationships. This may take a while if you have a lot of entries.'); 63 | $this->newLine(); 64 | 65 | Event::listen(UpdatingRelationshipsEvent::class, function (UpdatingRelationshipsEvent $event) { 66 | $this->info("Updating relationship: {$event->relationship->getDescription()}..."); 67 | $this->line(" Added: {$event->results->getAddedCount()} Removed: {$event->results->getRemovedCount()} Same: {$event->results->getSameCount()}"); 68 | }); 69 | 70 | Event::listen(UpdatedRelationshipsEvent::class, function (UpdatedRelationshipsEvent $event) use (&$loggedMessages) { 71 | $this->info("Updating relationship: {$event->relationship->getDescription()}... done!"); 72 | }); 73 | } 74 | 75 | public function handle() 76 | { 77 | $verbosity = $this->getOutput()->getVerbosity(); 78 | 79 | if ($verbosity == OutputInterface::VERBOSITY_NORMAL) { 80 | $this->setupNormalVerbosity(); 81 | } elseif ($verbosity > OutputInterface::VERBOSITY_NORMAL) { 82 | $this->setupVerboseLogging(); 83 | } 84 | 85 | if ($verbosity == OutputInterface::VERBOSITY_DEBUG) { 86 | Event::listen(UpdatingRelatedEntryEvent::class, function (UpdatingRelatedEntryEvent $event) { 87 | $this->line(" Updating entry: {$event->updatedEntry->id()}..."); 88 | }); 89 | Event::listen(UpdatedRelatedEntryEvent::class, function (UpdatedRelatedEntryEvent $event) { 90 | $this->line(" Updating entry: {$event->updatedEntry->id()}... done!"); 91 | }); 92 | } elseif ($verbosity >= OutputInterface::VERBOSITY_VERY_VERBOSE) { 93 | Event::listen(UpdatingRelatedEntryEvent::class, function (UpdatingRelatedEntryEvent $event) { 94 | $this->line(" Updating entry: {$event->updatedEntry->id()}"); 95 | }); 96 | } 97 | 98 | $this->processor->manager()->processor()->setIsDryRun($this->option('dry')); 99 | 100 | $collection = $this->argument('collection'); 101 | 102 | if ($collection != null && is_string($collection)) { 103 | $this->processor->fillCollection($collection); 104 | 105 | return; 106 | } 107 | 108 | $this->processor->fillAll(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Console/Commands/ListRelationshipsCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 22 | } 23 | 24 | public function handle() 25 | { 26 | $relationships = $this->manager->getAllRelationships(); 27 | 28 | $collection = $this->argument('collection'); 29 | 30 | if ($collection != null && is_string($collection)) { 31 | $relationships = $this->manager->getRelationshipsForCollection($collection); 32 | } 33 | 34 | $list = []; 35 | 36 | foreach ($relationships as $relationship) { 37 | $list[] = [ 38 | 'index' => $relationship->index, 39 | 'left_collection' => '['.$relationship->leftType.'].'.$relationship->leftCollection, 40 | 'left_field' => '['.$relationship->leftType.'].'.$relationship->leftField, 41 | 'right_collection' => '['.$relationship->rightType.'].'.$relationship->rightCollection, 42 | 'right_field' => $relationship->rightField, 43 | 'type_description' => $relationship->getRelationshipDescription(), 44 | 'with_events' => $relationship->withEvents ? 'Yes' : 'No', 45 | 'allow_delete' => $relationship->allowDelete ? 'Yes' : 'No', 46 | 'automatic_inverse' => $relationship->isAutomaticInverse ? "Yes ({$relationship->inverseIndex})" : 'No', 47 | ]; 48 | } 49 | 50 | $this->table([ 51 | '', 52 | 'Primary Collection', 53 | 'Related Collection', 54 | 'Primary Field', 55 | 'Related Field', 56 | 'Relationship', 57 | 'With Events?', 58 | 'Allow Deletes?', 59 | 'Is Automatic Inverse?', 60 | ], $list); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/EntryRelationship.php: -------------------------------------------------------------------------------- 1 | index = self::$relationshipCount; 51 | } 52 | 53 | public function collection($collection) 54 | { 55 | $this->leftCollection = $collection; 56 | } 57 | 58 | public function field($handle, $entityType = 'entry') 59 | { 60 | $this->leftField = $handle; 61 | $this->leftType = $entityType; 62 | 63 | return $this; 64 | } 65 | 66 | public function isRelatedTo($rightCollectionHandle) 67 | { 68 | $this->rightCollection = $rightCollectionHandle; 69 | 70 | return $this; 71 | } 72 | 73 | public function through($rightCollectionFieldHandle, $entityType = 'entry') 74 | { 75 | $this->rightField = $rightCollectionFieldHandle; 76 | $this->rightType = $entityType; 77 | 78 | return $this; 79 | } 80 | 81 | public function manyToMany() 82 | { 83 | $this->type = self::TYPE_MANY_TO_MANY; 84 | 85 | return $this; 86 | } 87 | 88 | public function oneToOne() 89 | { 90 | $this->type = self::TYPE_ONE_TO_ONE; 91 | 92 | return $this; 93 | } 94 | 95 | public function oneToMany() 96 | { 97 | $this->type = self::TYPE_ONE_TO_MANY; 98 | 99 | return $this; 100 | } 101 | 102 | public function manyToOne() 103 | { 104 | $this->type = self::TYPE_MANY_TO_ONE; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Sets whether affected entries will be saved quietly. 111 | * 112 | * @param bool $withEvents Whether to trigger events. 113 | * @return $this 114 | */ 115 | public function withEvents($withEvents = true) 116 | { 117 | $this->withEvents = $withEvents; 118 | 119 | return $this; 120 | } 121 | 122 | public function withOriginRelationship(EntryRelationship $relationship) 123 | { 124 | $this->origin = $relationship; 125 | $relationship->inverted = $this; 126 | 127 | return $this; 128 | } 129 | 130 | public function isAutomaticInverse($isInverse = true, $inverseIndex = null) 131 | { 132 | $this->isAutomaticInverse = $isInverse; 133 | 134 | if ($inverseIndex != null) { 135 | $this->inverseIndex = $inverseIndex; 136 | } else { 137 | $this->inverseIndex = $this->index - 1; 138 | } 139 | 140 | return $this; 141 | } 142 | 143 | public function getInverse() 144 | { 145 | if ($this->isAutomaticInverse) { 146 | return $this->origin; 147 | } 148 | 149 | return $this->inverted; 150 | } 151 | 152 | /** 153 | * Sets whether affected entries will be updated when deleting related entries. 154 | * 155 | * @param bool $allowDelete Whether to allow deletes. 156 | * @return $this 157 | */ 158 | public function allowDeletes($allowDelete = true) 159 | { 160 | $this->allowDelete = $allowDelete; 161 | 162 | return $this; 163 | } 164 | 165 | public function getRelationshipDescription() 166 | { 167 | if ($this->type == EntryRelationship::TYPE_MANY_TO_ONE) { 168 | return 'Many to One (*-1)'; 169 | } elseif ($this->type == EntryRelationship::TYPE_ONE_TO_MANY) { 170 | return 'One to Many (1-*)'; 171 | } elseif ($this->type == EntryRelationship::TYPE_MANY_TO_MANY) { 172 | return 'Many to Many (*-*)'; 173 | } elseif ($this->type == EntryRelationship::TYPE_ONE_TO_ONE) { 174 | return 'One to One (1-1)'; 175 | } 176 | 177 | return ''; 178 | } 179 | 180 | public function getSymbolDescription() 181 | { 182 | if ($this->type == EntryRelationship::TYPE_MANY_TO_ONE) { 183 | return '(*-1)'; 184 | } elseif ($this->type == EntryRelationship::TYPE_ONE_TO_MANY) { 185 | return '(1-*)'; 186 | } elseif ($this->type == EntryRelationship::TYPE_MANY_TO_MANY) { 187 | return '(*-*)'; 188 | } elseif ($this->type == EntryRelationship::TYPE_ONE_TO_ONE) { 189 | return '(1-1)'; 190 | } 191 | 192 | return ''; 193 | } 194 | 195 | public function getDescription() 196 | { 197 | return "{$this->getSymbolDescription()} [{$this->leftCollection}.{$this->leftField} {$this->rightCollection}.{$this->rightField}]"; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Events/EventStack.php: -------------------------------------------------------------------------------- 1 | count++; 12 | } 13 | 14 | public function decrement() 15 | { 16 | $this->count--; 17 | } 18 | 19 | public function count() 20 | { 21 | return $this->count; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Events/UpdatedRelatedEntryEvent.php: -------------------------------------------------------------------------------- 1 | updatedEntry = $updatedEntry; 29 | $this->relatedEntry = $relatedEntry; 30 | $this->relationship = $relationship; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Events/UpdatedRelationshipsEvent.php: -------------------------------------------------------------------------------- 1 | relationship = $relationship; 24 | $this->results = $results; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Events/UpdatingRelatedEntryEvent.php: -------------------------------------------------------------------------------- 1 | updatedEntry = $updatedEntry; 29 | $this->relatedEntry = $relatedEntry; 30 | $this->relationship = $relationship; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Events/UpdatingRelationshipsEvent.php: -------------------------------------------------------------------------------- 1 | relationship = $relationship; 24 | $this->results = $results; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/BaseListener.php: -------------------------------------------------------------------------------- 1 | model() ?: $object; 14 | } 15 | 16 | return clone $object; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Listeners/EntryDeletedListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 19 | } 20 | 21 | public function handle(EntryDeleted $event) 22 | { 23 | /** @var Entry $entry */ 24 | $entry = $event->entry; 25 | $collection = $entry->collectionHandle(); 26 | 27 | if (! $this->manager->hasRelationshipsForCollection($collection)) { 28 | return; 29 | } 30 | 31 | $relationships = $this->manager->getRelationshipsForCollection($collection); 32 | 33 | $this->manager->processor()->setIsDeleting()->setEntryId($entry->id()) 34 | ->setPristineDetails($entry, false)->process($relationships); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Listeners/EntrySavedListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 20 | } 21 | 22 | public function handle(EntrySaved $event) 23 | { 24 | EventStack::decrement(); 25 | 26 | /** @var Entry $entry */ 27 | $entry = $event->entry; 28 | $collection = $entry->collection(); 29 | 30 | if ($collection == null) { 31 | return; 32 | } 33 | 34 | if (EventStack::count() > 0 || $this->manager->processor()->isProcessingRelationships()) { 35 | return; 36 | } 37 | 38 | $this->manager->processor()->setUpdatedEntryDetails($entry); 39 | 40 | $handle = $collection->handle(); 41 | $relationships = $this->manager->getRelationshipsForCollection($handle); 42 | 43 | $this->manager->processor()->process($relationships); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Listeners/EntrySavingListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 26 | $this->entries = $entries; 27 | } 28 | 29 | public function handle(EntrySaving $event) 30 | { 31 | EventStack::increment(); 32 | 33 | /** @var Entry $entry */ 34 | $entry = $event->entry; 35 | $collection = $entry->collectionHandle(); 36 | 37 | if (! $this->manager->hasRelationshipsForCollection($collection)) { 38 | return; 39 | } 40 | 41 | if (EventStack::count() > 1 || $this->manager->processor()->isProcessingRelationships()) { 42 | return; 43 | } 44 | 45 | $isUpdating = $entry->id() !== null; 46 | 47 | if ($isUpdating) { 48 | $foundEntry = $this->entries->find($entry->id()); 49 | 50 | if ($foundEntry === null) { 51 | $isUpdating = false; 52 | } else { 53 | $entry = clone $foundEntry; 54 | $isUpdating = true; 55 | } 56 | } 57 | 58 | $entry = $this->checkForDatabaseObject($entry); 59 | 60 | $this->manager->processor()->setIsDeleting(false)->setPristineDetails($entry, ! $isUpdating); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Listeners/TermDeletedListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 19 | } 20 | 21 | public function handle(TermDeleted $event) 22 | { 23 | /** @var Term $term */ 24 | $term = $event->term; 25 | 26 | if (! $this->manager->hasTermRelationships()) { 27 | return; 28 | } 29 | 30 | $relationships = $this->manager->getTermRelationshipsFor($term->taxonomy()->handle()); 31 | 32 | $this->manager->processor()->setIsDeleting()->setEntryId($term->slug()) 33 | ->setPristineDetails($term, false)->process($relationships); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Listeners/TermSavedListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 17 | } 18 | 19 | public function handle(TermSaved $event) 20 | { 21 | /** @var Term $term */ 22 | $term = $event->term; 23 | 24 | if (! $this->manager->hasTermRelationships()) { 25 | return; 26 | } 27 | 28 | $this->manager->processor()->setUpdatedEntryDetails($term); 29 | 30 | $relationships = $this->manager->getTermRelationshipsFor($term->taxonomy()->handle()); 31 | 32 | $this->manager->processor()->process($relationships); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Listeners/TermSavingListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 18 | } 19 | 20 | public function handle(TermSaving $event) 21 | { 22 | /** @var Term $term */ 23 | $term = $event->term; 24 | 25 | if (! $this->manager->hasTermRelationships()) { 26 | return; 27 | } 28 | 29 | $isUpdating = $term->id() !== null; 30 | 31 | if ($isUpdating) { 32 | $foundTerm = TermFacade::find($event->term->id()); 33 | 34 | if ($foundTerm === null) { 35 | $isUpdating = false; 36 | } else { 37 | $term = clone $foundTerm; 38 | $isUpdating = true; 39 | } 40 | } 41 | 42 | $term = $this->checkForDatabaseObject($term); 43 | 44 | $this->manager->processor()->setIsDeleting(false) 45 | ->setPristineDetails($term, ! $isUpdating); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Listeners/UserDeletedListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 16 | } 17 | 18 | public function handle(UserDeleted $event) 19 | { 20 | /** @var User $user */ 21 | $user = $event->user; 22 | 23 | if (! $this->manager->hasUserRelationships()) { 24 | return; 25 | } 26 | 27 | $relationships = $this->manager->getAllUserRelationships(); 28 | 29 | $this->manager->processor()->setIsDeleting()->setEntryId($user->id()) 30 | ->setPristineDetails($user, false)->process($relationships); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Listeners/UserSavedListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 19 | } 20 | 21 | public function handle(UserSaved $event) 22 | { 23 | /** @var User $user */ 24 | $user = $event->user; 25 | 26 | if (! $this->manager->hasUserRelationships()) { 27 | return; 28 | } 29 | 30 | $this->manager->processor()->setUpdatedEntryDetails($user); 31 | 32 | $relationships = $this->manager->getAllUserRelationships(); 33 | 34 | $this->manager->processor()->process($relationships); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Listeners/UserSavingListener.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 20 | } 21 | 22 | public function handle(UserSaving $event) 23 | { 24 | /** @var User $user */ 25 | $user = $event->user; 26 | 27 | if (! $this->manager->hasUserRelationships()) { 28 | return; 29 | } 30 | 31 | $isUpdating = $user->id() !== null; 32 | 33 | if ($isUpdating) { 34 | $foundUser = UserFacade::find($event->user->id()); 35 | 36 | if ($foundUser === null) { 37 | $isUpdating = false; 38 | } else { 39 | $user = clone $foundUser; 40 | $isUpdating = true; 41 | } 42 | } 43 | 44 | $user = $this->checkForDatabaseObject($user); 45 | 46 | $this->manager->processor()->setIsDeleting(false) 47 | ->setPristineDetails($user, ! $isUpdating); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Processors/Concerns/GetsFieldValues.php: -------------------------------------------------------------------------------- 1 | model(); 16 | } 17 | 18 | if ($entry instanceof Model) { 19 | return $entry[$fieldName] ?? $default; 20 | } 21 | 22 | return $this->getFieldValue($fieldName, $entry, $default); 23 | } 24 | 25 | /** 26 | * @param string $fieldName 27 | * @param Entry $entry 28 | * @param mixed|null $default 29 | */ 30 | protected function getFieldValue($fieldName, $entry, $default = null) 31 | { 32 | if (Str::contains($fieldName, '*')) { 33 | $data = data_get($entry->data()->all(), $fieldName, $default); 34 | } else { 35 | $data = $entry->get($fieldName, $default); 36 | } 37 | 38 | if (is_array($data)) { 39 | $data = array_filter($data); 40 | } 41 | 42 | return $data; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Processors/Concerns/ProcessesManyToMany.php: -------------------------------------------------------------------------------- 1 | processingRelationships = true; 13 | 14 | foreach ($results->added as $addedId) { 15 | if (! $this->shouldProcessRelationship($relationship, $addedId)) { 16 | continue; 17 | } 18 | 19 | $this->addItemToEntry($relationship, $this->getEffectedEntity($relationship, $addedId)); 20 | } 21 | 22 | foreach ($results->removed as $removedId) { 23 | if (! $this->shouldProcessRelationship($relationship, $removedId)) { 24 | continue; 25 | } 26 | 27 | $this->removeItemFromEntry($relationship, $this->getEffectedEntity($relationship, $removedId)); 28 | } 29 | 30 | $this->processingRelationships = false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Processors/Concerns/ProcessesManyToOne.php: -------------------------------------------------------------------------------- 1 | removed as $removedId) { 13 | if ($this->shouldProcessRelationship($relationship, $removedId)) { 14 | $this->removeItemFromEntry($relationship, $this->getEffectedEntity($relationship, $removedId)); 15 | } 16 | } 17 | 18 | if (! empty($results->added) && count($results->added) == 1 && $this->shouldProcessRelationship($relationship, $results->added[0])) { 19 | $this->addItemToEntry($relationship, $this->getEffectedEntity($relationship, $results->added[0])); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Processors/Concerns/ProcessesOneToMany.php: -------------------------------------------------------------------------------- 1 | removed as $removedId) { 14 | if ($this->shouldProcessRelationship($relationship, $removedId)) { 15 | $this->dependencies[] = $removedId; 16 | $this->dependencies[] = $this->getDependency($relationship, $removedId); 17 | $this->removeFieldValue($relationship, $this->getEffectedEntity($relationship, $removedId)); 18 | } 19 | } 20 | 21 | foreach ($results->added as $addedId) { 22 | if ($this->shouldProcessRelationship($relationship, $addedId)) { 23 | $this->dependencies[] = $addedId; 24 | $dependent = Data::find($this->getDependency($relationship, $addedId)); 25 | 26 | if ($this->withDependent && $dependent !== null && $inverse = $relationship->getInverse()) { 27 | $leftReference = $dependent->get($relationship->leftField); 28 | 29 | if (($key = array_search($addedId, $leftReference)) !== false) { 30 | unset($leftReference[$key]); 31 | $dependent->set($relationship->leftField, array_values($leftReference)); 32 | 33 | $dependent->saveQuietly(); 34 | } 35 | } 36 | 37 | $this->setFieldValue($relationship, $this->getEffectedEntity($relationship, $addedId)); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Processors/Concerns/ProcessesOneToOne.php: -------------------------------------------------------------------------------- 1 | added) && count($results->added) == 1 && $this->shouldProcessRelationship($relationship, $results->added[0])) { 13 | $this->setFieldValue($relationship, $this->getEffectedEntity($relationship, $results->added[0])); 14 | } 15 | 16 | foreach ($results->removed as $removedId) { 17 | if ($this->shouldProcessRelationship($relationship, $removedId)) { 18 | $this->removeItemFromEntry($relationship, $this->getEffectedEntity($relationship, $removedId)); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Processors/FillRelationshipsProcessor.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 30 | $this->entries = $entries; 31 | } 32 | 33 | /** 34 | * Gets the RelationshipManager instance. 35 | * 36 | * @return RelationshipManager 37 | */ 38 | public function manager() 39 | { 40 | return $this->manager; 41 | } 42 | 43 | public function fillAll() 44 | { 45 | foreach ($this->manager->getAll() as $relationships) { 46 | if ($relationships instanceof EntryRelationship) { 47 | $this->fillRelationship($relationships); 48 | } elseif (is_array($relationships)) { 49 | $this->fillRelationships($relationships); 50 | } 51 | } 52 | } 53 | 54 | public function fillCollection($collection) 55 | { 56 | $this->fillRelationships($this->manager->getRelationshipsForCollection($collection)); 57 | } 58 | 59 | /** 60 | * @param EntryRelationship[] $relationships 61 | * @return void 62 | */ 63 | protected function fillRelationships($relationships) 64 | { 65 | foreach ($relationships as $relationship) { 66 | $this->fillRelationship($relationship); 67 | } 68 | } 69 | 70 | protected function processData($data, EntryRelationship $relationship) 71 | { 72 | foreach ($data as $item) { 73 | $related = $this->getFieldValue($relationship->leftField, $item, null); 74 | 75 | if ($related == null) { 76 | continue; 77 | } 78 | 79 | if (! is_array($related)) { 80 | $related = [$related]; 81 | } 82 | 83 | $mockResults = new ComparisonResult(); 84 | $mockResults->added = $related; 85 | 86 | $fillId = $item->id(); 87 | 88 | if ($item instanceof Term) { 89 | $fillId = $item->slug(); 90 | } 91 | 92 | $this->manager->processor()->withDependent(false)->setEntryId($fillId) 93 | ->processRelationship($relationship, $mockResults); 94 | } 95 | } 96 | 97 | protected function fillTaxonomyRelationship(EntryRelationship $relationship) 98 | { 99 | $terms = Taxonomy::find($relationship->taxonomyName)->queryTerms()->get(); 100 | 101 | if (count($terms) === 0) { 102 | return; 103 | } 104 | 105 | $this->processData($terms, $relationship); 106 | } 107 | 108 | protected function fillRelationship(EntryRelationship $relationship) 109 | { 110 | if ($relationship->leftCollection === '[term]') { 111 | $this->fillTaxonomyRelationship($relationship); 112 | 113 | return; 114 | } 115 | 116 | $collectionEntries = $this->entries->query() 117 | ->whereIn('collection', [$relationship->leftCollection]) 118 | ->where($relationship->leftField, '!=', null) 119 | ->get(); 120 | 121 | if (count($collectionEntries) == 0) { 122 | return; 123 | } 124 | 125 | $this->processData($collectionEntries, $relationship); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Processors/RelationshipProcessor.php: -------------------------------------------------------------------------------- 1 | processingRelationships; 99 | } 100 | 101 | public function setIsDeleting($isDeleting = true) 102 | { 103 | $this->isDelete = $isDeleting; 104 | 105 | return $this; 106 | } 107 | 108 | public function setPristineDetails($entryData, $isNew) 109 | { 110 | if ($isNew) { 111 | return $this->setPristineNewDetails($entryData); 112 | } 113 | 114 | return $this->setPristineUpdateDetails($entryData); 115 | } 116 | 117 | public function setPristineNewDetails($entryData) 118 | { 119 | $this->isNewEntry = true; 120 | $this->pristineEntry = $entryData; 121 | 122 | return $this; 123 | } 124 | 125 | public function withDependent($withDependent = true) { 126 | $this->withDependent = $withDependent; 127 | 128 | return $this; 129 | } 130 | 131 | public function setIsDryRun($isDry = true) 132 | { 133 | $this->isDry = $isDry; 134 | 135 | return $this; 136 | } 137 | 138 | public function setPristineUpdateDetails($entryData) 139 | { 140 | $this->isNewEntry = false; 141 | $this->pristineEntry = $entryData; 142 | 143 | return $this; 144 | } 145 | 146 | public function setEntryId($entryId) 147 | { 148 | $this->entryId = $entryId; 149 | 150 | return $this; 151 | } 152 | 153 | public function setUpdatedEntryDetails($entryData) 154 | { 155 | $this->updatedEntry = $entryData; 156 | 157 | if ($entryData instanceof Term) { 158 | $this->entryId = $this->updatedEntry->slug(); 159 | } else { 160 | $this->entryId = $this->updatedEntry->id(); 161 | } 162 | 163 | return $this; 164 | } 165 | 166 | public function getPristineEntry() 167 | { 168 | return $this->pristineEntry; 169 | } 170 | 171 | public function getUpdatedEntry() 172 | { 173 | return $this->updatedEntry; 174 | } 175 | 176 | /** 177 | * @param Entry $entry 178 | * @param EntryRelationship $relationship 179 | */ 180 | protected function updateEntry($entry, $relationship) 181 | { 182 | UpdatingRelatedEntryEvent::dispatch($entry, $this->pristineEntry, $relationship); 183 | 184 | if (! $this->isDry) { 185 | if ($relationship->withEvents) { 186 | if ($entry instanceof LocalizedTerm) { 187 | $entry->term()->save(); 188 | } else { 189 | $entry->save(); 190 | } 191 | } else { 192 | if ($entry instanceof LocalizedTerm) { 193 | $entry->term()->saveQuietly(); 194 | } else { 195 | $entry->saveQuietly(); 196 | } 197 | } 198 | } 199 | 200 | UpdatedRelatedEntryEvent::dispatch($entry, $this->pristineEntry, $relationship); 201 | } 202 | 203 | protected function getEntryData($relationship) 204 | { 205 | $fieldName = $relationship->leftField; 206 | $pristine = []; 207 | $updated = []; 208 | 209 | if ($this->pristineEntry != null && $this->isNewEntry == false) { 210 | $pristine = $this->getPristineValue($fieldName, $this->pristineEntry, []); 211 | } 212 | 213 | if ($this->isDelete) { 214 | $deletedResults = new ComparisonResult(); 215 | 216 | if (! is_array($pristine)) { 217 | $pristine = [$pristine]; 218 | } 219 | 220 | $deletedResults->removed = $pristine; 221 | 222 | return $deletedResults; 223 | } 224 | 225 | if ($this->updatedEntry != null) { 226 | $updated = $this->getFieldValue($fieldName, $this->updatedEntry, []); 227 | } 228 | 229 | return ListComparator::compare($pristine, $updated); 230 | } 231 | 232 | /** 233 | * @param EntryRelationship $relationship 234 | * @param string[] $entryIds 235 | * @return void 236 | */ 237 | protected function getEffectedEntries($relationship, $entryIds) 238 | { 239 | // The right-hand side of the relationship will indicate what is being updated. 240 | if ($relationship->rightType == 'entry') { 241 | if ($this->entries == null) { 242 | $this->entries = app(EntryRepository::class); 243 | } 244 | 245 | /** @var EntryCollection $entries */ 246 | $entries = $this->entries->query()->whereIn('id', $entryIds)->get(); 247 | 248 | $this->effectedEntries = array_merge( 249 | $this->effectedEntries, 250 | $entries->keyBy('id')->all() 251 | ); 252 | } elseif ($relationship->rightType == 'user') { 253 | $users = $this->getUsersByIds($entryIds); 254 | 255 | $this->effectedUsers = array_merge( 256 | $this->effectedUsers, 257 | $users->keyBy('id')->all() 258 | ); 259 | } elseif ($relationship->rightType == 'term') { 260 | $terms = $this->getTermsByIds($relationship, $entryIds); 261 | 262 | $this->effectedTerms = array_merge( 263 | $this->effectedTerms, 264 | $terms->keyBy(fn($term) => $term->slug())->all() 265 | ); 266 | } 267 | } 268 | 269 | private function getUsersByIds($userIds) 270 | { 271 | $users = []; 272 | 273 | foreach ($userIds as $userId) { 274 | $user = \Statamic\Facades\User::find($userId); 275 | 276 | if ($user != null) { 277 | $users[] = $user; 278 | } 279 | } 280 | 281 | return collect($users); 282 | } 283 | 284 | private function getTermsByIds(EntryRelationship $relationship, $termIds) 285 | { 286 | $terms = []; 287 | 288 | /** @var TermRepository $termsRepository */ 289 | $termsRepository = app(TermRepository::class); 290 | 291 | foreach ($termIds as $termId) { 292 | $term = $termsRepository->whereTaxonomy($relationship->rightCollection)->where('slug', $termId)->first(); 293 | 294 | if ($term != null) { 295 | $terms[] = $term; 296 | } 297 | } 298 | 299 | return collect($terms); 300 | } 301 | 302 | public function process($relationships) 303 | { 304 | if (empty($relationships)) { 305 | return; 306 | } 307 | 308 | /** @var EntryRelationship $relationship */ 309 | foreach ($relationships as $relationship) { 310 | if ($this->isDelete && ! $relationship->allowDelete) { 311 | continue; 312 | } 313 | 314 | $this->processRelationship($relationship, $this->getEntryData($relationship)); 315 | } 316 | } 317 | 318 | public function processRelationship($relationship, $results) 319 | { 320 | $this->processingRelationships = true; 321 | UpdatingRelationshipsEvent::dispatch($relationship, $results); 322 | 323 | if (! $results->hasChanges()) { 324 | $this->processingRelationships = false; 325 | UpdatedRelationshipsEvent::dispatch($relationship, $results); 326 | 327 | return; 328 | } 329 | 330 | $this->getEffectedEntries($relationship, $results->getEffectedIds()); 331 | 332 | if ($relationship->type == EntryRelationship::TYPE_MANY_TO_MANY) { 333 | $this->processManyToMany($results, $relationship); 334 | } elseif ($relationship->type == EntryRelationship::TYPE_ONE_TO_ONE) { 335 | $this->processOneToOne($results, $relationship); 336 | } elseif ($relationship->type == EntryRelationship::TYPE_ONE_TO_MANY) { 337 | $this->processOneToMany($results, $relationship); 338 | } elseif ($relationship->type == EntryRelationship::TYPE_MANY_TO_ONE) { 339 | $this->processManyToOne($results, $relationship); 340 | } 341 | $this->processingRelationships = false; 342 | 343 | UpdatedRelationshipsEvent::dispatch($relationship, $results); 344 | } 345 | 346 | /** 347 | * Determines if the relationship should be processed for the provided entitiy. 348 | * 349 | * @param EntryRelationship $relationship The relationship. 350 | * @param string $id The related entity ID. 351 | * @return bool 352 | */ 353 | protected function shouldProcessRelationship(EntryRelationship $relationship, $id) 354 | { 355 | if ($relationship->rightType == 'entry' && ! array_key_exists($id, $this->effectedEntries)) { 356 | return false; 357 | } 358 | 359 | if ($relationship->rightType == 'user' && ! array_key_exists($id, $this->effectedUsers)) { 360 | return false; 361 | } 362 | 363 | if ($relationship->rightType == 'term' && ! array_key_exists($id, $this->effectedTerms)) { 364 | return false; 365 | } 366 | 367 | return true; 368 | } 369 | 370 | protected function getDependency(EntryRelationship $relationship, $id) 371 | { 372 | $data = null; 373 | 374 | if ($relationship->rightType == 'entry') { 375 | $data = $this->effectedEntries[$id]; 376 | } elseif ($relationship->rightType == 'user') { 377 | $data = $this->effectedUsers[$id]; 378 | } elseif ($relationship->rightType == 'term') { 379 | $data = $this->effectedTerms[$id]; 380 | } 381 | 382 | if ($data === null || ! method_exists($data, 'get')) { 383 | return null; 384 | } 385 | 386 | return $data->get($relationship->rightField); 387 | } 388 | 389 | protected function getEffectedEntity(EntryRelationship $relationship, $id) 390 | { 391 | if ($relationship->rightType == 'entry') { 392 | return $this->effectedEntries[$id]; 393 | } elseif ($relationship->rightType == 'user') { 394 | return $this->effectedUsers[$id]; 395 | } elseif ($relationship->rightType == 'term') { 396 | return $this->effectedTerms[$id]; 397 | } 398 | 399 | return null; 400 | } 401 | 402 | /** 403 | * @param EntryRelationship $relationship 404 | * @param Entry $entry 405 | */ 406 | protected function removeFieldValue($relationship, $entry) 407 | { 408 | if ($relationship->rightType == 'entry') { 409 | if ($entry->collectionHandle() != $relationship->rightCollection) { 410 | return; 411 | } 412 | } 413 | 414 | $rightReference = $entry->get($relationship->rightField, null); 415 | 416 | if ($rightReference != $this->entryId) { 417 | return; 418 | } 419 | 420 | $entry->set($relationship->rightField, null); 421 | 422 | $this->updateEntry($entry, $relationship); 423 | } 424 | 425 | /** 426 | * @param EntryRelationship $relationship 427 | * @param Entry $entry 428 | */ 429 | protected function setFieldValue($relationship, $entry) 430 | { 431 | if ($relationship->rightType == 'entry') { 432 | if ($entry->collectionHandle() != $relationship->rightCollection) { 433 | return; 434 | } 435 | } 436 | 437 | $rightReference = $entry->get($relationship->rightField, null); 438 | 439 | if ($rightReference !== $this->entryId) { 440 | $entry->set($relationship->rightField, $this->entryId); 441 | 442 | $this->updateEntry($entry, $relationship); 443 | } 444 | } 445 | 446 | /** 447 | * @param EntryRelationship $relationship 448 | * @param Entry|User $entry 449 | */ 450 | protected function addItemToEntry($relationship, $entry) 451 | { 452 | if ($relationship->rightType == 'entry') { 453 | if ($entry->collectionHandle() != $relationship->rightCollection) { 454 | return; 455 | } 456 | } 457 | 458 | $rightReference = $entry->get($relationship->rightField, []); 459 | 460 | if ($rightReference == null) { 461 | $rightReference = []; 462 | } 463 | 464 | if (is_string($rightReference) && Str::startsWith($rightReference, '[') && Str::endsWith($rightReference, ']')) { 465 | $rightReference = json_decode($rightReference, true); 466 | } 467 | 468 | if (in_array($this->entryId, $rightReference)) { 469 | return; 470 | } 471 | 472 | $rightReference[] = $this->entryId; 473 | $entry->set($relationship->rightField, array_values($rightReference)); 474 | 475 | $this->updateEntry($entry, $relationship); 476 | } 477 | 478 | /** 479 | * @param EntryRelationship $relationship 480 | * @param Entry $entry 481 | */ 482 | protected function removeItemFromEntry($relationship, $entry) 483 | { 484 | if ($relationship->rightType == 'entry') { 485 | if ($entry->collectionHandle() != $relationship->rightCollection) { 486 | return; 487 | } 488 | } 489 | 490 | $rightReference = $entry->get($relationship->rightField, []); 491 | 492 | if (! is_array($rightReference)) { 493 | $rightReference = [$rightReference]; 494 | } 495 | 496 | if (($key = array_search($this->entryId, $rightReference)) !== false) { 497 | unset($rightReference[$key]); 498 | 499 | $entry->set($relationship->rightField, array_values($rightReference)); 500 | 501 | $this->updateEntry($entry, $relationship); 502 | } else { 503 | $value = null; 504 | 505 | if (is_array($rightReference) && count($rightReference) > 0) { 506 | $value = $rightReference; 507 | } 508 | 509 | $entry->set($relationship->rightField, $value); 510 | $this->updateEntry($entry, $relationship); 511 | } 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /src/RelationshipManager.php: -------------------------------------------------------------------------------- 1 | processor = $processor; 27 | } 28 | 29 | /** 30 | * @return RelationshipProcessor 31 | */ 32 | public function processor() 33 | { 34 | return $this->processor; 35 | } 36 | 37 | /** 38 | * @param string $handle The collection handle. 39 | * @return EntryRelationship 40 | */ 41 | public function collection($handle) 42 | { 43 | $relationship = new EntryRelationship(); 44 | $relationship->collection($handle); 45 | 46 | if (! array_key_exists('entries', $this->relationships)) { 47 | $this->relationships['entries'] = []; 48 | } 49 | 50 | if (! array_key_exists($handle, $this->relationships['entries'])) { 51 | $this->relationships['entries'][$handle] = []; 52 | } 53 | 54 | $this->relationships['entries'][$handle][] = $relationship; 55 | 56 | return $relationship; 57 | } 58 | 59 | public function user($fieldName) 60 | { 61 | $relationship = new EntryRelationship(); 62 | $relationship->leftType = 'user'; 63 | $relationship->leftField = $fieldName; 64 | $relationship->leftCollection = '[user]'; 65 | 66 | if (! array_key_exists('users', $this->relationships)) { 67 | $this->relationships['users'] = []; 68 | } 69 | 70 | $this->relationships['users'][] = $relationship; 71 | 72 | return $relationship; 73 | } 74 | 75 | public function term($termName) 76 | { 77 | $relationship = new EntryRelationship(); 78 | $relationship->leftType = 'term'; 79 | $relationship->leftField = $termName; 80 | $relationship->leftCollection = '[term]'; 81 | $relationship->taxonomyName = $termName; 82 | 83 | if (! array_key_exists('terms', $this->relationships)) { 84 | $this->relationships['terms'] = []; 85 | } 86 | 87 | $this->relationships['terms'][] = $relationship; 88 | 89 | return $relationship; 90 | } 91 | 92 | protected function getRelationshipBuilder($left, $leftType) 93 | { 94 | if ($leftType == 'entry') { 95 | return $this->collection($left); 96 | } elseif ($leftType == 'user') { 97 | return $this->user($left); 98 | } elseif ($leftType == 'term') { 99 | return $this->term($left); 100 | } 101 | } 102 | 103 | private function getRelationship($left, $right) 104 | { 105 | if (! in_array($left[0], $this->validEntityTypes)) { 106 | throw new InvalidArgumentException($left[0].' is not a valid entity type.'); 107 | } 108 | 109 | if (! in_array($right[0], $this->validEntityTypes)) { 110 | throw new InvalidArgumentException($right[0].' is not a valid entity type.'); 111 | } 112 | 113 | return $this->getRelationshipBuilder($left[1], $left[0]) 114 | ->field($left[2], $left[0]) 115 | ->isRelatedTo($right[1]) 116 | ->through($right[2], $right[0]); 117 | } 118 | 119 | private function buildOneToOneRelationships($relationships) 120 | { 121 | $builtRelationships = []; 122 | 123 | foreach ($relationships as $relationship) { 124 | $left = $this->getFieldDetails($relationship[0]); 125 | $right = $this->getFieldDetails($relationship[1]); 126 | 127 | $builtRelationships[] = $this->getRelationship($left, $right)->oneToOne(); 128 | $builtRelationships[] = $this->getRelationship($right, $left) 129 | ->oneToOne() 130 | ->isAutomaticInverse() 131 | ->withOriginRelationship($builtRelationships[0]); 132 | } 133 | 134 | return new RelationshipProxy($builtRelationships); 135 | } 136 | 137 | /** 138 | * @param string $leftCollectionHandle 139 | * @param string $rightCollectionHandle 140 | * @return RelationshipProxy 141 | */ 142 | public function oneToOne($leftCollectionHandle, $rightCollectionHandle) 143 | { 144 | return $this->buildOneToOneRelationships($this->getRelationshipItems($leftCollectionHandle, $rightCollectionHandle)); 145 | } 146 | 147 | private function buildOneToManyRelationships($relationships) 148 | { 149 | $builtRelationships = []; 150 | 151 | foreach ($relationships as $relationship) { 152 | $left = $this->getFieldDetails($relationship[0]); 153 | $right = $this->getFieldDetails($relationship[1]); 154 | 155 | $builtRelationships[] = $this->getRelationship($left, $right)->manyToOne(); 156 | $builtRelationships[] = $this->getRelationship($right, $left) 157 | ->oneToMany() 158 | ->isAutomaticInverse() 159 | ->withOriginRelationship($builtRelationships[0]); 160 | } 161 | 162 | return new RelationshipProxy($builtRelationships); 163 | } 164 | 165 | /** 166 | * @param string $leftCollectionHandle 167 | * @param string $rightCollectionHandle 168 | * @return RelationshipProxy 169 | */ 170 | public function oneToMany($leftCollectionHandle, $rightCollectionHandle) 171 | { 172 | return $this->buildOneToManyRelationships($this->getRelationshipItems($leftCollectionHandle, $rightCollectionHandle)); 173 | } 174 | 175 | private function buildManyToOneRelationships($relationships) 176 | { 177 | $builtRelationships = []; 178 | 179 | foreach ($relationships as $relationship) { 180 | $left = $this->getFieldDetails($relationship[0]); 181 | $right = $this->getFieldDetails($relationship[1]); 182 | 183 | $builtRelationships[] = $this->getRelationship($left, $right)->oneToOne(); 184 | $builtRelationships[] = $this->getRelationship($right, $left) 185 | ->manyToOne() 186 | ->isAutomaticInverse() 187 | ->withOriginRelationship($builtRelationships[0]); 188 | } 189 | 190 | return new RelationshipProxy($builtRelationships); 191 | } 192 | 193 | /** 194 | * @param string $leftCollectionHandle 195 | * @param string $rightCollectionHandle 196 | * @return RelationshipProxy 197 | */ 198 | public function manyToOne($leftCollectionHandle, $rightCollectionHandle) 199 | { 200 | return $this->buildManyToOneRelationships($this->getRelationshipItems($leftCollectionHandle, $rightCollectionHandle)); 201 | } 202 | 203 | /** 204 | * Extracts collection and field information from relationship set notation. 205 | * 206 | * @param string $handles 207 | * @return string[] 208 | */ 209 | public static function extractCollections($handles) 210 | { 211 | if (! Str::contains($handles, '{')) { 212 | return [$handles]; 213 | } 214 | 215 | $parts = explode(':', $handles, 2); 216 | $type = $parts[0]; 217 | $parts = explode('.', $parts[1], 2); 218 | $field = $parts[1]; 219 | 220 | if (ctype_punct(mb_substr($parts[0], 0, 1))) { 221 | $parts[0] = Str::substr($parts[0], 1, -1); 222 | } 223 | 224 | $handles = collect(explode(',', $parts[0])); 225 | 226 | return $handles->map(function ($handle) use ($type, $field) { 227 | return $type.':'.$handle.'.'.$field; 228 | })->all(); 229 | } 230 | 231 | private function getRelationshipItems($leftCollectionHandle, $rightCollectionHandle) 232 | { 233 | $relationships = []; 234 | 235 | if (Str::contains($leftCollectionHandle, '{') || Str::contains($rightCollectionHandle, '{')) { 236 | $left = self::extractCollections($leftCollectionHandle); 237 | $right = self::extractCollections($rightCollectionHandle); 238 | $inverted[] = []; 239 | $relationships = collect(Arr::crossJoin($left, $right))->filter(function ($pair) use (&$inverted) { 240 | $normal = $pair[0].':'.$pair[1]; 241 | 242 | if (in_array($normal, $inverted)) { 243 | return false; 244 | } 245 | 246 | $inverted[] = $pair[1].':'.$pair[0]; 247 | 248 | return true; 249 | })->all(); 250 | } else { 251 | $relationships[] = [$leftCollectionHandle, $rightCollectionHandle]; 252 | } 253 | 254 | return $relationships; 255 | } 256 | 257 | private function buildManyToManyRelationships($relationships) 258 | { 259 | $builtRelationships = []; 260 | 261 | foreach ($relationships as $relationship) { 262 | $left = $this->getFieldDetails($relationship[0]); 263 | $right = $this->getFieldDetails($relationship[1]); 264 | 265 | $builtRelationships[] = $this->getRelationship($left, $right)->manyToMany(); 266 | $builtRelationships[] = $this->getRelationship($right, $left) 267 | ->manyToMany() 268 | ->isAutomaticInverse() 269 | ->withOriginRelationship($builtRelationships[0]); 270 | } 271 | 272 | return new RelationshipProxy($builtRelationships); 273 | } 274 | 275 | /** 276 | * @param string $leftCollectionHandle 277 | * @param string $rightCollectionHandle 278 | * @return RelationshipProxy 279 | */ 280 | public function manyToMany($leftCollectionHandle, $rightCollectionHandle) 281 | { 282 | return $this->buildManyToManyRelationships($this->getRelationshipItems($leftCollectionHandle, $rightCollectionHandle)); 283 | } 284 | 285 | protected function getFieldDetails($handle) 286 | { 287 | $details = explode('.', $handle, 2); 288 | 289 | if (Str::contains($details[0], ':')) { 290 | $typeDetails = array_shift($details); 291 | $additionalDetails = explode(':', $typeDetails, 2); 292 | 293 | if ($additionalDetails[0] == 'user') { 294 | array_unshift($additionalDetails, 'user'); 295 | } 296 | 297 | array_unshift($details, ...$additionalDetails); 298 | } else { 299 | array_unshift($details, 'entry'); 300 | } 301 | 302 | return $details; 303 | } 304 | 305 | /** 306 | * Determines if relationships exist for the specified collection. 307 | * 308 | * @param string $handle The collection handle. 309 | * @return bool 310 | */ 311 | public function hasRelationshipsForCollection($handle) 312 | { 313 | return ! empty($this->getRelationshipsForCollection($handle)); 314 | } 315 | 316 | public function hasUserRelationships() 317 | { 318 | if (! array_key_exists('users', $this->relationships)) { 319 | return false; 320 | } 321 | 322 | return count($this->relationships['users']) > 0; 323 | } 324 | 325 | public function hasTermRelationships() 326 | { 327 | if (! array_key_exists('terms', $this->relationships)) { 328 | return false; 329 | } 330 | 331 | return count($this->relationships['terms']) > 0; 332 | } 333 | 334 | /** 335 | * Gets all relationships for the specified collection. 336 | * 337 | * @param string $handle The collection handle. 338 | * @return EntryRelationship[] 339 | */ 340 | public function getRelationshipsForCollection($handle) 341 | { 342 | if (! array_key_exists('entries', $this->relationships)) { 343 | return []; 344 | } 345 | 346 | if (! array_key_exists($handle, $this->relationships['entries'])) { 347 | return []; 348 | } 349 | 350 | return $this->relationships['entries'][$handle]; 351 | } 352 | 353 | public function getAll() 354 | { 355 | return $this->getAllRelationships(); 356 | } 357 | 358 | /** 359 | * Returns all relationships for the provided entity type. 360 | * 361 | * @param string $entityType The entity type. 362 | * @return array|EntryRelationship 363 | */ 364 | private function getEntityTypeRelationships($entityType) 365 | { 366 | if (! array_key_exists($entityType, $this->relationships)) { 367 | return []; 368 | } 369 | 370 | return $this->relationships[$entityType]; 371 | } 372 | 373 | /** 374 | * Returns all entry relationships. 375 | * 376 | * @return array|EntryRelationship 377 | */ 378 | public function getAllEntryRelationships() 379 | { 380 | return $this->getEntityTypeRelationships('entries'); 381 | } 382 | 383 | /** 384 | * Returns all user relationships. 385 | * 386 | * @return array|EntryRelationship 387 | */ 388 | public function getAllUserRelationships() 389 | { 390 | return $this->getEntityTypeRelationships('users'); 391 | } 392 | 393 | public function getAllTermRelationships() 394 | { 395 | return $this->getEntityTypeRelationships('terms'); 396 | } 397 | 398 | public function getTermRelationshipsFor($term) 399 | { 400 | return collect($this->getAllTermRelationships())->filter(function (EntryRelationship $relationship) use ($term) { 401 | return $relationship->leftType == 'term' && $relationship->taxonomyName == $term; 402 | })->all(); 403 | } 404 | 405 | public function getAllRelationships() 406 | { 407 | $relationships = []; 408 | 409 | foreach ($this->getAllEntryRelationships() as $collectionRelationships) { 410 | $relationships = array_merge($relationships, $collectionRelationships); 411 | } 412 | 413 | foreach ($this->getAllUserRelationships() as $userRelationship) { 414 | $relationships[] = $userRelationship; 415 | } 416 | 417 | foreach ($this->getAllTermRelationships() as $termRelationship) { 418 | $relationships[] = $termRelationship; 419 | } 420 | 421 | return collect($relationships)->sortBy('index')->values()->all(); 422 | } 423 | 424 | /** 425 | * Returns the names of all collections that have relationships. 426 | * 427 | * @return string[] 428 | */ 429 | public function getCollections() 430 | { 431 | return array_keys($this->relationships['entries']); 432 | } 433 | 434 | /** 435 | * Clears all relationships. 436 | * 437 | * @return $this 438 | */ 439 | public function clear() 440 | { 441 | $this->relationships = []; 442 | 443 | return $this; 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/RelationshipProxy.php: -------------------------------------------------------------------------------- 1 | relationships = $relationships; 21 | } 22 | 23 | /** 24 | * Sets whether affected entries will be updated when deleting related entries. 25 | * 26 | * @param bool $allowDelete Whether to allow deletes. 27 | * @return $this 28 | */ 29 | public function allowDelete($allowDelete = true) 30 | { 31 | $this->relationships->each(function (EntryRelationship $relationship) use ($allowDelete) { 32 | $relationship->allowDeletes($allowDelete); 33 | }); 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * Sets whether affected entries will be saved quietly. 40 | * 41 | * @param bool $withEvents Whether to trigger events. 42 | * @return $this 43 | */ 44 | public function withEvents($withEvents = true) 45 | { 46 | $this->relationships->each(function (EntryRelationship $relationship) use ($withEvents) { 47 | $relationship->withEvents($withEvents); 48 | }); 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 33 | EntrySavingListener::class, 34 | ], 35 | EntrySaved::class => [ 36 | EntrySavedListener::class, 37 | ], 38 | EntryDeleted::class => [ 39 | EntryDeletedListener::class, 40 | ], 41 | 42 | UserSaving::class => [ 43 | UserSavingListener::class, 44 | ], 45 | UserSaved::class => [ 46 | UserSavedListener::class, 47 | ], 48 | UserDeleted::class => [ 49 | UserDeletedListener::class, 50 | ], 51 | TermSaving::class => [ 52 | TermSavingListener::class, 53 | ], 54 | TermSaved::class => [ 55 | TermSavedListener::class, 56 | ], 57 | TermDeleted::class => [ 58 | TermDeletedListener::class, 59 | ], 60 | ]; 61 | 62 | protected $commands = [ 63 | FillRelationshipsCommand::class, 64 | ListRelationshipsCommand::class, 65 | ]; 66 | 67 | public function register() 68 | { 69 | $this->app->singleton(EventStack::class, function ($app) { 70 | return new EventStack(); 71 | }); 72 | 73 | $this->app->singleton(RelationshipManager::class, function ($app) { 74 | return new RelationshipManager($app->make(RelationshipProcessor::class)); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Support/Facades/EventStack.php: -------------------------------------------------------------------------------- 1 | preventSavingStacheItemsToDisk(); 17 | } 18 | 19 | if ($this->shouldFakeVersion) { 20 | \Facades\Statamic\Version::shouldReceive('get')->andReturn('3.0.0-testing'); 21 | $this->addToAssertionCount(-1); // Dont want to assert this 22 | } 23 | 24 | // Boot our Addon's events so those work :) 25 | $provider = collect(app()->getProviders(\Stillat\Relationships\ServiceProvider::class))->first(); 26 | $provider->bootEvents(); 27 | } 28 | 29 | public function tearDown(): void 30 | { 31 | $uses = array_flip(class_uses_recursive(static::class)); 32 | 33 | if (isset($uses[PreventSavingStacheItemsToDisk::class])) { 34 | $this->deleteFakeStacheDirectory(); 35 | } 36 | 37 | parent::tearDown(); 38 | } 39 | 40 | protected function getPackageProviders($app) 41 | { 42 | return [ 43 | \Statamic\Providers\StatamicServiceProvider::class, 44 | \Wilderborn\Partyline\ServiceProvider::class, 45 | \Archetype\ServiceProvider::class, 46 | \Stillat\Relationships\ServiceProvider::class, 47 | ]; 48 | } 49 | 50 | protected function getPackageAliases($app) 51 | { 52 | return ['Statamic' => 'Statamic\Statamic']; 53 | } 54 | 55 | protected function resolveApplicationConfiguration($app) 56 | { 57 | parent::resolveApplicationConfiguration($app); 58 | 59 | $configs = [ 60 | 'assets', 'cp', 'forms', 'routes', 'static_caching', 61 | 'sites', 'stache', 'system', 'users', 62 | ]; 63 | 64 | foreach ($configs as $config) { 65 | $app['config']->set("statamic.$config", require (__DIR__."/__fixtures__/config/{$config}.php")); 66 | } 67 | 68 | $app['config']->set('statamic.antlers.version', 'runtime'); 69 | } 70 | 71 | protected function getEnvironmentSetUp($app) 72 | { 73 | // We changed the default sites setup but the tests assume defaults like the following. 74 | $app['config']->set('statamic.sites', [ 75 | 'default' => 'en', 76 | 'sites' => [ 77 | 'default' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://localhost/'], 78 | ], 79 | ]); 80 | $app['config']->set('auth.providers.users.driver', 'statamic'); 81 | $app['config']->set('statamic.stache.watcher', true); 82 | $app['config']->set('statamic.users.repository', 'file'); 83 | $app['config']->set('statamic.stache.stores.users', [ 84 | 'class' => \Statamic\Stache\Stores\UsersStore::class, 85 | 'directory' => __DIR__.'/__fixtures__/users', 86 | ]); 87 | 88 | $app['config']->set('statamic.stache.stores.taxonomies.directory', __DIR__.'/__fixtures__/content/taxonomies'); 89 | $app['config']->set('statamic.stache.stores.terms.directory', __DIR__.'/__fixtures__/content/taxonomies'); 90 | $app['config']->set('statamic.stache.stores.collections.directory', __DIR__.'/__fixtures__/content/collections'); 91 | $app['config']->set('statamic.stache.stores.entries.directory', __DIR__.'/__fixtures__/content/collections'); 92 | $app['config']->set('statamic.stache.stores.navigation.directory', __DIR__.'/__fixtures__/content/navigation'); 93 | $app['config']->set('statamic.stache.stores.globals.directory', __DIR__.'/__fixtures__/content/globals'); 94 | $app['config']->set('statamic.stache.stores.global-variables.directory', __DIR__.'/__fixtures__/content/globals'); 95 | $app['config']->set('statamic.stache.stores.asset-containers.directory', __DIR__.'/__fixtures__/content/assets'); 96 | $app['config']->set('statamic.stache.stores.nav-trees.directory', __DIR__.'/__fixtures__/content/structures/navigation'); 97 | $app['config']->set('statamic.stache.stores.collection-trees.directory', __DIR__.'/__fixtures__/content/structures/collections'); 98 | 99 | $app['config']->set('statamic.api.enabled', true); 100 | $app['config']->set('statamic.graphql.enabled', true); 101 | $app['config']->set('statamic.editions.pro', true); 102 | 103 | $app['config']->set('cache.stores.array.driver', 'null'); 104 | $app['config']->set('cache.stores.outpost', [ 105 | 'driver' => 'file', 106 | 'path' => storage_path('framework/cache/outpost-data'), 107 | ]); 108 | 109 | $viewPaths = $app['config']->get('view.paths'); 110 | $viewPaths[] = __DIR__.'/__fixtures__/views/'; 111 | 112 | $app['config']->set('view.paths', $viewPaths); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Factories/EntryFactory.php: -------------------------------------------------------------------------------- 1 | reset(); 32 | } 33 | 34 | public function id($id) 35 | { 36 | $this->id = $id; 37 | 38 | return $this; 39 | } 40 | 41 | public function slug($slug) 42 | { 43 | $this->slug = $slug; 44 | 45 | return $this; 46 | } 47 | 48 | public function collection($collection) 49 | { 50 | $this->collection = $collection; 51 | 52 | return $this; 53 | } 54 | 55 | public function data($data) 56 | { 57 | $this->data = $data; 58 | 59 | return $this; 60 | } 61 | 62 | public function published($published) 63 | { 64 | $this->published = $published; 65 | 66 | return $this; 67 | } 68 | 69 | public function locale($locale) 70 | { 71 | $this->locale = $locale; 72 | 73 | return $this; 74 | } 75 | 76 | public function origin($origin) 77 | { 78 | $this->origin = $origin; 79 | 80 | return $this; 81 | } 82 | 83 | public function make() 84 | { 85 | $entry = Entry::make() 86 | ->locale($this->locale) 87 | ->collection($this->createCollection()) 88 | ->slug($this->slug) 89 | ->data($this->data) 90 | ->origin($this->origin) 91 | ->published($this->published); 92 | 93 | if ($this->id) { 94 | $entry->id($this->id); 95 | } 96 | 97 | $this->reset(); 98 | 99 | return $entry; 100 | } 101 | 102 | public function create() 103 | { 104 | return tap($this->make())->save(); 105 | } 106 | 107 | protected function createCollection() 108 | { 109 | if ($this->collection instanceof StatamicCollection) { 110 | return $this->collection; 111 | } 112 | 113 | return Collection::findByHandle($this->collection) 114 | ?? Collection::make($this->collection) 115 | ->sites(['default']) 116 | ->save(); 117 | } 118 | 119 | private function reset() 120 | { 121 | 122 | $this->id = null; 123 | $this->slug = null; 124 | $this->data = []; 125 | $this->published = true; 126 | $this->order = null; 127 | $this->locale = 'default'; 128 | $this->origin = null; 129 | $this->collection = null; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/ManyToManyDeleteTest.php: -------------------------------------------------------------------------------- 1 | manyToMany('conferences.sponsors', 'sponsors.sponsoring'); 15 | 16 | Entry::find('sponsors-1')->set('sponsoring', [ 17 | 'conferences-1', 18 | 'conferences-2', 19 | ])->save(); 20 | 21 | Entry::find('sponsors-2')->set('sponsoring', [ 22 | 'conferences-2', 23 | ])->save(); 24 | 25 | Entry::find('conferences-1')->delete(); 26 | 27 | $this->assertSame(['conferences-2'], Entry::find('sponsors-1')->get('sponsoring')); 28 | $this->assertSame(['conferences-2'], Entry::find('sponsors-2')->get('sponsoring')); 29 | 30 | Entry::find('conferences-2')->delete(); 31 | 32 | $this->assertSame([], Entry::find('sponsors-1')->get('sponsoring', [])); 33 | $this->assertSame([], Entry::find('sponsors-2')->get('sponsoring', [])); 34 | } 35 | 36 | public function test_many_to_many_user_delete() 37 | { 38 | Relate::clear() 39 | ->manyToMany('conferences.conference_users', 'user:user_conferences'); 40 | 41 | User::find('user-1')->set('user_conferences', [ 42 | 'conferences-1', 43 | 'conferences-2', 44 | ])->save(); 45 | 46 | User::find('user-2')->set('user_conferences', [ 47 | 'conferences-1', 48 | ])->save(); 49 | 50 | Entry::find('conferences-1')->delete(); 51 | 52 | $this->assertSame(['user-1'], Entry::find('conferences-2')->get('conference_users', [])); 53 | $this->assertSame(['conferences-2'], User::find('user-1')->get('user_conferences', [])); 54 | $this->assertSame([], User::find('user-2')->get('user_conferences', [])); 55 | } 56 | 57 | public function test_many_to_many_term_delete() 58 | { 59 | Relate::clear() 60 | ->manyToMany('term:topics.posts', 'entry:articles.topics'); 61 | 62 | Entry::find('articles-1')->set('topics', [ 63 | 'topics-one', 64 | 'topics-two', 65 | ])->save(); 66 | 67 | Entry::find('articles-2')->set('topics', [ 68 | 'topics-one', 69 | ])->save(); 70 | 71 | Entry::find('articles-3')->set('topics', [ 72 | 'topics-two', 73 | ])->save(); 74 | 75 | $this->getTerm('topics-one')->delete(); 76 | $this->getTerm('topics-two')->delete(); 77 | 78 | $this->assertSame([], Entry::find('articles-1')->get('topics', [])); 79 | $this->assertSame([], Entry::find('articles-2')->get('topics', [])); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/ManyToManyDisabledDeleteTest.php: -------------------------------------------------------------------------------- 1 | manyToMany('conferences.sponsors', 'sponsors.sponsoring') 15 | ->allowDelete(false); 16 | 17 | Entry::find('sponsors-1')->set('sponsoring', [ 18 | 'conferences-1', 19 | 'conferences-2', 20 | ])->save(); 21 | 22 | Entry::find('sponsors-2')->set('sponsoring', [ 23 | 'conferences-2', 24 | ])->save(); 25 | 26 | Entry::find('conferences-1')->delete(); 27 | 28 | $this->assertSame(['conferences-1', 'conferences-2'], Entry::find('sponsors-1')->get('sponsoring')); 29 | $this->assertSame(['conferences-2'], Entry::find('sponsors-2')->get('sponsoring')); 30 | 31 | Entry::find('conferences-2')->delete(); 32 | 33 | $this->assertSame(['conferences-1', 'conferences-2'], Entry::find('sponsors-1')->get('sponsoring', [])); 34 | $this->assertSame(['conferences-2'], Entry::find('sponsors-2')->get('sponsoring', [])); 35 | } 36 | 37 | public function test_many_to_many_user_deletes_can_be_disabled() 38 | { 39 | Relate::clear() 40 | ->manyToMany('conferences.conference_users', 'user:user_conferences') 41 | ->allowDelete(false); 42 | 43 | User::find('user-1')->set('user_conferences', [ 44 | 'conferences-1', 45 | 'conferences-2', 46 | ])->save(); 47 | 48 | User::find('user-2')->set('user_conferences', [ 49 | 'conferences-1', 50 | ])->save(); 51 | 52 | Entry::find('conferences-1')->delete(); 53 | 54 | $this->assertSame(['user-1'], Entry::find('conferences-2')->get('conference_users', [])); 55 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('user_conferences', [])); 56 | $this->assertSame(['conferences-1'], User::find('user-2')->get('user_conferences', [])); 57 | } 58 | 59 | public function test_many_to_many_term_deletes_can_be_disabled() 60 | { 61 | Relate::clear() 62 | ->manyToMany('term:topics.posts', 'entry:articles.topics') 63 | ->allowDelete(false); 64 | 65 | Entry::find('articles-1')->set('topics', [ 66 | 'topics-one', 67 | 'topics-two', 68 | ])->save(); 69 | 70 | Entry::find('articles-2')->set('topics', [ 71 | 'topics-one', 72 | ])->save(); 73 | 74 | Entry::find('articles-3')->set('topics', [ 75 | 'topics-two', 76 | ])->save(); 77 | 78 | $this->getTerm('topics-one')->delete(); 79 | $this->getTerm('topics-two')->delete(); 80 | 81 | $this->assertSame(['topics-one', 'topics-two'], Entry::find('articles-1')->get('topics', [])); 82 | $this->assertSame(['topics-one'], Entry::find('articles-2')->get('topics', [])); 83 | $this->assertSame(['topics-two'], Entry::find('articles-3')->get('topics', [])); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/ManyToManyTest.php: -------------------------------------------------------------------------------- 1 | manyToMany('conferences.sponsors', 'sponsors.sponsoring'); 15 | 16 | Entry::find('sponsors-1')->set('sponsoring', [ 17 | 'conferences-1', 18 | 'conferences-2', 19 | ])->save(); 20 | 21 | Entry::find('sponsors-2')->set('sponsoring', [ 22 | 'conferences-2', 23 | ])->save(); 24 | 25 | $this->assertSame(['sponsors-1', 'sponsors-2'], Entry::find('conferences-2')->get('sponsors')); 26 | $this->assertSame(['sponsors-1'], Entry::find('conferences-1')->get('sponsors')); 27 | 28 | Entry::find('conferences-1')->set('sponsors', ['sponsors-2'])->save(); 29 | 30 | $this->assertSame(['conferences-2'], Entry::find('sponsors-1')->get('sponsoring')); 31 | $this->assertSame(['conferences-2', 'conferences-1'], Entry::find('sponsors-2')->get('sponsoring')); 32 | 33 | Entry::find('conferences-1')->set('sponsors', [])->save(); 34 | Entry::find('conferences-2')->set('sponsors', [])->save(); 35 | 36 | $this->assertSame([], Entry::find('sponsors-1')->get('sponsoring', [])); 37 | $this->assertSame([], Entry::find('sponsors-2')->get('sponsoring', [])); 38 | } 39 | 40 | public function test_many_to_many_relationship_with_events() 41 | { 42 | Relate::clear() 43 | ->manyToMany('conferences.sponsors', 'sponsors.sponsoring') 44 | ->withEvents(); 45 | 46 | Entry::find('sponsors-1')->set('sponsoring', [ 47 | 'conferences-1', 48 | 'conferences-2', 49 | ])->save(); 50 | 51 | Entry::find('sponsors-2')->set('sponsoring', [ 52 | 'conferences-2', 53 | ])->save(); 54 | 55 | $this->assertSame(['sponsors-1', 'sponsors-2'], Entry::find('conferences-2')->get('sponsors')); 56 | $this->assertSame(['sponsors-1'], Entry::find('conferences-1')->get('sponsors')); 57 | 58 | Entry::find('conferences-1')->set('sponsors', ['sponsors-2'])->save(); 59 | 60 | $this->assertSame(['conferences-2'], Entry::find('sponsors-1')->get('sponsoring')); 61 | $this->assertSame(['conferences-2', 'conferences-1'], Entry::find('sponsors-2')->get('sponsoring')); 62 | 63 | Entry::find('conferences-1')->set('sponsors', [])->save(); 64 | Entry::find('conferences-2')->set('sponsors', [])->save(); 65 | 66 | $this->assertSame([], Entry::find('sponsors-1')->get('sponsoring', [])); 67 | $this->assertSame([], Entry::find('sponsors-2')->get('sponsoring', [])); 68 | } 69 | 70 | public function test_many_to_many_user_relationships() 71 | { 72 | Relate::clear() 73 | ->manyToMany('conferences.conference_users', 'user:user_conferences'); 74 | 75 | User::find('user-1')->set('user_conferences', [ 76 | 'conferences-1', 77 | 'conferences-2', 78 | ])->save(); 79 | 80 | $this->assertSame(['user-1'], Entry::find('conferences-1')->get('conference_users', [])); 81 | $this->assertSame(['user-1'], Entry::find('conferences-2')->get('conference_users', [])); 82 | 83 | Entry::find('conferences-1')->set('conference_users', ['user-2'])->save(); 84 | 85 | $this->assertSame(['user-2'], Entry::find('conferences-1')->get('conference_users', [])); 86 | $this->assertSame(['conferences-2'], User::find('user-1')->get('user_conferences', [])); 87 | $this->assertSame(['conferences-1'], User::find('user-2')->get('user_conferences', [])); 88 | } 89 | 90 | public function test_many_to_many_term_relationships() 91 | { 92 | Relate::clear() 93 | ->manyToMany('term:topics.posts', 'entry:articles.topics'); 94 | 95 | Entry::find('articles-1')->set('topics', [ 96 | 'topics-one', 97 | 'topics-two', 98 | ])->save(); 99 | 100 | $this->assertSame(['articles-1'], $this->getTerm('topics-one')->get('posts', [])); 101 | $this->assertSame(['articles-1'], $this->getTerm('topics-two')->get('posts', [])); 102 | 103 | Entry::find('articles-1')->set('topics', ['topics-two'])->save(); 104 | 105 | $this->assertSame(['articles-1'], $this->getTerm('topics-two')->get('posts', [])); 106 | 107 | Entry::find('articles-1')->set('topics', [])->save(); 108 | 109 | $this->assertSame([], $this->getTerm('topics-one')->get('posts', [])); 110 | $this->assertSame([], $this->getTerm('topics-two')->get('posts', [])); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/ManyToOneDeleteTest.php: -------------------------------------------------------------------------------- 1 | manyToOne('authors.books', 'books.author'); 15 | 16 | Entry::find('books-1')->set('author', 'authors-1')->save(); 17 | 18 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 19 | 20 | Entry::find('books-2')->set('author', 'authors-1')->save(); 21 | 22 | $this->assertSame(['books-1', 'books-2'], Entry::find('authors-1')->get('books', [])); 23 | 24 | Entry::find('books-2')->set('author', 'authors-2')->save(); 25 | 26 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 27 | $this->assertSame(['books-2'], Entry::find('authors-2')->get('books', [])); 28 | 29 | Entry::find('authors-1')->delete(); 30 | Entry::find('authors-2')->delete(); 31 | 32 | $this->assertNull(Entry::find('books-1')->get('author')); 33 | $this->assertNull(Entry::find('books-2')->get('author')); 34 | } 35 | 36 | public function test_many_to_one_user_delete() 37 | { 38 | Relate::clear() 39 | ->manyToOne('user:managing_conferences', 'conferences.managed_by'); 40 | 41 | Entry::find('conferences-1')->set('managed_by', 'user-1')->save(); 42 | 43 | $this->assertSame(['conferences-1'], User::find('user-1')->get('managing_conferences', [])); 44 | 45 | Entry::find('conferences-2')->set('managed_by', 'user-1')->save(); 46 | 47 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('managing_conferences', [])); 48 | 49 | Entry::find('conferences-2')->set('managed_by', 'user-2')->save(); 50 | 51 | $this->assertSame(['conferences-1'], User::find('user-1')->get('managing_conferences', [])); 52 | $this->assertSame(['conferences-2'], User::find('user-2')->get('managing_conferences', [])); 53 | 54 | User::find('user-1')->delete(); 55 | User::find('user-2')->delete(); 56 | 57 | $this->assertNull(Entry::find('conferences-1')->get('managed_by')); 58 | $this->assertNull(Entry::find('conferences-2')->get('managed_by')); 59 | } 60 | 61 | public function test_many_to_one_term_delete() 62 | { 63 | Relate::clear() 64 | ->manyToOne('term:topics.posts', 'entry:articles.post_topic'); 65 | 66 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 67 | Entry::find('articles-2')->set('post_topic', 'topics-one')->save(); 68 | 69 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 70 | Entry::find('articles-1')->delete(); 71 | $this->assertSame(['articles-2'], $this->getTerm('topics-one')->get('posts', [])); 72 | 73 | Entry::find('articles-2')->delete(); 74 | $this->assertSame([], $this->getTerm('topics-one')->get('posts', [])); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/ManyToOneDisabledDeleteTest.php: -------------------------------------------------------------------------------- 1 | manyToOne('authors.books', 'books.author') 15 | ->allowDelete(false); 16 | 17 | Entry::find('books-1')->set('author', 'authors-1')->save(); 18 | 19 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 20 | 21 | Entry::find('books-2')->set('author', 'authors-1')->save(); 22 | 23 | $this->assertSame(['books-1', 'books-2'], Entry::find('authors-1')->get('books', [])); 24 | 25 | Entry::find('books-2')->set('author', 'authors-2')->save(); 26 | 27 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 28 | $this->assertSame(['books-2'], Entry::find('authors-2')->get('books', [])); 29 | 30 | Entry::find('authors-1')->delete(); 31 | Entry::find('authors-2')->delete(); 32 | 33 | $this->assertSame('authors-1', Entry::find('books-1')->get('author')); 34 | $this->assertSame('authors-2', Entry::find('books-2')->get('author')); 35 | } 36 | 37 | public function test_many_to_one_user_delete_disabled() 38 | { 39 | Relate::clear() 40 | ->manyToOne('user:managing_conferences', 'conferences.managed_by') 41 | ->allowDelete(false); 42 | 43 | Entry::find('conferences-1')->set('managed_by', 'user-1')->save(); 44 | 45 | $this->assertSame(['conferences-1'], User::find('user-1')->get('managing_conferences', [])); 46 | 47 | Entry::find('conferences-2')->set('managed_by', 'user-1')->save(); 48 | 49 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('managing_conferences', [])); 50 | 51 | Entry::find('conferences-2')->set('managed_by', 'user-2')->save(); 52 | 53 | $this->assertSame(['conferences-1'], User::find('user-1')->get('managing_conferences', [])); 54 | $this->assertSame(['conferences-2'], User::find('user-2')->get('managing_conferences', [])); 55 | 56 | User::find('user-1')->delete(); 57 | User::find('user-2')->delete(); 58 | 59 | $this->assertSame('user-1', Entry::find('conferences-1')->get('managed_by')); 60 | $this->assertSame('user-2', Entry::find('conferences-2')->get('managed_by')); 61 | } 62 | 63 | public function test_many_to_one_term_delete_disabled() 64 | { 65 | Relate::clear() 66 | ->manyToOne('term:topics.posts', 'entry:articles.post_topic') 67 | ->allowDelete(false); 68 | 69 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 70 | Entry::find('articles-2')->set('post_topic', 'topics-one')->save(); 71 | 72 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 73 | Entry::find('articles-1')->delete(); 74 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 75 | 76 | Entry::find('articles-2')->delete(); 77 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/ManyToOneTest.php: -------------------------------------------------------------------------------- 1 | manyToOne('authors.books', 'books.author'); 15 | 16 | Entry::find('books-1')->set('author', 'authors-1')->save(); 17 | 18 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 19 | 20 | Entry::find('books-2')->set('author', 'authors-1')->save(); 21 | 22 | $this->assertSame(['books-1', 'books-2'], Entry::find('authors-1')->get('books', [])); 23 | 24 | Entry::find('books-2')->set('author', 'authors-2')->save(); 25 | 26 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 27 | $this->assertSame(['books-2'], Entry::find('authors-2')->get('books', [])); 28 | 29 | Entry::find('books-1')->set('author', null)->save(); 30 | Entry::find('books-2')->set('author', null)->save(); 31 | 32 | $this->assertSame([], Entry::find('authors-1')->get('books', [])); 33 | $this->assertSame([], Entry::find('authors-2')->get('books', [])); 34 | } 35 | 36 | public function test_many_to_one_user_relationship() 37 | { 38 | Relate::clear() 39 | ->manyToOne('user:managing_conferences', 'conferences.managed_by'); 40 | 41 | Entry::find('conferences-1')->set('managed_by', 'user-1')->save(); 42 | 43 | $this->assertSame(['conferences-1'], User::find('user-1')->get('managing_conferences', [])); 44 | 45 | Entry::find('conferences-2')->set('managed_by', 'user-1')->save(); 46 | 47 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('managing_conferences', [])); 48 | 49 | Entry::find('conferences-2')->set('managed_by', 'user-2')->save(); 50 | 51 | $this->assertSame(['conferences-1'], User::find('user-1')->get('managing_conferences', [])); 52 | $this->assertSame(['conferences-2'], User::find('user-2')->get('managing_conferences', [])); 53 | } 54 | 55 | public function test_many_to_one_term_relationship() 56 | { 57 | Relate::clear() 58 | ->manyToOne('term:topics.posts', 'entry:articles.post_topic'); 59 | 60 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 61 | 62 | $this->assertSame(['articles-1'], $this->getTerm('topics-one')->get('posts', [])); 63 | 64 | Entry::find('articles-2')->set('post_topic', 'topics-one')->save(); 65 | 66 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 67 | 68 | Entry::find('articles-2')->set('post_topic', 'topics-two')->save(); 69 | 70 | $this->assertSame(['articles-1'], $this->getTerm('topics-one')->get('posts', [])); 71 | 72 | $this->assertSame(['articles-2'], $this->getTerm('topics-two')->get('posts', [])); 73 | 74 | Entry::find('articles-1')->set('post_topic', null)->save(); 75 | 76 | $this->assertSame([], $this->getTerm('topics-one')->get('posts', [])); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/OneToManyDeleteTest.php: -------------------------------------------------------------------------------- 1 | oneToMany('books.author', 'authors.books'); 15 | 16 | Entry::find('books-1')->set('author', 'authors-1')->save(); 17 | 18 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 19 | 20 | Entry::find('books-2')->set('author', 'authors-1')->save(); 21 | 22 | $this->assertSame(['books-1', 'books-2'], Entry::find('authors-1')->get('books', [])); 23 | 24 | Entry::find('books-2')->set('author', 'authors-2')->save(); 25 | 26 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 27 | $this->assertSame(['books-2'], Entry::find('authors-2')->get('books', [])); 28 | 29 | Entry::find('books-1')->delete(); 30 | Entry::find('books-2')->delete(); 31 | 32 | $this->assertSame([], Entry::find('authors-1')->get('books', [])); 33 | $this->assertSame([], Entry::find('authors-2')->get('books', [])); 34 | } 35 | 36 | public function test_one_to_many_user_delete() 37 | { 38 | Relate::clear() 39 | ->oneToMany('conferences.managed_by', 'user:managing_conferences'); 40 | 41 | Entry::find('conferences-1')->set('managed_by', 'user-1')->save(); 42 | Entry::find('conferences-2')->set('managed_by', 'user-1')->save(); 43 | 44 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('managing_conferences', [])); 45 | Entry::find('conferences-1')->delete(); 46 | $this->assertSame(['conferences-2'], User::find('user-1')->get('managing_conferences', [])); 47 | 48 | Entry::find('conferences-2')->delete(); 49 | $this->assertSame([], User::find('user-1')->get('managing_conferences', [])); 50 | } 51 | 52 | public function test_one_to_many_term_delete() 53 | { 54 | Relate::clear() 55 | ->oneToMany('entry:articles.post_topic', 'term:topics.posts'); 56 | 57 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 58 | Entry::find('articles-2')->set('post_topic', 'topics-one')->save(); 59 | 60 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 61 | Entry::find('articles-1')->delete(); 62 | $this->assertSame(['articles-2'], $this->getTerm('topics-one')->get('posts', [])); 63 | 64 | Entry::find('articles-2')->delete(); 65 | $this->assertSame([], $this->getTerm('topics-one')->get('posts', [])); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/OneToManyDisabledDeleteTest.php: -------------------------------------------------------------------------------- 1 | oneToMany('books.author', 'authors.books') 15 | ->allowDelete(false); 16 | 17 | Entry::find('books-1')->set('author', 'authors-1')->save(); 18 | 19 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 20 | 21 | Entry::find('books-2')->set('author', 'authors-1')->save(); 22 | 23 | $this->assertSame(['books-1', 'books-2'], Entry::find('authors-1')->get('books', [])); 24 | 25 | Entry::find('books-2')->set('author', 'authors-2')->save(); 26 | 27 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 28 | $this->assertSame(['books-2'], Entry::find('authors-2')->get('books', [])); 29 | 30 | Entry::find('books-1')->delete(); 31 | Entry::find('books-2')->delete(); 32 | 33 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 34 | $this->assertSame(['books-2'], Entry::find('authors-2')->get('books', [])); 35 | } 36 | 37 | public function test_one_to_many_user_delete_disabled() 38 | { 39 | Relate::clear() 40 | ->oneToMany('conferences.managed_by', 'user:managing_conferences') 41 | ->allowDelete(false); 42 | 43 | Entry::find('conferences-1')->set('managed_by', 'user-1')->save(); 44 | Entry::find('conferences-2')->set('managed_by', 'user-1')->save(); 45 | 46 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('managing_conferences', [])); 47 | Entry::find('conferences-1')->delete(); 48 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('managing_conferences', [])); 49 | 50 | Entry::find('conferences-2')->delete(); 51 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('managing_conferences', [])); 52 | } 53 | 54 | public function test_one_to_many_term_delete_disabled() 55 | { 56 | Relate::clear() 57 | ->oneToMany('entry:articles.post_topic', 'term:topics.posts') 58 | ->allowDelete(false); 59 | 60 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 61 | Entry::find('articles-2')->set('post_topic', 'topics-one')->save(); 62 | 63 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 64 | Entry::find('articles-1')->delete(); 65 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 66 | 67 | Entry::find('articles-2')->delete(); 68 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/OneToManyTest.php: -------------------------------------------------------------------------------- 1 | oneToMany('books.author', 'authors.books'); 15 | 16 | Entry::find('books-1')->set('author', 'authors-1')->save(); 17 | 18 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 19 | 20 | Entry::find('books-2')->set('author', 'authors-1')->save(); 21 | 22 | $this->assertSame(['books-1', 'books-2'], Entry::find('authors-1')->get('books', [])); 23 | 24 | Entry::find('books-2')->set('author', 'authors-2')->save(); 25 | 26 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 27 | $this->assertSame(['books-2'], Entry::find('authors-2')->get('books', [])); 28 | 29 | Entry::find('books-1')->set('author', null)->save(); 30 | Entry::find('books-2')->set('author', null)->save(); 31 | 32 | $this->assertSame([], Entry::find('authors-1')->get('books', [])); 33 | $this->assertSame([], Entry::find('authors-2')->get('books', [])); 34 | } 35 | 36 | public function test_explicit_one_to_many() 37 | { 38 | Relate::clear(); 39 | Relate::collection('books') 40 | ->field('author') 41 | ->isRelatedTo('authors') 42 | ->through('books') 43 | ->manyToOne(); 44 | 45 | Relate::collection('authors') 46 | ->field('books') 47 | ->isRelatedTo('books') 48 | ->through('author') 49 | ->oneToMany(); 50 | 51 | Entry::find('books-1')->set('author', 'authors-1')->save(); 52 | 53 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 54 | 55 | Entry::find('books-2')->set('author', 'authors-1')->save(); 56 | 57 | $this->assertSame(['books-1', 'books-2'], Entry::find('authors-1')->get('books', [])); 58 | 59 | Entry::find('books-2')->set('author', 'authors-2')->save(); 60 | 61 | $this->assertSame(['books-1'], Entry::find('authors-1')->get('books', [])); 62 | $this->assertSame(['books-2'], Entry::find('authors-2')->get('books', [])); 63 | 64 | Entry::find('books-1')->set('author', null)->save(); 65 | Entry::find('books-2')->set('author', null)->save(); 66 | 67 | $this->assertSame([], Entry::find('authors-1')->get('books', [])); 68 | $this->assertSame([], Entry::find('authors-2')->get('books', [])); 69 | } 70 | 71 | public function test_one_to_many_user_relationships() 72 | { 73 | Relate::clear() 74 | ->oneToMany('conferences.managed_by', 'user:managing_conferences'); 75 | 76 | Entry::find('conferences-1')->set('managed_by', 'user-1')->save(); 77 | 78 | $this->assertSame(['conferences-1'], User::find('user-1')->get('managing_conferences', [])); 79 | 80 | Entry::find('conferences-2')->set('managed_by', 'user-1')->save(); 81 | 82 | $this->assertSame(['conferences-1', 'conferences-2'], User::find('user-1')->get('managing_conferences', [])); 83 | 84 | Entry::find('conferences-2')->set('managed_by', 'user-2')->save(); 85 | 86 | $this->assertSame(['conferences-1'], User::find('user-1')->get('managing_conferences', [])); 87 | $this->assertSame(['conferences-2'], User::find('user-2')->get('managing_conferences', [])); 88 | 89 | Entry::find('conferences-1')->set('managed_by', null)->save(); 90 | Entry::find('conferences-2')->set('managed_by', null)->save(); 91 | 92 | $this->assertSame([], User::find('user-1')->get('managing_conferences', [])); 93 | $this->assertSame([], User::find('user-2')->get('managing_conferences', [])); 94 | } 95 | 96 | public function test_one_to_many_term_relationships() 97 | { 98 | Relate::clear() 99 | ->oneToMany('entry:articles.post_topic', 'term:topics.posts'); 100 | 101 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 102 | 103 | $this->assertSame(['articles-1'], $this->getTerm('topics-one')->get('posts', [])); 104 | 105 | Entry::find('articles-2')->set('post_topic', 'topics-one')->save(); 106 | 107 | $this->assertSame(['articles-1', 'articles-2'], $this->getTerm('topics-one')->get('posts', [])); 108 | 109 | Entry::find('articles-2')->set('post_topic', 'topics-two')->save(); 110 | 111 | $this->assertSame(['articles-1'], $this->getTerm('topics-one')->get('posts', [])); 112 | $this->assertSame(['articles-2'], $this->getTerm('topics-two')->get('posts', [])); 113 | 114 | Entry::find('articles-1')->set('post_topic', null)->save(); 115 | Entry::find('articles-2')->set('post_topic', null)->save(); 116 | 117 | $this->assertSame([], $this->getTerm('topics-one')->get('posts', [])); 118 | $this->assertSame([], $this->getTerm('topics-two')->get('posts', [])); 119 | } 120 | 121 | public function test_one_to_many_updates_dependents() 122 | { 123 | Relate::clear(); 124 | Relate::oneToMany( 125 | 'books.author', 126 | 'authors.books' 127 | ); 128 | 129 | Entry::find('books-1')->set('author', 'authors-1')->save(); 130 | Entry::find('books-2')->set('author', 'authors-1')->save(); 131 | Entry::find('books-3')->set('author', 'authors-1')->save(); 132 | 133 | Entry::find('books-4')->set('author', 'authors-1')->save(); 134 | Entry::find('books-5')->set('author', 'authors-2')->save(); 135 | 136 | Entry::find('authors-1')->set('books', [ 137 | 'books-1', 138 | 'books-2', 139 | 'books-3', 140 | ])->save(); 141 | 142 | Entry::find('authors-2')->set('books', [ 143 | 'books-4', 144 | 'books-5', 145 | ])->save(); 146 | 147 | Entry::find('authors-1')->set('books', [ 148 | 'books-1', 149 | 'books-2', 150 | 'books-3', 151 | 'books-4', 152 | ])->save(); 153 | 154 | $this->assertSame(['books-5'], Entry::find('authors-2')->get('books')); 155 | 156 | $this->assertSame([ 157 | 'books-1', 158 | 'books-2', 159 | 'books-3', 160 | 'books-4', 161 | ], Entry::find('authors-1')->get('books')); 162 | 163 | $this->assertSame('authors-1', Entry::find('books-1')->get('author')); 164 | $this->assertSame('authors-1', Entry::find('books-2')->get('author')); 165 | $this->assertSame('authors-1', Entry::find('books-3')->get('author')); 166 | $this->assertSame('authors-1', Entry::find('books-4')->get('author')); 167 | 168 | $this->assertSame('authors-2', Entry::find('books-5')->get('author')); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/OneToOneDeleteTest.php: -------------------------------------------------------------------------------- 1 | oneToOne('employees.position', 'positions.filled_by'); 16 | 17 | Entry::find('employees-1')->set('position', 'positions-1')->save(); 18 | 19 | $this->assertSame('employees-1', Entry::find('positions-1')->get('filled_by', null)); 20 | 21 | Entry::find('positions-1')->delete(); 22 | 23 | $this->assertNull(Entry::find('employees-1')->get('position', null)); 24 | } 25 | 26 | public function test_one_to_one_user_delete() 27 | { 28 | Relate::clear() 29 | ->oneToOne('books.book_author', 'user:book'); 30 | 31 | Entry::find('books-1')->set('book_author', 'user-1')->save(); 32 | 33 | $this->assertSame('books-1', User::find('user-1')->get('book', null)); 34 | 35 | User::find('user-1')->delete(); 36 | 37 | $this->assertNull(Entry::find('books-1')->get('book_author', null)); 38 | } 39 | 40 | public function test_one_to_one_term_delete() 41 | { 42 | Relate::clear() 43 | ->oneToOne('term:topics.single_post', 'entry:articles.post_topic'); 44 | 45 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 46 | 47 | $this->assertSame('articles-1', $this->getTerm('topics-one')->get('single_post', null)); 48 | 49 | TermDeletedListener::$break = true; 50 | $this->getTerm('topics-one')->delete(); 51 | 52 | $this->assertNull(Entry::find('articles-1')->get('post_topic', null)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/OneToOneDisabledDeleteTest.php: -------------------------------------------------------------------------------- 1 | oneToOne('employees.position', 'positions.filled_by') 15 | ->allowDelete(false); 16 | 17 | Entry::find('employees-1')->set('position', 'positions-1')->save(); 18 | 19 | $this->assertSame('employees-1', Entry::find('positions-1')->get('filled_by', null)); 20 | 21 | Entry::find('positions-1')->delete(); 22 | 23 | $this->assertSame('positions-1', Entry::find('employees-1')->get('position', null)); 24 | } 25 | 26 | public function test_one_to_one_user_disabled_delete() 27 | { 28 | Relate::clear() 29 | ->oneToOne('books.book_author', 'user:book') 30 | ->allowDelete(false); 31 | 32 | Entry::find('books-1')->set('book_author', 'user-1')->save(); 33 | 34 | $this->assertSame('books-1', User::find('user-1')->get('book', null)); 35 | 36 | User::find('user-1')->delete(); 37 | 38 | $this->assertSame('user-1', Entry::find('books-1')->get('book_author', null)); 39 | } 40 | 41 | public function test_one_to_one_term_delete_disabled() 42 | { 43 | Relate::clear() 44 | ->oneToOne('term:topics.single_post', 'entry:articles.post_topic') 45 | ->allowDelete(false); 46 | 47 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 48 | 49 | $this->assertSame('articles-1', $this->getTerm('topics-one')->get('single_post', null)); 50 | 51 | $this->getTerm('topics-one')->delete(); 52 | 53 | $this->assertSame('topics-one', Entry::find('articles-1')->get('post_topic', null)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/OneToOneTest.php: -------------------------------------------------------------------------------- 1 | oneToOne('employees.position', 'positions.filled_by'); 15 | 16 | Entry::find('employees-1')->set('position', 'positions-1')->save(); 17 | 18 | $this->assertSame('employees-1', Entry::find('positions-1')->get('filled_by', null)); 19 | 20 | Entry::find('positions-1')->set('filled_by', null)->save(); 21 | 22 | $this->assertNull(Entry::find('employees-1')->get('position', null)); 23 | } 24 | 25 | public function test_one_to_one_user_relationship() 26 | { 27 | Relate::clear() 28 | ->oneToOne('books.book_author', 'user:book'); 29 | 30 | Entry::find('books-1')->set('book_author', 'user-1')->save(); 31 | 32 | $this->assertSame('books-1', User::find('user-1')->get('book', null)); 33 | 34 | User::find('user-1')->set('book', null)->save(); 35 | $this->assertNull(Entry::find('books-1')->get('book_author', null)); 36 | } 37 | 38 | public function test_one_to_one_term_relationship() 39 | { 40 | Relate::clear() 41 | ->oneToOne('term:topics.single_post', 'entry:articles.post_topic'); 42 | 43 | Entry::find('articles-1')->set('post_topic', 'topics-one')->save(); 44 | 45 | $this->assertSame('articles-1', $this->getTerm('topics-one')->get('single_post', null)); 46 | 47 | $this->getTerm('topics-one')->set('single_post', null)->save(); 48 | 49 | $this->assertNull(Entry::find('articles-1')->get('post_topic', null)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/PreventSavingStacheItemsToDisk.php: -------------------------------------------------------------------------------- 1 | fakeStacheDirectory = Path::tidy($this->fakeStacheDirectory); 16 | 17 | Stache::stores()->each(function ($store) { 18 | $dir = Path::tidy(__DIR__.'/__fixtures__'); 19 | $relative = Str::after(Str::after($store->directory(), $dir), '/'); 20 | $store->directory($this->fakeStacheDirectory.'/'.$relative); 21 | }); 22 | } 23 | 24 | protected function deleteFakeStacheDirectory() 25 | { 26 | app('files')->deleteDirectory($this->fakeStacheDirectory); 27 | 28 | if (! file_exists($this->fakeStacheDirectory)) { 29 | mkdir($this->fakeStacheDirectory); 30 | } 31 | 32 | touch($this->fakeStacheDirectory.'/.gitkeep'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/RelationshipTestCase.php: -------------------------------------------------------------------------------- 1 | createBlueprints(); 27 | $this->createUserBlueprint(); 28 | $this->createTaxonomies(); 29 | $this->createTerms(); 30 | $this->createCollections(); 31 | $this->createCollectionEntries(); 32 | $this->createUsers(); 33 | } 34 | 35 | protected function buildBlueprint($name, $path) 36 | { 37 | $fields = YAML::parse(file_get_contents(__DIR__.'/__fixtures__/blueprints/'.$name.'.yaml'))['sections']['main']['fields']; 38 | 39 | $blueprint = new Blueprint(); 40 | $blueprint->setContents([ 41 | 'fields' => $fields, 42 | ]); 43 | $blueprint->setHandle($name); 44 | 45 | $this->blueprints[$name] = $blueprint; 46 | 47 | BlueprintRepository::shouldReceive('find')->zeroOrMoreTimes()->with($path)->andReturn($blueprint); 48 | BlueprintRepository::shouldReceive('in')->zeroOrMoreTimes()->with($path)->andReturn(collect([ 49 | $name => $blueprint, 50 | ])); 51 | } 52 | 53 | protected function createUserBlueprint() 54 | { 55 | $this->buildBlueprint('user', 'user'); 56 | } 57 | 58 | protected function makeBlueprint($name) 59 | { 60 | $this->buildBlueprint($name, 'collections/'.$name); 61 | } 62 | 63 | protected function makeTaxonomyBlueprint($name) 64 | { 65 | $this->buildBlueprint($name, 'taxonomies/'.$name); 66 | } 67 | 68 | protected function createBlueprints() 69 | { 70 | $this->makeTaxonomyBlueprint('topics'); 71 | $this->makeBlueprint('authors'); 72 | $this->makeBlueprint('books'); 73 | $this->makeBlueprint('conferences'); 74 | $this->makeBlueprint('employees'); 75 | $this->makeBlueprint('positions'); 76 | $this->makeBlueprint('sponsors'); 77 | $this->makeBlueprint('articles'); 78 | } 79 | 80 | protected function createCollections() 81 | { 82 | Collection::make('authors')->routes('authors/{slug}')->save(); 83 | Collection::make('books')->routes('books/{slug}')->save(); 84 | Collection::make('conferences')->routes('conferences/{slug}')->save(); 85 | Collection::make('employees')->routes('employees/{slug}')->save(); 86 | Collection::make('positions')->routes('positions/{slug}')->save(); 87 | Collection::make('sponsors')->routes('sponsors/{slug}')->save(); 88 | } 89 | 90 | protected function createTaxonomies() 91 | { 92 | \Statamic\Facades\Taxonomy::make('topics')->save(); 93 | } 94 | 95 | protected function createTerms() 96 | { 97 | $terms = [ 98 | 'one', 'two', 'three', 'four', 99 | ]; 100 | 101 | foreach ($terms as $term) { 102 | $title = 'Term '.Str::ucfirst($term); 103 | 104 | Term::make()->taxonomy('topics')->slug('topics-'.$term)->data([ 105 | 'title' => $title, 106 | ])->save(); 107 | } 108 | } 109 | 110 | /** 111 | * Locate a taxonomy term by its slug. 112 | * 113 | * @param string $slug 114 | * @return LocalizedTerm|null 115 | */ 116 | protected function getTerm($slug) 117 | { 118 | return Term::query()->where('slug', $slug)->first(); 119 | } 120 | 121 | protected function createUsers() 122 | { 123 | User::make()->id('user-1')->email('user1@example.org')->save(); 124 | User::make()->id('user-2')->email('user2@example.org')->save(); 125 | } 126 | 127 | private function createEntry($collection, $id, $data) 128 | { 129 | EntryFactory::collection($collection)->id($id)->slug($id)->data($data)->create(); 130 | } 131 | 132 | private function createEntries($collection, $entries) 133 | { 134 | $count = 1; 135 | 136 | foreach ($entries as $entry) { 137 | $this->createEntry($collection, $collection.'-'.$count, $entry); 138 | $count += 1; 139 | } 140 | } 141 | 142 | protected function createCollectionEntries() 143 | { 144 | $this->createEntries('authors', [ 145 | [ 146 | 'title' => 'Author One', 147 | ], 148 | [ 149 | 'title' => 'Author Two', 150 | ], 151 | ]); 152 | 153 | $this->createEntries('books', [ 154 | [ 155 | 'title' => 'Book One', 156 | ], 157 | [ 158 | 'title' => 'Book Two', 159 | ], 160 | [ 161 | 'title' => 'Book Three', 162 | ], 163 | [ 164 | 'title' => 'Book Four', 165 | ], 166 | [ 167 | 'title' => 'Book Five', 168 | ], 169 | ]); 170 | 171 | $this->createEntries('conferences', [ 172 | [ 173 | 'title' => 'Conference One', 174 | ], 175 | [ 176 | 'title' => 'Conference Two', 177 | ], 178 | ]); 179 | 180 | $this->createEntries('employees', [ 181 | [ 182 | 'title' => 'Employee One', 183 | ], 184 | [ 185 | 'title' => 'Employee Two', 186 | ], 187 | ]); 188 | 189 | $this->createEntries('positions', [ 190 | [ 191 | 'title' => 'Position One', 192 | ], 193 | [ 194 | 'title' => 'Position Two', 195 | ], 196 | ]); 197 | 198 | $this->createEntries('sponsors', [ 199 | [ 200 | 'title' => 'Sponsor One', 201 | ], 202 | [ 203 | 'title' => 'Sponsor Two', 204 | ], 205 | ]); 206 | 207 | $this->createEntries('articles', [ 208 | [ 209 | 'title' => 'Article One', 210 | ], 211 | [ 212 | 'title' => 'Article Two', 213 | ], 214 | [ 215 | 'title' => 'Article Three', 216 | ], 217 | [ 218 | 'title' => 'Article Four', 219 | ], 220 | ]); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /tests/SetNotationRelationshipsTest.php: -------------------------------------------------------------------------------- 1 | assertSame([ 16 | 'entry:events.related', 17 | 'entry:clubs.related', 18 | 'entry:businesses.related', 19 | 'entry:services.related', 20 | 'entry:articles.related', 21 | ], RelationshipManager::extractCollections($left)); 22 | } 23 | 24 | public function test_it_extracts_collection_names_without_sets() 25 | { 26 | Relate::clear()->manyToMany( 27 | 'term:categories.products', 28 | 'entry:{pens,markers}.categories' 29 | ); 30 | 31 | /** @var EntryRelationship[] $relationships */ 32 | $relationships = Relate::getAllRelationships(); 33 | 34 | $this->assertCount(4, $relationships); 35 | 36 | $this->assertRelationshipDetails($relationships[0], 'term', 'entry', '[term]', 'pens', 'products', 'categories'); 37 | $this->assertRelationshipDetails($relationships[1], 'entry', 'term', 'pens', 'categories', 'categories', 'products'); 38 | $this->assertRelationshipDetails($relationships[2], 'term', 'entry', '[term]', 'markers', 'products', 'categories'); 39 | $this->assertRelationshipDetails($relationships[3], 'entry', 'term', 'markers', 'categories', 'categories', 'products'); 40 | } 41 | 42 | protected function assertRelationshipDetails(EntryRelationship $relationship, $leftType, $rightType, $leftCollection, $rightCollection, $leftField, $rightField) 43 | { 44 | $this->assertSame($leftType, $relationship->leftType); 45 | $this->assertSame($rightType, $relationship->rightType); 46 | $this->assertSame($leftCollection, $relationship->leftCollection); 47 | $this->assertSame($rightCollection, $relationship->rightCollection); 48 | $this->assertSame($leftField, $relationship->leftField); 49 | $this->assertSame($rightField, $relationship->rightField); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/articles.yaml: -------------------------------------------------------------------------------- 1 | title: Article 2 | sections: 3 | main: 4 | display: Main 5 | fields: 6 | - 7 | handle: title 8 | field: 9 | type: text 10 | required: true 11 | display: Title 12 | validate: 13 | - required 14 | - 15 | handle: content 16 | field: 17 | type: markdown 18 | display: Content 19 | restrict: false 20 | automatic_line_breaks: true 21 | automatic_links: false 22 | escape_markup: false 23 | smartypants: false 24 | - 25 | handle: excerpt 26 | field: 27 | type: textarea 28 | display: Excerpt 29 | character_limit: 160 30 | sidebar: 31 | display: Sidebar 32 | fields: 33 | - 34 | handle: slug 35 | field: 36 | type: slug 37 | localizable: true 38 | - 39 | handle: date 40 | field: 41 | type: date 42 | required: true 43 | validate: 44 | - required 45 | - 46 | handle: topics 47 | field: 48 | type: terms 49 | taxonomies: 50 | - topics 51 | display: Topics 52 | mode: select 53 | - 54 | handle: post_topic 55 | field: 56 | max_items: 1 57 | mode: default 58 | create: true 59 | taxonomies: 60 | - topics 61 | display: 'Post Topic' 62 | type: terms 63 | icon: taxonomy 64 | listable: hidden 65 | instructions_position: above 66 | visibility: visible 67 | always_save: false 68 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/authors.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | main: 3 | display: Main 4 | fields: 5 | - 6 | handle: title 7 | field: 8 | type: text 9 | required: true 10 | validate: 11 | - required 12 | - 13 | handle: content 14 | field: 15 | type: markdown 16 | localizable: true 17 | - 18 | handle: books 19 | field: 20 | mode: default 21 | create: true 22 | collections: 23 | - books 24 | display: Books 25 | type: entries 26 | icon: entries 27 | listable: hidden 28 | instructions_position: above 29 | read_only: false 30 | sidebar: 31 | display: Sidebar 32 | fields: 33 | - 34 | handle: slug 35 | field: 36 | type: slug 37 | localizable: true 38 | title: Authors 39 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/books.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | main: 3 | display: Main 4 | fields: 5 | - 6 | handle: title 7 | field: 8 | type: text 9 | required: true 10 | validate: 11 | - required 12 | - 13 | handle: content 14 | field: 15 | type: markdown 16 | localizable: true 17 | - 18 | handle: author 19 | field: 20 | max_items: 1 21 | mode: default 22 | create: true 23 | collections: 24 | - authors 25 | display: Author 26 | type: entries 27 | icon: entries 28 | listable: hidden 29 | instructions_position: above 30 | read_only: false 31 | - 32 | handle: book_author 33 | field: 34 | max_items: 1 35 | mode: select 36 | display: 'Book Author' 37 | type: users 38 | icon: users 39 | listable: hidden 40 | instructions_position: above 41 | visibility: visible 42 | sidebar: 43 | display: Sidebar 44 | fields: 45 | - 46 | handle: slug 47 | field: 48 | type: slug 49 | localizable: true 50 | title: Books 51 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/conferences.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | main: 3 | display: Main 4 | fields: 5 | - 6 | handle: title 7 | field: 8 | type: text 9 | required: true 10 | validate: 11 | - required 12 | - 13 | handle: content 14 | field: 15 | type: markdown 16 | localizable: true 17 | - 18 | handle: start_date 19 | field: 20 | mode: single 21 | time_enabled: false 22 | time_seconds_enabled: false 23 | full_width: false 24 | inline: false 25 | columns: 1 26 | rows: 1 27 | display: 'Start Date' 28 | type: date 29 | icon: date 30 | listable: hidden 31 | instructions_position: above 32 | read_only: false 33 | - 34 | handle: end_date 35 | field: 36 | mode: single 37 | time_enabled: false 38 | time_seconds_enabled: false 39 | full_width: false 40 | inline: false 41 | columns: 1 42 | rows: 1 43 | display: 'End Date' 44 | type: date 45 | icon: date 46 | listable: hidden 47 | instructions_position: above 48 | read_only: false 49 | - 50 | handle: sponsored_by 51 | field: 52 | mode: default 53 | create: true 54 | collections: 55 | - sponsors 56 | display: Sponsors 57 | type: entries 58 | icon: entries 59 | listable: hidden 60 | instructions_position: above 61 | read_only: false 62 | - 63 | handle: managed_by 64 | field: 65 | max_items: 1 66 | mode: select 67 | display: 'Managed By' 68 | type: users 69 | icon: users 70 | listable: hidden 71 | instructions_position: above 72 | visibility: visible 73 | - 74 | handle: special_sponsors 75 | field: 76 | collapse: false 77 | sets: 78 | new_set: 79 | display: 'New Set' 80 | fields: 81 | - 82 | handle: sponsor 83 | field: 84 | max_items: 1 85 | mode: default 86 | create: true 87 | collections: 88 | - sponsors 89 | display: Sponsor 90 | type: entries 91 | icon: entries 92 | listable: hidden 93 | instructions_position: above 94 | read_only: false 95 | - 96 | handle: special_notes 97 | field: 98 | antlers: false 99 | display: 'Special Notes' 100 | type: textarea 101 | icon: textarea 102 | listable: hidden 103 | instructions_position: above 104 | read_only: false 105 | display: 'Special Sponsors' 106 | type: replicator 107 | icon: replicator 108 | listable: hidden 109 | instructions_position: above 110 | read_only: false 111 | - 112 | handle: conference_users 113 | field: 114 | mode: select 115 | display: 'Conference Users' 116 | type: users 117 | icon: users 118 | listable: hidden 119 | instructions_position: above 120 | visibility: visible 121 | sidebar: 122 | display: Sidebar 123 | fields: 124 | - 125 | handle: slug 126 | field: 127 | type: slug 128 | localizable: true 129 | title: Conferences 130 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/employees.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | main: 3 | display: Main 4 | fields: 5 | - 6 | handle: title 7 | field: 8 | type: text 9 | required: true 10 | validate: 11 | - required 12 | - 13 | handle: content 14 | field: 15 | type: markdown 16 | localizable: true 17 | - 18 | handle: position 19 | field: 20 | max_items: 1 21 | mode: default 22 | create: true 23 | collections: 24 | - positions 25 | display: Position 26 | type: entries 27 | icon: entries 28 | listable: hidden 29 | instructions_position: above 30 | read_only: false 31 | sidebar: 32 | display: Sidebar 33 | fields: 34 | - 35 | handle: slug 36 | field: 37 | type: slug 38 | localizable: true 39 | title: Employees 40 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/positions.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | main: 3 | display: Main 4 | fields: 5 | - 6 | handle: title 7 | field: 8 | type: text 9 | required: true 10 | validate: 11 | - required 12 | - 13 | handle: content 14 | field: 15 | type: markdown 16 | localizable: true 17 | - 18 | handle: filled_by 19 | field: 20 | max_items: 1 21 | mode: default 22 | create: true 23 | collections: 24 | - employees 25 | display: 'Filled By' 26 | type: entries 27 | icon: entries 28 | listable: hidden 29 | instructions_position: above 30 | read_only: false 31 | sidebar: 32 | display: Sidebar 33 | fields: 34 | - 35 | handle: slug 36 | field: 37 | type: slug 38 | localizable: true 39 | title: Positions 40 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/sponsors.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | main: 3 | display: Main 4 | fields: 5 | - 6 | handle: title 7 | field: 8 | type: text 9 | required: true 10 | validate: 11 | - required 12 | - 13 | handle: content 14 | field: 15 | type: markdown 16 | localizable: true 17 | - 18 | handle: sponsoring 19 | field: 20 | mode: default 21 | create: true 22 | collections: 23 | - conferences 24 | display: Sponsoring 25 | type: entries 26 | icon: entries 27 | listable: hidden 28 | instructions_position: above 29 | read_only: false 30 | sidebar: 31 | display: Sidebar 32 | fields: 33 | - 34 | handle: slug 35 | field: 36 | type: slug 37 | localizable: true 38 | title: Sponsors 39 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/topics.yaml: -------------------------------------------------------------------------------- 1 | title: Topic 2 | sections: 3 | main: 4 | display: Main 5 | fields: 6 | - 7 | handle: title 8 | field: 9 | type: text 10 | required: true 11 | display: Title 12 | validate: 13 | - required 14 | - 15 | handle: posts 16 | field: 17 | mode: default 18 | create: true 19 | display: Posts 20 | type: entries 21 | icon: entries 22 | listable: hidden 23 | instructions_position: above 24 | visibility: visible 25 | always_save: false 26 | - 27 | handle: single_post 28 | field: 29 | mode: default 30 | create: true 31 | max_items: 1 32 | display: Posts 33 | type: entries 34 | icon: entries 35 | listable: hidden 36 | instructions_position: above 37 | visibility: visible 38 | always_save: false 39 | sidebar: 40 | display: Sidebar 41 | fields: 42 | - 43 | handle: slug 44 | field: 45 | type: slug 46 | required: true 47 | validate: 48 | - required 49 | -------------------------------------------------------------------------------- /tests/__fixtures__/blueprints/user.yaml: -------------------------------------------------------------------------------- 1 | sections: 2 | main: 3 | display: Main 4 | fields: 5 | - 6 | handle: name 7 | field: 8 | type: text 9 | display: Name 10 | - 11 | handle: email 12 | field: 13 | type: text 14 | input: email 15 | display: 'Email Address' 16 | - 17 | handle: user_conferences 18 | field: 19 | mode: default 20 | create: true 21 | collections: 22 | - conferences 23 | display: 'User Conferences' 24 | type: entries 25 | icon: entries 26 | listable: hidden 27 | instructions_position: above 28 | visibility: visible 29 | - 30 | handle: book 31 | field: 32 | max_items: 1 33 | mode: default 34 | create: true 35 | collections: 36 | - books 37 | display: Book 38 | type: entries 39 | icon: entries 40 | listable: hidden 41 | instructions_position: above 42 | visibility: visible 43 | - 44 | handle: managing_conferences 45 | field: 46 | mode: default 47 | create: true 48 | collections: 49 | - conferences 50 | display: 'Managing Conferences' 51 | type: entries 52 | icon: entries 53 | listable: hidden 54 | instructions_position: above 55 | visibility: visible 56 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/amp.php: -------------------------------------------------------------------------------- 1 | false, 6 | 'route' => 'amp', 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/antlers.php: -------------------------------------------------------------------------------- 1 | = 3.3 Antlers, recommended for new sites. 13 | | 14 | */ 15 | 16 | 'version' => 'regex', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Guarded Variables 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Any variable pattern that appears in this list will not be allowed 24 | | in any Antlers template, including any user-supplied values. 25 | | 26 | */ 27 | 28 | 'guardedVariables' => [ 29 | 'config.app.key', 30 | ], 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Guarded Tags 35 | |-------------------------------------------------------------------------- 36 | | 37 | | Any tag pattern that appears in this list will not be allowed 38 | | in any Antlers template, including any user-supplied values. 39 | | 40 | */ 41 | 42 | 'guardedTags' => [ 43 | 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Guarded Modifiers 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Any modifier pattern that appears in this list will not be allowed 52 | | in any Antlers template, including any user-supplied values. 53 | | 54 | */ 55 | 56 | 'guardedModifiers' => [ 57 | 58 | ], 59 | 60 | ]; 61 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/api.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_API_ENABLED', false), 19 | 20 | 'resources' => [ 21 | 'collections' => false, 22 | 'navs' => false, 23 | 'taxonomies' => false, 24 | 'assets' => false, 25 | 'globals' => false, 26 | 'forms' => false, 27 | 'users' => false, 28 | ], 29 | 30 | 'route' => env('STATAMIC_API_ROUTE', 'api'), 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Middleware & Authentication 35 | |-------------------------------------------------------------------------- 36 | | 37 | | Define the middleware / middleware group that will be applied to the 38 | | API route group. If you want to externally expose this API, here 39 | | you can configure a middleware based authentication layer. 40 | | 41 | */ 42 | 43 | 'middleware' => env('STATAMIC_API_MIDDLEWARE', 'api'), 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Pagination 48 | |-------------------------------------------------------------------------- 49 | | 50 | | The numbers of items to show on each paginated page. 51 | | 52 | */ 53 | 54 | 'pagination_size' => 50, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Caching 59 | |-------------------------------------------------------------------------- 60 | | 61 | | By default, Statamic will cache each endpoint until the specified 62 | | expiry, or until content is changed. See the documentation for 63 | | more details on how to customize your cache implementation. 64 | | 65 | | https://statamic.dev/content-api#caching 66 | | 67 | */ 68 | 69 | 'cache' => [ 70 | 'expiry' => 60, 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Exclude Keys 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Here you may provide an array of keys to be excluded from API responses. 79 | | For example, you may want to hide things like edit_url, api_url, etc. 80 | | 81 | */ 82 | 83 | 'excluded_keys' => [ 84 | // 85 | ], 86 | 87 | ]; 88 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/assets.php: -------------------------------------------------------------------------------- 1 | [ 6 | 7 | /* 8 | |-------------------------------------------------------------------------- 9 | | Route Prefix 10 | |-------------------------------------------------------------------------- 11 | | 12 | | The route prefix for serving HTTP based manipulated images through Glide. 13 | | If using the cached option, this should be the URL of the cached path. 14 | | 15 | */ 16 | 17 | 'route' => 'img', 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Require Glide security token 22 | |-------------------------------------------------------------------------- 23 | | 24 | | With this option enabled, you are protecting your website from mass image 25 | | resize attacks. You will need to generate tokens using the Glide tag 26 | | but may want to disable this while in development to tinker. 27 | | 28 | */ 29 | 30 | 'secure' => true, 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Image Manipulation Driver 35 | |-------------------------------------------------------------------------- 36 | | 37 | | The driver that will be used under the hood for image manipulation. 38 | | Supported: "gd" or "imagick" (if installed on your server) 39 | | 40 | */ 41 | 42 | 'driver' => 'gd', 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Save Cached Images 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Enabling this will make Glide save publicly accessible images. It will 50 | | increase performance at the cost of the dynamic nature of HTTP based 51 | | image manipulation. You will need to invalidate images manually. 52 | | 53 | */ 54 | 55 | 'cache' => false, 56 | 'cache_path' => public_path('img'), 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | Image Manipulation Presets 61 | |-------------------------------------------------------------------------- 62 | | 63 | | Rather than specifying your manipulation params in your templates with 64 | | the glide tag, you may define them here and reference their handles. 65 | | They will also be automatically generated when you upload assets. 66 | | 67 | */ 68 | 69 | 'presets' => [ 70 | // 'small' => ['w' => 200, 'h' => 200, 'q' => 75, 'fit' => 'crop'], 71 | ], 72 | 73 | ], 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Auto-Crop Assets 78 | |-------------------------------------------------------------------------- 79 | | 80 | | Enabling this will make Glide automatically crop assets at their focal 81 | | point (at at the center if no focal point is defined). Otherwise, 82 | | you will need to manually add any crop related parameters. 83 | | 84 | */ 85 | 86 | 'auto_crop' => true, 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Control Panel Thumbnail Restrictions 91 | |-------------------------------------------------------------------------- 92 | | 93 | | Thumbnails will not be generated for any assets any larger (in either 94 | | axis) than the values listed below. This helps prevent memory usage 95 | | issues out of the box. You may increase or decrease as necessary. 96 | | 97 | */ 98 | 99 | 'thumbnails' => [ 100 | 'max_width' => 10000, 101 | 'max_height' => 10000, 102 | ], 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | File Previews with Google Docs 107 | |-------------------------------------------------------------------------- 108 | | 109 | | Filetypes that cannot be rendered with HTML5 can opt into the Google Docs 110 | | Viewer. Google will get temporary access to these files so keep that in 111 | | mind for any privacy implications: https://policies.google.com/privacy 112 | | 113 | */ 114 | 115 | 'google_docs_viewer' => false, 116 | 117 | /* 118 | |-------------------------------------------------------------------------- 119 | | Cache Metadata 120 | |-------------------------------------------------------------------------- 121 | | 122 | | Asset metadata (filesize, dimensions, custom data, etc) will get cached 123 | | to optimize performance, so that it will not need to be constantly 124 | | re-evaluated from disk. You may disable this option if you are 125 | | planning to continually modify the same asset repeatedly. 126 | | 127 | */ 128 | 129 | 'cache_meta' => true, 130 | 131 | /* 132 | |-------------------------------------------------------------------------- 133 | | Focal Point Editor 134 | |-------------------------------------------------------------------------- 135 | | 136 | | When editing images in the Control Panel, there is an option to choose 137 | | a focal point. When working with third-party image providers such as 138 | | Cloudinary it can be useful to disable Statamic's built-in editor. 139 | | 140 | */ 141 | 142 | 'focal_point_editor' => true, 143 | ]; 144 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/cp.php: -------------------------------------------------------------------------------- 1 | env('CP_ENABLED', true), 15 | 16 | 'route' => env('CP_ROUTE', 'cp'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Start Page 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When a user logs into the Control Panel, they will be taken here. 24 | | For example: "dashboard", "collections/pages", etc. 25 | | 26 | */ 27 | 28 | 'start_page' => 'dashboard', 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Dashboard Widgets 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Here you may define any number of dashboard widgets. You're free to 36 | | use the same widget multiple times in different configurations. 37 | | 38 | */ 39 | 40 | 'widgets' => [ 41 | 'getting_started', 42 | ], 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Date Format 47 | |-------------------------------------------------------------------------- 48 | | 49 | | When a date is encountered throughout the Control Panel, it will be 50 | | rendered in the following format unless overridden in specific 51 | | fields, and so on. Any PHP date variables are permitted. 52 | | 53 | | This takes precedence over the date_format in system.php. 54 | | 55 | | https://www.php.net/manual/en/function.date.php 56 | | 57 | */ 58 | 59 | 'date_format' => 'Y-m-d', 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Pagination 64 | |-------------------------------------------------------------------------- 65 | | 66 | | The numbers of items to show on each paginated page. 67 | | 68 | */ 69 | 70 | 'pagination_size' => 50, 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Links to Documentation 75 | |-------------------------------------------------------------------------- 76 | | 77 | | Show contextual links to documentation throughout the Control Panel. 78 | | 79 | */ 80 | 81 | 'link_to_docs' => env('STATAMIC_LINK_TO_DOCS', true), 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | Support Link 86 | |-------------------------------------------------------------------------- 87 | | 88 | | Set the location of the support link in the "Useful Links" header 89 | | dropdown. Use 'false' to remove it entirely. 90 | | 91 | */ 92 | 93 | 'support_url' => env('STATAMIC_SUPPORT_URL', 'https://statamic.com/support'), 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Theme 98 | |-------------------------------------------------------------------------- 99 | | 100 | | Optionally spice up the login and other outside-the-control-panel 101 | | screens. You may choose between "rad" or "business" themes. 102 | | 103 | */ 104 | 105 | 'theme' => env('STATAMIC_THEME', 'rad'), 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | White Labeling 110 | |-------------------------------------------------------------------------- 111 | | 112 | | When in Pro Mode you may replace the Statamic name, logo, favicon, 113 | | and add your own CSS to the control panel to match your 114 | | company or client's brand. 115 | | 116 | */ 117 | 118 | 'custom_cms_name' => env('STATAMIC_CUSTOM_CMS_NAME', 'Statamic'), 119 | 120 | 'custom_logo_url' => env('STATAMIC_CUSTOM_LOGO_URL', null), 121 | 122 | 'custom_favicon_url' => env('STATAMIC_CUSTOM_FAVICON_URL', null), 123 | 124 | 'custom_css_url' => env('STATAMIC_CUSTOM_CSS_URL', null), 125 | 126 | ]; 127 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/editions.php: -------------------------------------------------------------------------------- 1 | false, 6 | 7 | 'addons' => [ 8 | // 9 | ], 10 | 11 | ]; 12 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/forms.php: -------------------------------------------------------------------------------- 1 | resource_path('forms'), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Submissions Path 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Where your form submissions are stored. 22 | | 23 | */ 24 | 25 | 'submissions' => storage_path('forms'), 26 | 27 | ]; 28 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/git.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_GIT_ENABLED', false), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Automatically Run 23 | |-------------------------------------------------------------------------- 24 | | 25 | | By default, commits are automatically queued when `Saved` or `Deleted` 26 | | events are fired. If you prefer users to manually trigger commits 27 | | using the `Git` utility interface, you may set this to `false`. 28 | | 29 | | https://statamic.dev/git-automation#committing-changes 30 | | 31 | */ 32 | 33 | 'automatic' => env('STATAMIC_GIT_AUTOMATIC', true), 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Queue Connection 38 | |-------------------------------------------------------------------------- 39 | | 40 | | You may choose which queue connection should be used when dispatching 41 | | commit jobs. Unless specified, the default connection will be used. 42 | | 43 | | https://statamic.dev/git-automation#queueing-commits 44 | | 45 | */ 46 | 47 | 'queue_connection' => env('STATAMIC_GIT_QUEUE_CONNECTION'), 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Dispatch Delay 52 | |-------------------------------------------------------------------------- 53 | | 54 | | When `Saved` and `Deleted` events queue up commits, you may wish to 55 | | set a delay time in minutes for each queued job. This can allow 56 | | for more consolidated commits when you have multiple users 57 | | making simultaneous content changes to your repository. 58 | | 59 | | Note: Not supported by default `sync` queue driver. 60 | | 61 | */ 62 | 63 | 'dispatch_delay' => env('STATAMIC_GIT_DISPATCH_DELAY', 0), 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Git User 68 | |-------------------------------------------------------------------------- 69 | | 70 | | The git user that will be used when committing changes. By default, it 71 | | will attempt to commit with the authenticated user's name and email 72 | | when possible, falling back to the below user when not available. 73 | | 74 | | https://statamic.dev/git-automation#git-user 75 | | 76 | */ 77 | 78 | 'use_authenticated' => true, 79 | 80 | 'user' => [ 81 | 'name' => env('STATAMIC_GIT_USER_NAME', 'Spock'), 82 | 'email' => env('STATAMIC_GIT_USER_EMAIL', 'spock@example.com'), 83 | ], 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Tracked Paths 88 | |-------------------------------------------------------------------------- 89 | | 90 | | Define the tracked paths to be considered when staging changes. Default 91 | | stache and file locations are already set up for you, but feel free 92 | | to modify these paths to suit your storage config. Referencing 93 | | absolute paths to external repos is also completely valid. 94 | | 95 | */ 96 | 97 | 'paths' => [ 98 | base_path('content'), 99 | base_path('users'), 100 | resource_path('blueprints'), 101 | resource_path('fieldsets'), 102 | resource_path('forms'), 103 | resource_path('users'), 104 | storage_path('forms'), 105 | ], 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | Git Binary 110 | |-------------------------------------------------------------------------- 111 | | 112 | | By default, Statamic will try to use the "git" command, but you can set 113 | | an absolute path to the git binary if necessary for your environment. 114 | | 115 | */ 116 | 117 | 'binary' => env('STATAMIC_GIT_BINARY', 'git'), 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Commands 122 | |-------------------------------------------------------------------------- 123 | | 124 | | Define a list commands to be run when Statamic is ready to `git add` 125 | | and `git commit` your changes. These commands will be run once 126 | | per repo, attempting to consolidate commits where possible. 127 | | 128 | | https://statamic.dev/git-automation#customizing-commits 129 | | 130 | */ 131 | 132 | 'commands' => [ 133 | 'git add {{ paths }}', 134 | 'git -c "user.name={{ name }}" -c "user.email={{ email }}" commit -m "{{ message }}"', 135 | ], 136 | 137 | /* 138 | |-------------------------------------------------------------------------- 139 | | Push 140 | |-------------------------------------------------------------------------- 141 | | 142 | | Determine whether `git push` should be run after the commands above 143 | | have finished. This is disabled by default, but can be enabled 144 | | globally, or per environment using the provided variable. 145 | | 146 | | https://statamic.dev/git-automation#pushing-changes 147 | | 148 | */ 149 | 150 | 'push' => env('STATAMIC_GIT_PUSH', false), 151 | 152 | /* 153 | |-------------------------------------------------------------------------- 154 | | Ignored Events 155 | |-------------------------------------------------------------------------- 156 | | 157 | | Statamic will listen on all `Saved` and `Deleted` events, as well 158 | | as any events registered by installed addons. If you wish to 159 | | ignore any specific events, you may reference them here. 160 | | 161 | */ 162 | 163 | 'ignored_events' => [ 164 | // \Statamic\Events\UserSaved::class, 165 | // \Statamic\Events\UserDeleted::class, 166 | ], 167 | 168 | /* 169 | |-------------------------------------------------------------------------- 170 | | Locale 171 | |-------------------------------------------------------------------------- 172 | | 173 | | The locale to be used when translating commit messages, etc. By 174 | | default, the authenticated user's locale will be used, but 175 | | feel free to override this using the provided variable. 176 | | 177 | */ 178 | 179 | 'locale' => env('STATAMIC_GIT_LOCALE', null), 180 | 181 | ]; 182 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/graphql.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_GRAPHQL_ENABLED', false), 18 | 19 | 'resources' => [ 20 | 'collections' => false, 21 | 'navs' => false, 22 | 'taxonomies' => false, 23 | 'assets' => false, 24 | 'globals' => false, 25 | 'forms' => false, 26 | 'sites' => false, 27 | 'users' => false, 28 | ], 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Queries 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Here you may list queries to be added to the Statamic schema. 36 | | 37 | | https://statamic.dev/graphql#custom-queries 38 | | 39 | */ 40 | 41 | 'queries' => [ 42 | // 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Middleware 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Here you may list middleware to be added to the Statamic schema. 51 | | 52 | | https://statamic.dev/graphql#custom-middleware 53 | | 54 | */ 55 | 56 | 'middleware' => [ 57 | // 58 | ], 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Caching 63 | |-------------------------------------------------------------------------- 64 | | 65 | | By default, Statamic will cache each request until the specified 66 | | expiry, or until content is changed. See the documentation for 67 | | more details on how to customize your cache implementation. 68 | | 69 | | https://statamic.dev/graphql#caching 70 | | 71 | */ 72 | 73 | 'cache' => [ 74 | 'expiry' => 60, 75 | ], 76 | 77 | ]; 78 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/live_preview.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'Laptop' => ['width' => 1440, 'height' => 900], 17 | 'Tablet' => ['width' => 1024, 'height' => 786], 18 | 'Mobile' => ['width' => 375, 'height' => 812], 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Additional Inputs 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Additional fields may be added to the Live Preview header bar. You 27 | | may define a list of Vue components to be injected. Their values 28 | | will be added to the cascade on the front-end for you to use. 29 | | 30 | */ 31 | 32 | 'inputs' => [ 33 | // 34 | ], 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/oauth.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_OAUTH_ENABLED', false), 6 | 7 | 'email_login_enabled' => true, 8 | 9 | 'providers' => [ 10 | // 'github', 11 | ], 12 | 13 | 'routes' => [ 14 | 'login' => 'oauth/{provider}', 15 | 'callback' => 'oauth/{provider}/callback', 16 | ], 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Remember Me 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Whether or not the "remember me" functionality should be used when 24 | | authenticating using OAuth. When enabled, the user will remain 25 | | logged in indefinitely, or until they manually log out. 26 | | 27 | */ 28 | 29 | 'remember_me' => true, 30 | 31 | ]; 32 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/protect.php: -------------------------------------------------------------------------------- 1 | null, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Protection Schemes 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may define all of the protection schemes for your application 24 | | as well as their drivers. You may even define multiple schemes for 25 | | the same driver to easily protect different types of pages. 26 | | 27 | | Supported drivers: "ip_address", "auth", "password" 28 | | 29 | */ 30 | 31 | 'schemes' => [ 32 | 33 | 'ip_address' => [ 34 | 'driver' => 'ip_address', 35 | 'allowed' => ['127.0.0.1'], 36 | ], 37 | 38 | 'logged_in' => [ 39 | 'driver' => 'auth', 40 | 'login_url' => '/login', 41 | 'append_redirect' => true, 42 | ], 43 | 44 | 'password' => [ 45 | 'driver' => 'password', 46 | 'allowed' => ['secret'], 47 | 'form_url' => null, 48 | ], 49 | 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/revisions.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_REVISIONS_ENABLED', false), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Storage Path 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This is the directory where your revision files will be located. Within 24 | | here, they will be further organized into collection, site, ID, etc. 25 | | 26 | */ 27 | 28 | 'path' => storage_path('statamic/revisions'), 29 | 30 | ]; 31 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/routes.php: -------------------------------------------------------------------------------- 1 | true, 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Action Route Prefix 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Some extensions may provide routes that go through the frontend of your 25 | | website. These URLs begin with the following prefix. We've chosen an 26 | | unobtrusive default but you are free to select whatever you want. 27 | | 28 | */ 29 | 30 | 'action' => '!', 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Middleware 35 | |-------------------------------------------------------------------------- 36 | | 37 | | Define the middleware that will be applied to the web route group. 38 | | 39 | */ 40 | 41 | 'middleware' => 'web', 42 | 43 | ]; 44 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/search.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_DEFAULT_SEARCH_INDEX', 'default'), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Search Indexes 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Here you can define all of the available search indexes. 23 | | 24 | */ 25 | 26 | 'indexes' => [ 27 | 28 | 'default' => [ 29 | 'driver' => 'local', 30 | 'searchables' => 'all', 31 | 'fields' => ['title'], 32 | ], 33 | 34 | // 'blog' => [ 35 | // 'driver' => 'local', 36 | // 'searchables' => 'collection:blog', 37 | // ], 38 | 39 | ], 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Driver Defaults 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Here you can specify default configuration to be applied to all indexes 47 | | that use the corresponding driver. For instance, if you have two 48 | | indexes that use the "local" driver, both of them can have the 49 | | same base configuration. You may override for each index. 50 | | 51 | */ 52 | 53 | 'drivers' => [ 54 | 55 | 'local' => [ 56 | 'path' => storage_path('statamic/search'), 57 | ], 58 | 59 | 'algolia' => [ 60 | 'credentials' => [ 61 | 'id' => env('ALGOLIA_APP_ID', ''), 62 | 'secret' => env('ALGOLIA_SECRET', ''), 63 | ], 64 | ], 65 | 66 | ], 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Search Defaults 71 | |-------------------------------------------------------------------------- 72 | | 73 | | Here you can specify default configuration to be applied to all indexes 74 | | regardless of the driver. You can override these per driver or per index. 75 | | 76 | */ 77 | 78 | 'defaults' => [ 79 | 'fields' => ['title'], 80 | ], 81 | 82 | ]; 83 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/sites.php: -------------------------------------------------------------------------------- 1 | [ 17 | 18 | 'default' => [ 19 | 'name' => config('app.name'), 20 | 'locale' => 'en_US', 21 | 'url' => '/', 22 | ], 23 | 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/stache.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_STACHE_WATCHER', false), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may configure the stores that are used inside the Stache. 26 | | 27 | | https://statamic.dev/stache#stores 28 | | 29 | */ 30 | 31 | 'stores' => [ 32 | 33 | 'taxonomies' => [ 34 | 'class' => Stores\TaxonomiesStore::class, 35 | 'directory' => base_path('content/taxonomies'), 36 | ], 37 | 38 | 'terms' => [ 39 | 'class' => Stores\TermsStore::class, 40 | 'directory' => base_path('content/taxonomies'), 41 | ], 42 | 43 | 'collections' => [ 44 | 'class' => Stores\CollectionsStore::class, 45 | 'directory' => base_path('content/collections'), 46 | ], 47 | 48 | 'entries' => [ 49 | 'class' => Stores\EntriesStore::class, 50 | 'directory' => base_path('content/collections'), 51 | ], 52 | 53 | 'navigation' => [ 54 | 'class' => Stores\NavigationStore::class, 55 | 'directory' => base_path('content/navigation'), 56 | ], 57 | 58 | 'collection-trees' => [ 59 | 'class' => Stores\CollectionTreeStore::class, 60 | 'directory' => base_path('content/trees/collections'), 61 | ], 62 | 63 | 'nav-trees' => [ 64 | 'class' => Stores\NavTreeStore::class, 65 | 'directory' => base_path('content/trees/navigation'), 66 | ], 67 | 68 | 'globals' => [ 69 | 'class' => Stores\GlobalsStore::class, 70 | 'directory' => base_path('content/globals'), 71 | ], 72 | 73 | 'asset-containers' => [ 74 | 'class' => Stores\AssetContainersStore::class, 75 | 'directory' => base_path('content/assets'), 76 | ], 77 | 78 | 'assets' => [ 79 | 'class' => Stores\AssetsStore::class, 80 | ], 81 | 82 | 'users' => [ 83 | 'class' => Stores\UsersStore::class, 84 | 'directory' => base_path('users'), 85 | ], 86 | 87 | ], 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Indexes 92 | |-------------------------------------------------------------------------- 93 | | 94 | | Here you may define any additional indexes that will be inherited 95 | | by each store in the Stache. You may also define indexes on a 96 | | per-store level by adding an "indexes" key to its config. 97 | | 98 | */ 99 | 100 | 'indexes' => [ 101 | // 102 | ], 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | Locking 107 | |-------------------------------------------------------------------------- 108 | | 109 | | In order to prevent concurrent requests from updating the Stache at 110 | | the same and wasting resources, it will be "locked" so subsequent 111 | | requests will have to wait until the first has been completed. 112 | | 113 | | https://statamic.dev/stache#locks 114 | | 115 | */ 116 | 117 | 'lock' => [ 118 | 'enabled' => true, 119 | 'timeout' => 30, 120 | ], 121 | 122 | ]; 123 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/static_caching.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_STATIC_CACHING_STRATEGY', null), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Caching Strategies 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Here you may define all of the static caching strategies for your 23 | | application as well as their drivers. 24 | | 25 | | Supported drivers: "application", "file" 26 | | 27 | */ 28 | 29 | 'strategies' => [ 30 | 31 | 'half' => [ 32 | 'driver' => 'application', 33 | 'expiry' => null, 34 | ], 35 | 36 | 'full' => [ 37 | 'driver' => 'file', 38 | 'path' => public_path('static'), 39 | 'lock_hold_length' => 0, 40 | ], 41 | 42 | ], 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Exclusions 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Here you may define a list of URLs to be excluded from static 50 | | caching. You may want to exclude URLs containing dynamic 51 | | elements like contact forms, or shopping carts. 52 | | 53 | */ 54 | 55 | 'exclude' => [ 56 | // 57 | ], 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Invalidation Rules 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Here you may define the rules that trigger when and how content would be 65 | | flushed from the static cache. See the documentation for more details. 66 | | If a custom class is not defined, the default invalidator is used. 67 | | 68 | | https://statamic.dev/static-caching 69 | | 70 | */ 71 | 72 | 'invalidation' => [ 73 | 74 | 'class' => null, 75 | 76 | 'rules' => [ 77 | // 78 | ], 79 | 80 | ], 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | Ignoring Query Strings 85 | |-------------------------------------------------------------------------- 86 | | 87 | | Statamic will cache pages of the same URL but with different query 88 | | parameters separately. This is useful for pages with pagination. 89 | | If you'd like to ignore the query strings, you may do so. 90 | | 91 | */ 92 | 93 | 'ignore_query_strings' => false, 94 | 95 | ]; 96 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/system.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_LICENSE_KEY'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Default Addons Paths 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When generating addons via `php please make:addon`, this path will be 25 | | used by default. You can still specify custom repository paths in 26 | | your composer.json, but this is the path used by the generator. 27 | | 28 | */ 29 | 30 | 'addons_path' => base_path('addons'), 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Send the Powered-By Header 35 | |-------------------------------------------------------------------------- 36 | | 37 | | Websites like builtwith.com use the X-Powered-By header to determine 38 | | what technologies are used on a particular site. By default, we'll 39 | | send this header, but you are absolutely allowed to disable it. 40 | | 41 | */ 42 | 43 | 'send_powered_by_header' => true, 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Date Format 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Whenever a Carbon date is cast to a string on front-end routes, it will 51 | | use this format. On CP routes, the format defined in cp.php is used. 52 | | You can customize this format using PHP's date string constants. 53 | | Setting this value to null will use Carbon's default format. 54 | | 55 | | https://www.php.net/manual/en/function.date.php 56 | | 57 | */ 58 | 59 | 'date_format' => 'F jS, Y', 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Default Character Set 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Statamic will use this character set when performing specific string 67 | | encoding and decoding operations; This does not apply everywhere. 68 | | 69 | */ 70 | 71 | 'charset' => 'UTF-8', 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Track Last Update 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Statamic will automatically set an `updated_at` timestamp (along with 79 | | `updated_by`, where applicable) when specific content is updated. 80 | | In some situations, you may wish disable this functionality. 81 | | 82 | */ 83 | 84 | 'track_last_update' => true, 85 | 86 | /* 87 | |-------------------------------------------------------------------------- 88 | | Enable Cache Tags 89 | |-------------------------------------------------------------------------- 90 | | 91 | | Sometimes you'll want to be able to disable the {{ cache }} tags in 92 | | Antlers, so here is where you can do that. Otherwise, it will be 93 | | enabled all the time. 94 | | 95 | */ 96 | 97 | 'cache_tags_enabled' => env('STATAMIC_CACHE_TAGS_ENABLED', true), 98 | 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Intensive Operations 102 | |-------------------------------------------------------------------------- 103 | | 104 | | Sometimes Statamic requires extra resources to complete intensive 105 | | operations. Here you may configure system resource limits for 106 | | those rare times when we need to turn things up to eleven! 107 | | 108 | */ 109 | 110 | 'php_memory_limit' => '-1', 111 | 'php_max_execution_time' => '-1', 112 | 'ajax_timeout' => '600000', 113 | 'pcre_backtrack_limit' => '-1', 114 | 115 | ]; 116 | -------------------------------------------------------------------------------- /tests/__fixtures__/config/users.php: -------------------------------------------------------------------------------- 1 | 'eloquent', 19 | 20 | 'repositories' => [ 21 | 22 | 'file' => [ 23 | 'driver' => 'file', 24 | 'paths' => [ 25 | 'users' => base_path('users'), 26 | 'roles' => resource_path('users/roles.yaml'), 27 | 'groups' => resource_path('users/groups.yaml'), 28 | ], 29 | ], 30 | 31 | 'eloquent' => [ 32 | 'driver' => 'eloquent', 33 | ], 34 | 35 | ], 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Avatars 40 | |-------------------------------------------------------------------------- 41 | | 42 | | User avatars are initials by default, with custom options for services 43 | | like Gravatar.com. 44 | | 45 | | Supported: "initials", "gravatar", or a custom class name. 46 | | 47 | */ 48 | 49 | 'avatars' => 'initials', 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | New User Roles 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When registering new users through the user:register_form tag, these 57 | | roles will automatically be applied to your newly created users. 58 | | 59 | */ 60 | 61 | 'new_user_roles' => [ 62 | // 63 | ], 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | New User Groups 68 | |-------------------------------------------------------------------------- 69 | | 70 | | When registering new users through the user:register_form tag, these 71 | | groups will automatically be applied to your newly created users. 72 | | 73 | */ 74 | 75 | 'new_user_groups' => [ 76 | // 77 | ], 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Password Brokers 82 | |-------------------------------------------------------------------------- 83 | | 84 | | When resetting passwords, Statamic uses an appropriate password broker. 85 | | Here you may define which broker should be used for each situation. 86 | | You may want a longer expiry for user activations, for example. 87 | | 88 | */ 89 | 90 | 'passwords' => [ 91 | 'resets' => config('auth.defaults.passwords'), 92 | 'activations' => config('auth.defaults.passwords'), 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Database 98 | |-------------------------------------------------------------------------- 99 | | 100 | | Here you may configure the database connection and its table names. 101 | | 102 | */ 103 | 104 | 'database' => config('database.default'), 105 | 106 | 'tables' => [ 107 | 'users' => 'users', 108 | 'role_user' => 'role_user', 109 | 'group_user' => 'group_user', 110 | ], 111 | 112 | /* 113 | |-------------------------------------------------------------------------- 114 | | Authentication Guards 115 | |-------------------------------------------------------------------------- 116 | | 117 | | By default, Statamic will use the `web` authentication guard. However, 118 | | if you want to run Statamic alongside the default Laravel auth 119 | | guard, you can configure that for your cp and/or frontend. 120 | | 121 | */ 122 | 123 | 'guards' => [ 124 | 'cp' => 'web', 125 | 'web' => 'web', 126 | ], 127 | 128 | ]; 129 | -------------------------------------------------------------------------------- /tests/__fixtures__/content/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stillat/relationships/af96e50e0b618ff1d8eca6562662409e9d99b2f6/tests/__fixtures__/content/assets/.gitkeep -------------------------------------------------------------------------------- /tests/__fixtures__/content/collections/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stillat/relationships/af96e50e0b618ff1d8eca6562662409e9d99b2f6/tests/__fixtures__/content/collections/.gitkeep -------------------------------------------------------------------------------- /tests/__fixtures__/content/globals/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stillat/relationships/af96e50e0b618ff1d8eca6562662409e9d99b2f6/tests/__fixtures__/content/globals/.gitkeep -------------------------------------------------------------------------------- /tests/__fixtures__/content/structures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stillat/relationships/af96e50e0b618ff1d8eca6562662409e9d99b2f6/tests/__fixtures__/content/structures/.gitkeep -------------------------------------------------------------------------------- /tests/__fixtures__/content/taxonomies/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stillat/relationships/af96e50e0b618ff1d8eca6562662409e9d99b2f6/tests/__fixtures__/content/taxonomies/.gitkeep -------------------------------------------------------------------------------- /tests/__fixtures__/dev-null/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stillat/relationships/af96e50e0b618ff1d8eca6562662409e9d99b2f6/tests/__fixtures__/dev-null/.gitkeep -------------------------------------------------------------------------------- /tests/__fixtures__/users/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stillat/relationships/af96e50e0b618ff1d8eca6562662409e9d99b2f6/tests/__fixtures__/users/.gitkeep --------------------------------------------------------------------------------