├── LICENSE.md ├── composer.json └── src ├── DataEvent.php ├── ElementTransformer.php ├── JsonFeedV1Serializer.php ├── PaginatorAdapter.php ├── Plugin.php ├── ResourceAdapterInterface.php ├── Settings.php ├── controllers └── DefaultController.php ├── icon.svg └── resources └── ElementResource.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Pixel & Tonic, Inc. 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "craftcms/element-api", 3 | "description": "Create a JSON API for your elements in Craft", 4 | "type": "craft-plugin", 5 | "keywords": [ 6 | "api", 7 | "cms", 8 | "craftcms", 9 | "json", 10 | "yii2" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Pixel & Tonic", 16 | "homepage": "https://pixelandtonic.com/" 17 | } 18 | ], 19 | "support": { 20 | "email": "support@craftcms.com", 21 | "issues": "https://github.com/craftcms/element-api/issues?state=open", 22 | "source": "https://github.com/craftcms/element-api", 23 | "docs": "https://github.com/craftcms/element-api/blob/v2/README.md", 24 | "rss": "https://github.com/craftcms/element-api/commits/v2.atom" 25 | }, 26 | "minimum-stability": "dev", 27 | "prefer-stable": true, 28 | "require": { 29 | "craftcms/cms": "^4.3.0|^5.0.0-beta.1", 30 | "league/fractal": "^0.20.1" 31 | }, 32 | "require-dev": { 33 | "craftcms/ecs": "dev-main", 34 | "craftcms/phpstan": "dev-main", 35 | "craftcms/rector": "dev-main" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "craft\\elementapi\\": "src/" 40 | } 41 | }, 42 | "scripts": { 43 | "check-cs": "ecs check --ansi", 44 | "fix-cs": "ecs check --ansi --fix", 45 | "phpstan": "phpstan --memory-limit=1G" 46 | }, 47 | "extra": { 48 | "name": "Element API", 49 | "handle": "element-api", 50 | "documentationUrl": "https://github.com/craftcms/element-api/blob/v2/README.md" 51 | }, 52 | "config": { 53 | "platform": { 54 | "php": "8.0.2" 55 | }, 56 | "allow-plugins": { 57 | "yiisoft/yii2-composer": true, 58 | "craftcms/plugin-installer": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/DataEvent.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 2.0 13 | */ 14 | class DataEvent extends Event 15 | { 16 | /** 17 | * @var Scope The Fractal data associated with the event 18 | */ 19 | public Scope $payload; 20 | } 21 | -------------------------------------------------------------------------------- /src/ElementTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | * @since 2.0 14 | */ 15 | class ElementTransformer extends TransformerAbstract 16 | { 17 | public function transform(ElementInterface $element): array 18 | { 19 | // Get the serialized custom field values 20 | $fields = $element->getSerializedFieldValues(); 21 | 22 | // Get the element attributes that aren't custom fields 23 | /** @var Element $element */ 24 | $attributes = array_diff($element->attributes(), array_keys($fields)); 25 | 26 | // Return the element as an array merged with its serialized custom field values 27 | return array_merge($element->toArray($attributes), $fields); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/JsonFeedV1Serializer.php: -------------------------------------------------------------------------------- 1 | $data]; 23 | } 24 | 25 | /** 26 | * @inheritdoc 27 | */ 28 | public function meta(array $meta): array 29 | { 30 | return array_merge([ 31 | 'version' => 'https://jsonfeed.org/version/1.1', 32 | 'title' => \Craft::$app->getSites()->getCurrentSite()->name, 33 | 'home_page_url' => UrlHelper::baseSiteUrl(), 34 | 'feed_url' => UrlHelper::url(Craft::$app->getRequest()->getPathInfo()), 35 | ], $meta); 36 | } 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function paginator(PaginatorInterface $paginator): array 42 | { 43 | $currentPage = $paginator->getCurrentPage(); 44 | $lastPage = $paginator->getLastPage(); 45 | 46 | if ($currentPage < $lastPage) { 47 | return [ 48 | 'next_url' => $paginator->getUrl($currentPage + 1), 49 | ]; 50 | } 51 | 52 | return []; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/PaginatorAdapter.php: -------------------------------------------------------------------------------- 1 | 13 | * @since 2.0 14 | */ 15 | class PaginatorAdapter implements PaginatorInterface 16 | { 17 | /** 18 | * @var int 19 | */ 20 | protected int $elementsPerPage; 21 | 22 | /** 23 | * @var int 24 | */ 25 | protected int $totalElements; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected string $pageParam; 31 | 32 | /** 33 | * @var int 34 | */ 35 | protected int $totalPages; 36 | 37 | /** 38 | * @var int 39 | */ 40 | protected int $currentPage; 41 | 42 | /** 43 | * @var int 44 | */ 45 | protected int $count; 46 | 47 | /** 48 | * Constructor 49 | * 50 | * @param integer $elementsPerPage 51 | * @param integer $totalElements 52 | * @param string $pageParam 53 | */ 54 | public function __construct(int $elementsPerPage, int $totalElements, string $pageParam) 55 | { 56 | $this->elementsPerPage = $elementsPerPage; 57 | $this->totalElements = $totalElements; 58 | $this->pageParam = $pageParam; 59 | $this->totalPages = (int)ceil($this->totalElements / $this->elementsPerPage); 60 | $this->currentPage = $this->_currentPage(); 61 | } 62 | 63 | /** 64 | * Get the current page. 65 | * 66 | * @return int 67 | */ 68 | public function getCurrentPage(): int 69 | { 70 | return $this->currentPage; 71 | } 72 | 73 | /** 74 | * Get the last page. 75 | * 76 | * @return int 77 | */ 78 | public function getLastPage(): int 79 | { 80 | return $this->totalPages; 81 | } 82 | 83 | /** 84 | * Get the total. 85 | * 86 | * @return int 87 | */ 88 | public function getTotal(): int 89 | { 90 | return $this->totalElements; 91 | } 92 | 93 | /** 94 | * Get the count. 95 | * 96 | * @return int 97 | */ 98 | public function getCount(): int 99 | { 100 | return $this->count; 101 | } 102 | 103 | /** 104 | * Sets the count. 105 | * 106 | * @param int $count 107 | */ 108 | public function setCount(int $count): void 109 | { 110 | $this->count = $count; 111 | } 112 | 113 | /** 114 | * Get the number per page. 115 | * 116 | * @return int 117 | */ 118 | public function getPerPage(): int 119 | { 120 | return $this->elementsPerPage; 121 | } 122 | 123 | /** 124 | * Get the url for the given page. 125 | * 126 | * @param int $page 127 | * @return string 128 | */ 129 | public function getUrl(int $page): string 130 | { 131 | $request = Craft::$app->getRequest(); 132 | 133 | // Get the query string params without the path or `pattern` 134 | parse_str($request->getQueryString(), $params); 135 | $pathParam = Craft::$app->getConfig()->getGeneral()->pathParam; 136 | unset($params[$pathParam], $params['pattern']); 137 | 138 | // Return the URL with the page param 139 | $params[$this->pageParam] = $page; 140 | return UrlHelper::url($request->getPathInfo(), $params); 141 | } 142 | 143 | /** 144 | * Determines the current page. 145 | * 146 | * @return integer 147 | */ 148 | private function _currentPage(): int 149 | { 150 | $currentPage = Craft::$app->getRequest()->getQueryParam($this->pageParam, 1); 151 | 152 | if (is_numeric($currentPage) && $currentPage > $this->totalPages) { 153 | $currentPage = $this->totalPages > 0 ? $this->totalPages : 1; 154 | } elseif (!is_numeric($currentPage) || $currentPage < 0) { 155 | $currentPage = 1; 156 | } 157 | 158 | return (int)$currentPage; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | 21 | * @since 2.0 22 | */ 23 | class Plugin extends \craft\base\Plugin 24 | { 25 | /** 26 | * @var array The default Fractal resource adapter configuration 27 | * @see getDefaultResourceAdapterConfig() 28 | */ 29 | private ?array $_defaultResourceAdapterConfig = null; 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function init(): void 35 | { 36 | parent::init(); 37 | 38 | Event::on( 39 | UrlManager::class, 40 | UrlManager::EVENT_REGISTER_SITE_URL_RULES, 41 | [$this, 'registerUrlRules'] 42 | ); 43 | 44 | Event::on( 45 | ClearCaches::class, 46 | ClearCaches::EVENT_REGISTER_TAG_OPTIONS, 47 | function(RegisterCacheOptionsEvent $event) { 48 | $event->options[] = [ 49 | 'tag' => 'element-api', 50 | 'label' => Craft::t('element-api', 'Element API responses'), 51 | ]; 52 | } 53 | ); 54 | } 55 | 56 | /** 57 | * Returns the endpoint config for a given URL pattern. 58 | * 59 | * @param string $pattern 60 | * @return callable|array|ResourceAdapterInterface|null 61 | */ 62 | public function getEndpoint(string $pattern) 63 | { 64 | return $this->getSettings()->endpoints[$pattern] ?? null; 65 | } 66 | 67 | /** 68 | * Returns the default endpoint configuration. 69 | * 70 | * @return array 71 | */ 72 | public function getDefaultResourceAdapterConfig(): array 73 | { 74 | if ($this->_defaultResourceAdapterConfig !== null) { 75 | return $this->_defaultResourceAdapterConfig; 76 | } 77 | 78 | return $this->_defaultResourceAdapterConfig = $this->getSettings()->getDefaults(); 79 | } 80 | 81 | /** 82 | * Registers the site URL rules. 83 | * 84 | * @param RegisterUrlRulesEvent $event 85 | */ 86 | public function registerUrlRules(RegisterUrlRulesEvent $event): void 87 | { 88 | foreach ($this->getSettings()->endpoints as $pattern => $config) { 89 | $event->rules[$pattern] = [ 90 | 'route' => 'element-api', 91 | 'defaults' => ['pattern' => $pattern], 92 | ]; 93 | } 94 | } 95 | 96 | /** 97 | * Creates a Fractal resource based on the given config. 98 | * 99 | * @param array|ResourceInterface|ResourceAdapterInterface $config 100 | * @return ResourceInterface 101 | */ 102 | public function createResource($config): ResourceInterface 103 | { 104 | if ($config instanceof ResourceInterface) { 105 | return $config; 106 | } 107 | 108 | if ($config instanceof ResourceAdapterInterface) { 109 | return $config->getResource(); 110 | } 111 | 112 | if (!isset($config['class'])) { 113 | // Default to ElementResourceAdapter 114 | $config['class'] = ElementResource::class; 115 | } 116 | 117 | /** @var ResourceInterface|ResourceAdapterInterface $resource */ 118 | $resource = Craft::createObject($config); 119 | 120 | if ($resource instanceof ResourceAdapterInterface) { 121 | $resource = $resource->getResource(); 122 | } 123 | 124 | return $resource; 125 | } 126 | 127 | protected function createSettingsModel(): ?Model 128 | { 129 | return new Settings(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/ResourceAdapterInterface.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 2.0 12 | */ 13 | interface ResourceAdapterInterface 14 | { 15 | /** 16 | * Returns the Fractal data resource. 17 | * 18 | * @return ResourceInterface 19 | */ 20 | public function getResource(): ResourceInterface; 21 | } 22 | -------------------------------------------------------------------------------- /src/Settings.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 2.0 12 | */ 13 | class Settings extends Model 14 | { 15 | /** 16 | * @var callable|array The default endpoint configuration. 17 | */ 18 | public $defaults = []; 19 | 20 | /** 21 | * @var array The endpoint configurations. 22 | */ 23 | public array $endpoints = []; 24 | 25 | /** 26 | * Returns the default endpoint configuration. 27 | * 28 | * @return array The default endpoint configuration. 29 | * @since 2.6.0 30 | */ 31 | public function getDefaults(): array 32 | { 33 | return is_callable($this->defaults) ? call_user_func($this->defaults) : $this->defaults; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/controllers/DefaultController.php: -------------------------------------------------------------------------------- 1 | 37 | * @since 2.0 38 | */ 39 | class DefaultController extends Controller 40 | { 41 | /** 42 | * @event DataEvent The event that is triggered before sending the response data 43 | */ 44 | public const EVENT_BEFORE_SEND_DATA = 'beforeSendData'; 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | protected array|int|bool $allowAnonymous = true; 50 | 51 | /** 52 | * Returns the requested elements as JSON. 53 | * 54 | * @param string $pattern The endpoint URL pattern that was matched 55 | * @return Response 56 | * @throws InvalidConfigException 57 | */ 58 | public function actionIndex(string $pattern): Response 59 | { 60 | $callback = null; 61 | $jsonOptions = null; 62 | $pretty = false; 63 | /** @var mixed $cache */ 64 | $cache = true; 65 | $statusCode = 200; 66 | $statusText = null; 67 | 68 | try { 69 | $plugin = Plugin::getInstance(); 70 | $config = $plugin->getEndpoint($pattern); 71 | 72 | if (is_callable($config)) { 73 | /** @phpstan-ignore-next-line */ 74 | $params = Craft::$app->getUrlManager()->getRouteParams(); 75 | $config = $this->_callWithParams($config, $params); 76 | } 77 | 78 | if ($this->request->getIsOptions()) { 79 | // Now that the endpoint has had a chance to add CORS response headers, end the request 80 | $this->response->format = Response::FORMAT_RAW; 81 | return $this->response; 82 | } 83 | 84 | if (is_array($config)) { 85 | // Merge in the defaults 86 | $config = ArrayHelper::merge($plugin->getDefaultResourceAdapterConfig(), $config); 87 | } 88 | 89 | // Prevent API endpoints from getting indexed 90 | $this->response->getHeaders()->setDefault('X-Robots-Tag', 'none'); 91 | 92 | // Before anything else, check the cache 93 | $cache = ArrayHelper::remove($config, 'cache', true); 94 | if ($this->request->getIsPreview() || $this->request->getIsLivePreview()) { 95 | // Ignore config & disable cache for live preview 96 | $cache = false; 97 | } 98 | 99 | $cacheKey = ArrayHelper::remove($config, 'cacheKey') 100 | ?? implode(':', [ 101 | 'elementapi', 102 | Craft::$app->getSites()->getCurrentSite()->id, 103 | $this->request->getPathInfo(), 104 | $this->request->getQueryStringWithoutPath(), 105 | ]); 106 | 107 | if ($cache) { 108 | $cacheService = Craft::$app->getCache(); 109 | 110 | if (($cachedContent = $cacheService->get($cacheKey)) !== false) { 111 | if (StringHelper::startsWith($cachedContent, 'data:')) { 112 | [$contentType, $cachedContent] = explode(',', substr($cachedContent, 5), 2); 113 | } 114 | // Set the JSON headers 115 | $formatter = new JsonResponseFormatter([ 116 | 'contentType' => $contentType ?? null, 117 | ]); 118 | $formatter->format($this->response); 119 | 120 | // Set the cached JSON on the response and return 121 | $this->response->format = Response::FORMAT_RAW; 122 | $this->response->content = $cachedContent; 123 | return $this->response; 124 | } 125 | 126 | $elementsService = Craft::$app->getElements(); 127 | $elementsService->startCollectingCacheInfo(); 128 | } 129 | 130 | // Extract config settings that aren't meant for createResource() 131 | $serializer = ArrayHelper::remove($config, 'serializer'); 132 | $callback = ArrayHelper::remove($config, 'callback'); 133 | $jsonOptions = ArrayHelper::remove($config, 'jsonOptions', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 134 | $pretty = ArrayHelper::remove($config, 'pretty', false); 135 | $includes = ArrayHelper::remove($config, 'includes', []); 136 | $excludes = ArrayHelper::remove($config, 'excludes', []); 137 | $contentType = ArrayHelper::remove($config, 'contentType'); 138 | 139 | // Generate all transforms immediately 140 | Craft::$app->getConfig()->getGeneral()->generateTransformsBeforePageLoad = true; 141 | 142 | // Get the data resource 143 | $resource = $plugin->createResource($config); 144 | 145 | // Load Fractal 146 | $fractal = new Manager(); 147 | 148 | // Serialize the data 149 | if (!$serializer instanceof SerializerAbstract) { 150 | switch ($serializer) { 151 | case 'dataArray': 152 | $serializer = new DataArraySerializer(); 153 | break; 154 | case 'jsonApi': 155 | $serializer = new JsonApiSerializer(); 156 | break; 157 | case 'jsonFeed': 158 | $serializer = new JsonFeedV1Serializer(); 159 | if ($contentType === null) { 160 | $contentType = 'application/feed+json'; 161 | } 162 | break; 163 | default: 164 | $serializer = new ArraySerializer(); 165 | } 166 | } 167 | 168 | $fractal->setSerializer($serializer); 169 | 170 | // Parse includes/excludes 171 | $fractal->parseIncludes($includes); 172 | $fractal->parseExcludes($excludes); 173 | 174 | $data = $fractal->createData($resource); 175 | 176 | // Fire a 'beforeSendData' event 177 | if ($this->hasEventHandlers(self::EVENT_BEFORE_SEND_DATA)) { 178 | $this->trigger(self::EVENT_BEFORE_SEND_DATA, new DataEvent([ 179 | 'payload' => $data, 180 | ])); 181 | } 182 | 183 | $data = $data->toArray(); 184 | } catch (\Throwable $e) { 185 | $data = [ 186 | 'error' => [ 187 | 'code' => $e instanceof HttpException ? $e->statusCode : $e->getCode(), 188 | 'message' => $e instanceof UserException ? $e->getMessage() : 'A server error occurred.', 189 | ], 190 | ]; 191 | $statusCode = $e instanceof HttpException ? $e->statusCode : 500; 192 | if ($e instanceof UserException && ($message = $e->getMessage())) { 193 | $statusText = preg_split('/[\r\n]/', $message, 2)[0]; 194 | } else { 195 | $statusText = 'Server error'; 196 | } 197 | 198 | // Log the exception 199 | Craft::error('Error resolving Element API endpoint: ' . $e->getMessage(), __METHOD__); 200 | Craft::$app->getErrorHandler()->logException($e); 201 | } 202 | 203 | // Create a JSON response formatter with custom options 204 | $formatter = new JsonResponseFormatter([ 205 | 'contentType' => $contentType ?? null, 206 | 'useJsonp' => $callback !== null, 207 | 'encodeOptions' => $jsonOptions, 208 | 'prettyPrint' => $pretty, 209 | ]); 210 | 211 | // Manually format the response ahead of time, so we can access and cache the JSON 212 | if ($callback !== null) { 213 | $this->response->data = [ 214 | 'data' => $data, 215 | 'callback' => $callback, 216 | ]; 217 | } else { 218 | $this->response->data = $data; 219 | } 220 | 221 | $formatter->format($this->response); 222 | $this->response->data = null; 223 | $this->response->format = Response::FORMAT_RAW; 224 | 225 | // Cache it? 226 | if ($statusCode !== 200) { 227 | $cache = false; 228 | } 229 | if ($cache) { 230 | if ($cache !== true) { 231 | $expire = ConfigHelper::durationInSeconds($cache); 232 | } else { 233 | $expire = null; 234 | } 235 | 236 | /** @phpstan-ignore-next-line */ 237 | [$dep, $maxDuration] = $elementsService->stopCollectingCacheInfo(); 238 | $dep ??= new TagDependency(); 239 | $dep->tags[] = 'element-api'; 240 | 241 | if ($maxDuration) { 242 | if ($expire !== null) { 243 | $expire = min($expire, $maxDuration); 244 | } else { 245 | $expire = $maxDuration; 246 | } 247 | } 248 | 249 | $cachedContent = $this->response->content; 250 | if (isset($contentType)) { 251 | $cachedContent = "data:$contentType,$cachedContent"; 252 | } 253 | /** @phpstan-ignore-next-line */ 254 | $cacheService->set($cacheKey, $cachedContent, $expire, $dep); 255 | } 256 | 257 | // Don't double-encode the data 258 | $this->response->format = Response::FORMAT_RAW; 259 | $this->response->setStatusCode($statusCode, $statusText); 260 | return $this->response; 261 | } 262 | 263 | /** 264 | * Calls a given function. If any params are given, they will be mapped to the function's arguments. 265 | * 266 | * @param callable $func The function to call 267 | * @param array $params Any params that should be mapped to function arguments 268 | * @return mixed The result of the function 269 | * @throws InvalidConfigException 270 | */ 271 | private function _callWithParams($func, $params) 272 | { 273 | if (empty($params)) { 274 | return $func(); 275 | } 276 | 277 | $ref = new ReflectionFunction($func); 278 | $args = []; 279 | 280 | foreach ($ref->getParameters() as $param) { 281 | $name = $param->getName(); 282 | 283 | if (isset($params[$name])) { 284 | if ($param->isArray()) { 285 | $args[] = is_array($params[$name]) ? $params[$name] : [$params[$name]]; 286 | } elseif (!is_array($params[$name])) { 287 | $args[] = $params[$name]; 288 | } else { 289 | throw new InvalidConfigException("Unable to resolve $name param"); 290 | } 291 | } elseif ($param->isDefaultValueAvailable()) { 292 | $args[] = $param->getDefaultValue(); 293 | } else { 294 | throw new InvalidConfigException("Unable to resolve $name param"); 295 | } 296 | } 297 | 298 | return $ref->invokeArgs($args); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | element-api-v2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/resources/ElementResource.php: -------------------------------------------------------------------------------- 1 | 26 | * @since 2.0 27 | */ 28 | class ElementResource extends BaseObject implements ResourceAdapterInterface 29 | { 30 | /** 31 | * @var string The element type class name 32 | */ 33 | public string $elementType; 34 | 35 | /** 36 | * @var array The element criteria params that should be used to filter the matching elements 37 | */ 38 | public array $criteria = []; 39 | 40 | /** 41 | * @var callable|string|array|TransformerAbstract The transformer config, or an actual transformer object 42 | */ 43 | public $transformer = ElementTransformer::class; 44 | 45 | /** 46 | * @var bool Whether to only return one result 47 | */ 48 | public bool $one = false; 49 | 50 | /** 51 | * @var bool Whether to paginate the results 52 | */ 53 | public bool $paginate = true; 54 | 55 | /** 56 | * @var int The number of elements to include per page 57 | * @see paginate 58 | */ 59 | public int $elementsPerPage = 100; 60 | 61 | /** 62 | * @var string The query string param name that should be used to specify the page number 63 | * @see paginate 64 | */ 65 | public string $pageParam = 'page'; 66 | 67 | /** 68 | * @var string|null The resource key that should be set on the resource 69 | */ 70 | public ?string $resourceKey = 'data'; 71 | 72 | /** 73 | * @var array|null Custom meta values 74 | */ 75 | public ?array $meta = null; 76 | 77 | /** 78 | * @inheritdoc 79 | */ 80 | public function __construct(array $config = []) 81 | { 82 | if (array_key_exists('first', $config)) { 83 | $config['one'] = ArrayHelper::remove($config, 'first'); 84 | Craft::$app->getDeprecator()->log('ElementAPI:first', 'The `first` Element API endpoint setting has been renamed to `one`.'); 85 | } 86 | 87 | parent::__construct($config); 88 | } 89 | 90 | /** 91 | * @inheritdoc 92 | * @throws InvalidConfigException 93 | */ 94 | public function init(): void 95 | { 96 | if (!isset($this->elementType) || !is_subclass_of($this->elementType, ElementInterface::class)) { 97 | throw new InvalidConfigException('Endpoint has an invalid elementType'); 98 | } 99 | 100 | if ($this->paginate) { 101 | // Make sure the page param != the path param 102 | $pathParam = Craft::$app->getConfig()->getGeneral()->pathParam; 103 | if ($this->pageParam === $pathParam) { 104 | throw new InvalidConfigException("The pageParam cannot be set to \"{$pathParam}\" because that's the parameter Craft uses to check the requested path."); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * @inheritdoc 111 | * @throws Exception if [[one]] is true and no element matches [[criteria]] 112 | */ 113 | public function getResource(): ResourceInterface 114 | { 115 | /** @var ElementQuery $query */ 116 | $query = $this->getElementQuery(); 117 | $transformer = $this->getTransformer(); 118 | 119 | if ($this->one) { 120 | $element = $query->one(); 121 | 122 | if (!$element) { 123 | throw new NotFoundHttpException('No element exists that matches the endpoint criteria'); 124 | } 125 | 126 | $resource = new Item($element, $transformer, $this->resourceKey); 127 | } elseif ($this->paginate) { 128 | // Create the paginator 129 | $paginator = new PaginatorAdapter($this->elementsPerPage, $query->count(), $this->pageParam); 130 | 131 | // Fetch this page's elements 132 | $query->offset(($query->offset ?: 0) + $this->elementsPerPage * ($paginator->getCurrentPage() - 1)); 133 | $query->limit($this->elementsPerPage); 134 | $elements = $query->all(); 135 | $paginator->setCount(count($elements)); 136 | 137 | $resource = new Collection($elements, $transformer, $this->resourceKey); 138 | $resource->setPaginator($paginator); 139 | } else { 140 | $resource = new Collection($query->all(), $transformer, $this->resourceKey); 141 | } 142 | 143 | if ($this->meta !== null) { 144 | $resource->setMeta($this->meta); 145 | } 146 | 147 | return $resource; 148 | } 149 | 150 | /** 151 | * Returns the element query based on [[elementType]] and [[criteria]] 152 | * 153 | * @return ElementQueryInterface 154 | */ 155 | protected function getElementQuery(): ElementQueryInterface 156 | { 157 | /** @var string|ElementInterface $elementType */ 158 | $elementType = $this->elementType; 159 | $query = $elementType::find(); 160 | Craft::configure($query, $this->criteria); 161 | 162 | return $query; 163 | } 164 | 165 | /** 166 | * Returns the transformer based on the given endpoint 167 | * 168 | * @return callable|TransformerAbstract 169 | */ 170 | protected function getTransformer() 171 | { 172 | if (is_callable($this->transformer) || $this->transformer instanceof TransformerAbstract) { 173 | return $this->transformer; 174 | } 175 | 176 | return Craft::createObject($this->transformer); 177 | } 178 | } 179 | --------------------------------------------------------------------------------