diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index 9f017a6ad33..2bf3fd584a9 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -1116,7 +1116,7 @@ public function hasDuration() public function getQueryableValue(string $field) { - if (method_exists($this, $method = Str::camel($field))) { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { return $this->{$method}(); } @@ -1129,6 +1129,17 @@ public function getQueryableValue(string $field) return $field->fieldtype()->toQueryableValue($value); } + private function queryableMethods(): array + { + return [ + 'absoluteUrl', 'apiUrl', 'basename', 'blueprint', 'containerId', 'containerHandle', 'dimensions', + 'duration', 'editUrl', 'exists', 'extension', 'filename', 'folder', 'guessedExtension', + 'hasDimensions', 'hasDuration', 'height', 'id', 'isAudio', 'isImage', 'isMedia', 'isPdf', + 'isPreviewable', 'isSvg', 'isVideo', 'lastModified', 'mimeType', 'orientation', 'path', 'pdfUrl', + 'ratio', 'reference', 'size', 'thumbnailUrl', 'title', 'url', 'width', + ]; + } + public function getCurrentDirtyStateAttributes(): array { return array_merge([ diff --git a/src/Assets/AssetContainer.php b/src/Assets/AssetContainer.php index e73fc1d3377..2a555a0af12 100644 --- a/src/Assets/AssetContainer.php +++ b/src/Assets/AssetContainer.php @@ -7,6 +7,7 @@ use Statamic\Contracts\Assets\AssetContainer as AssetContainerContract; use Statamic\Contracts\Data\Augmentable; use Statamic\Contracts\Data\Augmented; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Data\ExistsAsFile; use Statamic\Data\HasAugmentedInstance; use Statamic\Events\AssetContainerBlueprintFound; @@ -27,9 +28,10 @@ use Statamic\Facades\Stache; use Statamic\Facades\URL; use Statamic\Support\Arr; +use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -class AssetContainer implements Arrayable, ArrayAccess, AssetContainerContract, Augmentable +class AssetContainer implements Arrayable, ArrayAccess, AssetContainerContract, Augmentable, ContainsQueryableValues { use ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance; @@ -693,6 +695,24 @@ public static function __callStatic($method, $parameters) return Facades\AssetContainer::{$method}(...$parameters); } + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return null; + } + + private function queryableMethods(): array + { + return [ + 'absoluteUrl', 'accessible', 'allowDownloading', 'allowMoving', 'allowRenaming', 'allowUploads', + 'blueprint', 'createFolders', 'diskHandle', 'diskPath', 'editUrl', 'handle', 'hasSearchIndex', + 'id', 'path', 'private', 'searchIndex', 'showUrl', 'sortDirection', 'sortField', 'title', 'url', + ]; + } + public function __toString() { return $this->handle(); diff --git a/src/Assets/AssetFolder.php b/src/Assets/AssetFolder.php index 794c9f72085..e190d9f1b13 100644 --- a/src/Assets/AssetFolder.php +++ b/src/Assets/AssetFolder.php @@ -6,6 +6,7 @@ use League\Flysystem\PathTraversalDetected; use Statamic\Assets\AssetUploader as Uploader; use Statamic\Contracts\Assets\AssetFolder as Contract; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Events\AssetFolderDeleted; use Statamic\Events\AssetFolderSaved; use Statamic\Facades\AssetContainer; @@ -13,7 +14,7 @@ use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -class AssetFolder implements Arrayable, Contract +class AssetFolder implements Arrayable, ContainsQueryableValues, Contract { use FluentlyGetsAndSets; @@ -237,4 +238,20 @@ public function toArray() 'basename' => (string) $this->basename(), ]; } + + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return null; + } + + private function queryableMethods(): array + { + return [ + 'basename', 'count', 'lastModified', 'path', 'resolvedPath', 'size', 'title', + ]; + } } diff --git a/src/Auth/User.php b/src/Auth/User.php index 86d935d6cff..e3cd7dfda80 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -365,7 +365,7 @@ protected function getComputedCallbacks() public function getQueryableValue(string $field) { - if (method_exists($this, $method = Str::camel($field))) { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { return $this->{$method}(); } @@ -377,4 +377,13 @@ public function getQueryableValue(string $field) return $field->fieldtype()->toQueryableValue($value); } + + private function queryableMethods(): array + { + return [ + 'apiUrl', 'avatar', 'blueprint', 'editUrl', 'email', 'gravatarUrl', 'groups', 'hasAvatarField', + 'id', 'initials', 'isSuper', 'isTaxonomizable', 'lastLogin', 'name', 'path', 'preferredLocale', + 'preferredTheme', 'reference', 'roles', 'title', + ]; + } } diff --git a/src/Entries/Collection.php b/src/Entries/Collection.php index 179389248e2..b66dd9b9c68 100644 --- a/src/Entries/Collection.php +++ b/src/Entries/Collection.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use Statamic\Contracts\Data\Augmentable as AugmentableContract; use Statamic\Contracts\Entries\Collection as Contract; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Data\ContainsCascadingData; use Statamic\Data\ExistsAsFile; use Statamic\Data\HasAugmentedData; @@ -35,7 +36,7 @@ use function Statamic\trans as __; -class Collection implements Arrayable, ArrayAccess, AugmentableContract, Contract +class Collection implements Arrayable, ArrayAccess, AugmentableContract, ContainsQueryableValues, Contract { use ContainsCascadingData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedData, HasDirtyState; @@ -948,6 +949,24 @@ public function augmentedArrayData() ]; } + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return null; + } + + private function queryableMethods(): array + { + return [ + 'dated', 'defaultPublishState', 'editUrl', 'handle', 'hasStructure', 'id', 'layout', 'mount', + 'orderable', 'path', 'requiresSlugs', 'revisionsEnabled', 'sites', 'sortDirection', 'sortField', + 'structureHandle', 'taxonomies', 'template', 'title', 'url', 'uri', + ]; + } + public function getCurrentDirtyStateAttributes(): array { return $this->fileData(); diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 9e0c7b0d4a2..406cd62d75a 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -1098,7 +1098,7 @@ public function getQueryableValue(string $field) Blink::store('entry-uris')->forget($this->id()); } - if (method_exists($this, $method = Str::camel($field))) { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { return $this->{$method}(); } @@ -1111,6 +1111,16 @@ public function getQueryableValue(string $field) return $field->fieldtype()->toQueryableValue($value); } + private function queryableMethods(): array + { + return [ + 'apiUrl', 'blueprint', 'collection', 'collectionHandle', 'date', 'editUrl', 'hasDate', 'hasExplicitDate', 'hasOrigin', + 'hasSeconds', 'hasStructure', 'hasTime', 'id', 'isRedirect', 'isRoot', 'lastModified', 'lastModifiedBy', + 'layout', 'locale', 'order', 'path', 'private', 'published', 'redirectUrl', 'reference', 'site', 'sites', 'slug', + 'status', 'template', 'uri', 'url', 'urlWithoutRedirect', + ]; + } + public function getSearchValue(string $field) { return method_exists($this, $field) ? $this->$field() : $this->value($field); diff --git a/src/Fields/Blueprint.php b/src/Fields/Blueprint.php index 616f2e4fec0..d72863dddcb 100644 --- a/src/Fields/Blueprint.php +++ b/src/Fields/Blueprint.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Collection; use Statamic\Contracts\Data\Augmentable; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Contracts\Query\QueryableValue; use Statamic\CP\Column; use Statamic\CP\Columns; @@ -30,7 +31,7 @@ use function Statamic\trans as __; -class Blueprint implements Arrayable, ArrayAccess, Augmentable, QueryableValue +class Blueprint implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableValues, QueryableValue { use ExistsAsFile, HasAugmentedData; @@ -792,4 +793,21 @@ public function writeFile($path = null) { File::put($path ?? $this->buildPath(), $this->fileContents()); } + + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return null; + } + + private function queryableMethods(): array + { + return [ + 'columns', 'fields', 'handle', 'hidden', 'isEmpty', 'isDeletable', 'isResettable', + 'namespace', 'order', 'path', 'tabs', 'title', + ]; + } } diff --git a/src/Fields/Fieldset.php b/src/Fields/Fieldset.php index 44e43ddd9e7..7dd20fcb88f 100644 --- a/src/Fields/Fieldset.php +++ b/src/Fields/Fieldset.php @@ -2,6 +2,7 @@ namespace Statamic\Fields; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Events\FieldsetCreated; use Statamic\Events\FieldsetCreating; use Statamic\Events\FieldsetDeleted; @@ -21,7 +22,7 @@ use Statamic\Support\Arr; use Statamic\Support\Str; -class Fieldset +class Fieldset implements ContainsQueryableValues { protected $handle; protected $contents = []; @@ -296,6 +297,23 @@ public function reset() return true; } + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return null; + } + + private function queryableMethods(): array + { + return [ + 'editUrl', 'fields', 'handle', 'isDeletable', 'isResettable', + 'namespace', 'path', 'title', + ]; + } + public static function __callStatic($method, $parameters) { return Facades\Fieldset::{$method}(...$parameters); diff --git a/src/Forms/Form.php b/src/Forms/Form.php index d093537ead2..6257703dbfe 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -8,6 +8,7 @@ use Statamic\Contracts\Forms\Form as FormContract; use Statamic\Contracts\Forms\Submission; use Statamic\Contracts\Forms\SubmissionQueryBuilder; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Data\ContainsData; use Statamic\Data\HasAugmentedInstance; use Statamic\Events\FormBlueprintFound; @@ -26,9 +27,10 @@ use Statamic\Forms\Exporters\Exporter; use Statamic\Statamic; use Statamic\Support\Arr; +use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -class Form implements Arrayable, Augmentable, FormContract +class Form implements Arrayable, Augmentable, ContainsQueryableValues, FormContract { use ContainsData, FluentlyGetsAndSets, HasAugmentedInstance; @@ -444,4 +446,27 @@ public function exporter(string $handle): ?Exporter { return $this->exporters()->get($handle); } + + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + $value = $this->get($field); + + if (! $field = $this->blueprint()->field($field)) { + return $value; + } + + return $field->fieldtype()->toQueryableValue($value); + } + + private function queryableMethods(): array + { + return [ + 'actionUrl', 'apiUrl', 'blueprint', 'dateFormat', 'editUrl', 'fields', + 'handle', 'honeypot', 'path', 'submissions', 'title', + ]; + } } diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php index c6fdc03a418..80c7e5c6028 100644 --- a/src/Forms/Submission.php +++ b/src/Forms/Submission.php @@ -5,6 +5,7 @@ use Carbon\Carbon; use Statamic\Contracts\Data\Augmentable; use Statamic\Contracts\Forms\Submission as SubmissionContract; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Data\ContainsData; use Statamic\Data\ExistsAsFile; use Statamic\Data\HasAugmentedData; @@ -21,9 +22,10 @@ use Statamic\Facades\Stache; use Statamic\Forms\Uploaders\AssetsUploader; use Statamic\Forms\Uploaders\FilesUploader; +use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -class Submission implements Augmentable, SubmissionContract +class Submission implements Augmentable, ContainsQueryableValues, SubmissionContract { use ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedData, TracksQueriedColumns, TracksQueriedRelations; @@ -274,6 +276,28 @@ public function fileData() return $this->data()->all(); } + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + $value = $this->get($field); + + if (! $field = $this->blueprint()->field($field)) { + return $value; + } + + return $field->fieldtype()->toQueryableValue($value); + } + + private function queryableMethods(): array + { + return [ + 'blueprint', 'date', 'form', 'formattedDate', 'id', 'path', + ]; + } + public function __get($key) { return $this->get($key); diff --git a/src/Globals/GlobalSet.php b/src/Globals/GlobalSet.php index d248e1b55f9..ba8050226a5 100644 --- a/src/Globals/GlobalSet.php +++ b/src/Globals/GlobalSet.php @@ -4,6 +4,7 @@ use Statamic\Contracts\Globals\GlobalSet as Contract; use Statamic\Contracts\Globals\Variables; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Data\ExistsAsFile; use Statamic\Events\GlobalSetCreated; use Statamic\Events\GlobalSetCreating; @@ -18,9 +19,10 @@ use Statamic\Facades\Site; use Statamic\Facades\Stache; use Statamic\Support\Arr; +use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -class GlobalSet implements Contract +class GlobalSet implements ContainsQueryableValues, Contract { use ExistsAsFile, FluentlyGetsAndSets; @@ -244,6 +246,22 @@ public function deleteUrl() return cp_route('globals.destroy', $this->handle()); } + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return null; + } + + private function queryableMethods(): array + { + return [ + 'blueprint', 'editUrl', 'handle', 'id', 'localizations', 'path', 'sites', 'title', + ]; + } + public static function __callStatic($method, $parameters) { return Facades\GlobalSet::{$method}(...$parameters); diff --git a/src/Globals/Variables.php b/src/Globals/Variables.php index 8d7992f4467..afb5ce94066 100644 --- a/src/Globals/Variables.php +++ b/src/Globals/Variables.php @@ -10,6 +10,7 @@ use Statamic\Contracts\Globals\GlobalSet; use Statamic\Contracts\Globals\Variables as Contract; use Statamic\Contracts\GraphQL\ResolvesValues as ResolvesValuesContract; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Data\ContainsData; use Statamic\Data\ExistsAsFile; use Statamic\Data\HasAugmentedInstance; @@ -27,9 +28,10 @@ use Statamic\Facades\Site; use Statamic\Facades\Stache; use Statamic\GraphQL\ResolvesValues; +use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -class Variables implements Arrayable, ArrayAccess, Augmentable, Contract, Localization, ResolvesValuesContract +class Variables implements Arrayable, ArrayAccess, Augmentable, ContainsQueryableValues, Contract, Localization, ResolvesValuesContract { use ContainsData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedInstance, HasOrigin, ResolvesValues, TracksQueriedRelations; @@ -281,4 +283,26 @@ public function fresh() { return Facades\GlobalSet::find($this->handle())->in($this->locale); } + + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + $value = $this->value($field); + + if (! $field = $this->blueprint()->field($field)) { + return $value; + } + + return $field->fieldtype()->toQueryableValue($value); + } + + private function queryableMethods(): array + { + return [ + 'blueprint', 'editUrl', 'handle', 'id', 'locale', 'path', 'reference', 'site', 'sites', 'title', + ]; + } } diff --git a/src/Http/Controllers/CP/Assets/BrowserController.php b/src/Http/Controllers/CP/Assets/BrowserController.php index 47dbe8f144a..bd50a5375b7 100644 --- a/src/Http/Controllers/CP/Assets/BrowserController.php +++ b/src/Http/Controllers/CP/Assets/BrowserController.php @@ -98,7 +98,7 @@ public function folder(Request $request, $container, $path = '/') $sortByMethod = $order === 'desc' ? 'sortByDesc' : 'sortBy'; $folders = $folders->$sortByMethod( - fn (AssetFolder $folder) => method_exists($folder, $sort) ? $folder->$sort() : $folder->basename() + fn (AssetFolder $folder) => in_array($sort, ['basename', 'title', 'lastModified', 'size', 'count', 'path']) ? $folder->$sort() : $folder->basename() ); $folders = $folders->slice(($page - 1) * $perPage, $perPage); diff --git a/src/Query/ResolveValue.php b/src/Query/ResolveValue.php index 09a10392a26..052d2240f90 100644 --- a/src/Query/ResolveValue.php +++ b/src/Query/ResolveValue.php @@ -9,6 +9,12 @@ class ResolveValue { + private array $denylist = [ + 'delete', 'deleteFile', 'deleteQuietly', + 'destroy', 'forceDelete', 'save', 'saveQuietly', + 'truncate', 'update', 'updateQuietly', 'write', 'writeFile', + ]; + public function __invoke($item, $name) { if (Str::startsWith($name, 'data->')) { @@ -52,7 +58,7 @@ private function getItemPartValue($item, $name) return $item->getQueryableValue($name); } - if (method_exists($item, $method = Str::camel($name))) { + if (method_exists($item, $method = Str::camel($name)) && ! in_array($method, $this->denylist)) { return $item->{$method}(); } diff --git a/src/Structures/Nav.php b/src/Structures/Nav.php index e6699a8ec21..208c674c18b 100644 --- a/src/Structures/Nav.php +++ b/src/Structures/Nav.php @@ -2,6 +2,7 @@ namespace Statamic\Structures; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Contracts\Structures\Nav as Contract; use Statamic\Contracts\Structures\NavTree; use Statamic\Contracts\Structures\NavTreeRepository; @@ -21,7 +22,7 @@ use Statamic\Facades\Stache; use Statamic\Support\Str; -class Nav extends Structure implements Contract +class Nav extends Structure implements ContainsQueryableValues, Contract { use ExistsAsFile; @@ -187,4 +188,21 @@ public function collectionsQueryScopes($scopes = null) }) ->args(func_get_args()); } + + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return null; + } + + private function queryableMethods(): array + { + return [ + 'blueprint', 'collections', 'editUrl', 'expectsRoot', 'handle', 'id', + 'maxDepth', 'path', 'showUrl', 'sites', 'title', 'trees', + ]; + } } diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 20653ce0a61..d65643370a9 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -13,6 +13,7 @@ use Statamic\Contracts\Data\BulkAugmentable; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\GraphQL\ResolvesValues as ResolvesValuesContract; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Contracts\Routing\UrlBuilder; use Statamic\Contracts\Structures\Nav; use Statamic\Data\ContainsSupplementalData; @@ -25,7 +26,7 @@ use Statamic\GraphQL\ResolvesValues; use Statamic\Support\Str; -class Page implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, Entry, JsonSerializable, Protectable, ResolvesValuesContract, Responsable +class Page implements Arrayable, ArrayAccess, Augmentable, BulkAugmentable, ContainsQueryableValues, Entry, JsonSerializable, Protectable, ResolvesValuesContract, Responsable { use ContainsSupplementalData, ForwardsCalls, HasAugmentedInstance, ResolvesValues, TracksQueriedColumns; @@ -493,6 +494,24 @@ public function __call($method, $args) return $this->forwardCallTo($this->entry(), $method, $args); } + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return $this->value($field); + } + + private function queryableMethods(): array + { + return [ + 'absoluteUrl', 'blueprint', 'collection', 'depth', 'editUrl', 'id', 'isRedirect', 'isRoot', + 'private', 'published', 'reference', 'route', 'site', 'slug', 'status', 'structure', + 'title', 'uri', 'url', 'urlWithoutRedirect', + ]; + } + #[\ReturnTypeWillChange] public function jsonSerialize() { diff --git a/src/Structures/Tree.php b/src/Structures/Tree.php index 1cbd7e1270e..4de7ccbef8b 100644 --- a/src/Structures/Tree.php +++ b/src/Structures/Tree.php @@ -3,6 +3,7 @@ namespace Statamic\Structures; use Statamic\Contracts\Data\Localization; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Contracts\Structures\Tree as Contract; use Statamic\Data\ExistsAsFile; use Statamic\Data\HasDirtyState; @@ -10,9 +11,10 @@ use Statamic\Facades\Entry; use Statamic\Facades\Site; use Statamic\Support\Arr; +use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -abstract class Tree implements Contract, Localization +abstract class Tree implements ContainsQueryableValues, Contract, Localization { use ExistsAsFile, FluentlyGetsAndSets, HasDirtyState; @@ -388,6 +390,22 @@ public function withEntries() return $this; } + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return null; + } + + private function queryableMethods(): array + { + return [ + 'editUrl', 'handle', 'locale', 'path', 'route', 'showUrl', 'site', + ]; + } + public function getCurrentDirtyStateAttributes(): array { return [ diff --git a/src/Taxonomies/LocalizedTerm.php b/src/Taxonomies/LocalizedTerm.php index c4ca9b0c04f..6c2cd6af72b 100644 --- a/src/Taxonomies/LocalizedTerm.php +++ b/src/Taxonomies/LocalizedTerm.php @@ -485,7 +485,7 @@ public function repository() public function getQueryableValue(string $field) { - if (method_exists($this, $method = Str::camel($field))) { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { return $this->{$method}(); } @@ -498,6 +498,16 @@ public function getQueryableValue(string $field) return $field->fieldtype()->toQueryableValue($value); } + private function queryableMethods(): array + { + return [ + 'absoluteUrl', 'apiUrl', 'blueprint', 'editUrl', 'entriesCount', 'hasOrigin', 'id', 'isRedirect', + 'isRoot', 'lastModified', 'lastModifiedBy', 'layout', 'locale', 'path', 'private', 'published', + 'redirectUrl', 'site', 'slug', 'status', 'taxonomy', 'taxonomyHandle', 'template', 'title', + 'uri', 'url', 'urlWithoutRedirect', 'values', + ]; + } + public function getCpSearchResultBadge() { return $this->taxonomy()->title(); diff --git a/src/Taxonomies/Taxonomy.php b/src/Taxonomies/Taxonomy.php index 597fcf79fc0..0acb6ee7096 100644 --- a/src/Taxonomies/Taxonomy.php +++ b/src/Taxonomies/Taxonomy.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Responsable; use Statamic\Contracts\Data\Augmentable as AugmentableContract; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Contracts\Taxonomies\Taxonomy as Contract; use Statamic\Data\ContainsCascadingData; use Statamic\Data\ContainsSupplementalData; @@ -32,7 +33,7 @@ use function Statamic\trans as __; -class Taxonomy implements Arrayable, ArrayAccess, AugmentableContract, Contract, Responsable +class Taxonomy implements Arrayable, ArrayAccess, AugmentableContract, ContainsQueryableValues, Contract, Responsable { use ContainsCascadingData, ContainsSupplementalData, ExistsAsFile, FluentlyGetsAndSets, HasAugmentedData; @@ -568,4 +569,22 @@ public function hasCustomTermTemplate() { return $this->termTemplate !== null; } + + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return $this->get($field); + } + + private function queryableMethods(): array + { + return [ + 'absoluteUrl', 'collection', 'collections', 'defaultPublishState', 'editUrl', 'handle', + 'hasSearchIndex', 'id', 'layout', 'path', 'revisionsEnabled', 'searchIndex', 'sites', + 'sortDirection', 'sortField', 'template', 'termTemplate', 'title', 'uri', 'url', + ]; + } } diff --git a/src/Taxonomies/Term.php b/src/Taxonomies/Term.php index 800261b1477..61a265f151e 100644 --- a/src/Taxonomies/Term.php +++ b/src/Taxonomies/Term.php @@ -2,6 +2,7 @@ namespace Statamic\Taxonomies; +use Statamic\Contracts\Query\ContainsQueryableValues; use Statamic\Contracts\Taxonomies\Term as TermContract; use Statamic\Data\ExistsAsFile; use Statamic\Data\HasDirtyState; @@ -20,7 +21,7 @@ use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; -class Term implements TermContract +class Term implements ContainsQueryableValues, TermContract { use ExistsAsFile, FluentlyGetsAndSets, HasDirtyState; @@ -302,6 +303,23 @@ public function set($key, $value) return $this; } + public function getQueryableValue(string $field) + { + if (in_array($method = Str::camel($field), $this->queryableMethods())) { + return $this->{$method}(); + } + + return $this->inDefaultLocale()->getQueryableValue($field); + } + + private function queryableMethods(): array + { + return [ + 'blueprint', 'collection', 'entriesCount', 'id', 'path', 'reference', + 'slug', 'taxonomy', 'taxonomyHandle', 'title', + ]; + } + public function getCurrentDirtyStateAttributes(): array { return array_merge([ diff --git a/tests/Data/Assets/AssetQueryBuilderTest.php b/tests/Data/Assets/AssetQueryBuilderTest.php index 7513bc9489b..15e8ee4495b 100644 --- a/tests/Data/Assets/AssetQueryBuilderTest.php +++ b/tests/Data/Assets/AssetQueryBuilderTest.php @@ -715,6 +715,17 @@ public function values_can_be_plucked() 'f.jpg', ], $this->container->queryAssets()->where('extension', 'jpg')->pluck('path')->all()); } + + #[Test] + public function sorting_by_unsafe_method_does_not_invoke_it() + { + $count = $this->container->assets()->count(); + $this->assertGreaterThan(0, $count); + + $this->container->queryAssets()->orderBy('delete', 'asc')->get(); + + $this->assertCount($count, $this->container->assets()); + } } class CustomScope extends Scope diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index cfaf8b4c2e9..bb86537a731 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -1270,6 +1270,19 @@ public function exists_returns_false_when_no_results_are_found() { $this->assertFalse(Entry::query()->exists()); } + + #[Test] + public function sorting_by_unsafe_method_does_not_invoke_it() + { + $this->createDummyCollectionAndEntries(); + + $count = Entry::all()->count(); + $this->assertGreaterThan(0, $count); + + Entry::query()->orderBy('delete', 'asc')->get(); + + $this->assertCount($count, Entry::all()); + } } class CustomScope extends Scope diff --git a/tests/Data/Taxonomies/TermQueryBuilderTest.php b/tests/Data/Taxonomies/TermQueryBuilderTest.php index 5d1afe80f8e..3537522fbe4 100644 --- a/tests/Data/Taxonomies/TermQueryBuilderTest.php +++ b/tests/Data/Taxonomies/TermQueryBuilderTest.php @@ -775,6 +775,21 @@ public function terms_are_found_using_where_relation() $this->assertCount(1, $terms); $this->assertEquals(['c'], $terms->map->slug->all()); } + + #[Test] + public function sorting_by_unsafe_method_does_not_invoke_it() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['title' => 'Alpha'])->save(); + Term::make('b')->taxonomy('tags')->data(['title' => 'Bravo'])->save(); + + $count = Term::all()->count(); + $this->assertGreaterThan(0, $count); + + Term::query()->orderBy('delete', 'asc')->get(); + + $this->assertCount($count, Term::all()); + } } class CustomScope extends Scope diff --git a/tests/Data/Users/UserQueryBuilderTest.php b/tests/Data/Users/UserQueryBuilderTest.php index 2ce099cb07b..039807a3575 100644 --- a/tests/Data/Users/UserQueryBuilderTest.php +++ b/tests/Data/Users/UserQueryBuilderTest.php @@ -496,6 +496,20 @@ public function users_are_found_using_scopes() $this->assertCount(1, User::query()->customScope(['email' => 'gandalf@precious.com'])->get()); $this->assertCount(1, User::query()->whereCustom(['email' => 'gandalf@precious.com'])->get()); } + + #[Test] + public function sorting_by_unsafe_method_does_not_invoke_it() + { + User::make()->email('a@example.com')->data(['name' => 'Alpha'])->save(); + User::make()->email('b@example.com')->data(['name' => 'Bravo'])->save(); + + $count = User::all()->count(); + $this->assertGreaterThan(0, $count); + + User::query()->orderBy('delete', 'asc')->get(); + + $this->assertCount($count, User::all()); + } } class CustomScope extends Scope