From 1b024a9e6ff04598d0de670ed497765b3874fa7c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 5 Nov 2023 21:50:32 +0100 Subject: [PATCH 1/3] First try --- composer.json | 17 +- phpstan.neon | 3 +- phpunit.xml.dist | 36 +- psalm.xml | 19 +- src/Datasource/Connection.php | 57 ++- .../Exception/MissingConnectionException.php | 2 +- src/Datasource/Marshaller.php | 34 +- src/Datasource/Query.php | 465 +++++++++++++++--- src/Datasource/ResultSet.php | 31 +- src/Datasource/Schema.php | 24 +- src/Model/Endpoint.php | 161 +++--- src/Model/EndpointLocator.php | 8 +- .../MissingEndpointSchemaException.php | 2 +- .../MissingResourceClassException.php | 2 +- src/Model/ResourceBasedEntityInterface.php | 2 +- src/Model/ResourceBasedEntityTrait.php | 2 +- src/Plugin.php | 6 +- src/Webservice/Driver/AbstractDriver.php | 23 +- .../Exception/MissingDriverException.php | 2 +- .../MissingWebserviceClassException.php | 2 +- .../Exception/UnexpectedDriverException.php | 2 +- .../UnimplementedDriverMethodException.php | 2 +- ...UnimplementedWebserviceMethodException.php | 2 +- src/Webservice/Webservice.php | 34 +- src/Webservice/WebserviceInterface.php | 6 +- tests/TestCase/AbstractDriverTest.php | 13 +- tests/TestCase/BootstrapTest.php | 8 +- tests/TestCase/ConnectionTest.php | 7 +- tests/TestCase/MarshallerTest.php | 7 +- tests/TestCase/Model/EndpointLocatorTest.php | 5 +- tests/TestCase/Model/EndpointTest.php | 6 +- tests/TestCase/QueryTest.php | 3 +- tests/TestCase/Webservice/WebserviceTest.php | 6 +- tests/bootstrap.php | 4 +- .../Driver/{Test.php => TestDriver.php} | 2 +- .../src/Webservice/EndpointTestWebservice.php | 28 +- tests/test_app/src/Webservice/Logger.php | 37 +- .../src/Webservice/StaticWebservice.php | 2 +- 38 files changed, 755 insertions(+), 317 deletions(-) rename tests/test_app/src/Webservice/Driver/{Test.php => TestDriver.php} (93%) diff --git a/composer.json b/composer.json index 75b78e6..d64f3bd 100644 --- a/composer.json +++ b/composer.json @@ -39,12 +39,16 @@ "irc": "irc://irc.freenode.org/muffin" }, "require": { - "cakephp/orm": "^4.2" + "cakephp/orm": "^5.0" }, "require-dev": { - "cakephp/cakephp": "^4.2", - "cakephp/cakephp-codesniffer": "^4.0", - "phpunit/phpunit": "^8.5 || ^9.3" + "cakephp/cakephp": "^5.0", + "cakephp/cakephp-codesniffer": "^5.0", + "phpunit/phpunit": "^10.1" + }, + "scripts": { + "cs-check": "phpcs --colors --parallel=16 -p src/ tests/", + "cs-fix": "phpcbf --colors --parallel=16 -p src/ tests/" }, "autoload": { "psr-4": { @@ -58,5 +62,10 @@ "SomeVendor\\SomePlugin\\": "tests/test_app/plugins/SomeVendor/SomePlugin/src", "TestPlugin\\": "tests/test_app/plugins/TestPlugin/src" } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/phpstan.neon b/phpstan.neon index 3c5f6df..16597c9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,8 @@ parameters: - level: 6 + level: 8 checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false + treatPhpDocTypesAsCertain: false paths: - src/ ignoreErrors: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 33055d7..36952e4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,30 +1,20 @@ - - + - + ./tests/ - - - - - - - - - - - - + + + + + + + + + ./src/ - - + + diff --git a/psalm.xml b/psalm.xml index 74ddb1f..6f36d46 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,10 +1,12 @@ @@ -14,16 +16,9 @@ - - - - - - - - - - - + + + + diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index f31bc3c..4a47d34 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -4,10 +4,12 @@ namespace Muffin\Webservice\Datasource; use Cake\Core\App; +use Cake\Datasource\ConnectionInterface; use Muffin\Webservice\Datasource\Exception\MissingConnectionException; use Muffin\Webservice\Webservice\Driver\AbstractDriver; use Muffin\Webservice\Webservice\Exception\MissingDriverException; use Muffin\Webservice\Webservice\Exception\UnexpectedDriverException; +use Psr\SimpleCache\CacheInterface; /** * Class Connection @@ -16,14 +18,21 @@ * @method \Muffin\Webservice\Webservice\WebserviceInterface getWebservice(string $name) Proxy method through to the Driver * @method string configName() Proxy method through to the Driver */ -class Connection +class Connection implements ConnectionInterface { /** * Driver * * @var \Muffin\Webservice\Webservice\Driver\AbstractDriver */ - protected $_driver; + protected ?AbstractDriver $_driver = null; + + protected CacheInterface $cacher; + + /** + * The connection name in the connection manager. + */ + protected string $configName = ''; /** * Constructor @@ -33,19 +42,55 @@ class Connection */ public function __construct(array $config) { + if (isset($config['name'])) { + $this->configName = $config['name']; + } $config = $this->_normalizeConfig($config); - /** @psalm-var class-string<\Muffin\Webservice\Webservice\Driver\AbstractDriver> */ $driver = $config['driver']; unset($config['driver'], $config['service']); $this->_driver = new $driver($config); - /** @psalm-suppress TypeDoesNotContainType */ if (!($this->_driver instanceof AbstractDriver)) { throw new UnexpectedDriverException(['driver' => $driver]); } } + public function setCacher(CacheInterface $cacher) { } + + public function getCacher(): CacheInterface { } + + /** + * {@inheritDoc} + * + * @see \Cake\Datasource\ConnectionInterface::getDriver() + * @return \Muffin\Webservice\Webservice\Driver\AbstractDriver + */ + public function getDriver(string $role = self::ROLE_WRITE): object + { + return $this->_driver; + } + + /** + * Get the configuration name for this connection. + * + * @return string + */ + public function configName(): string + { + return $this->configName; + } + + /** + * Get the config data for this connection. + * + * @return array + */ + public function config(): array + { + return $this->_driver->getConfig(); + } + /** * Validates certain custom configuration values. * @@ -61,7 +106,7 @@ protected function _normalizeConfig(array $config): array throw new MissingConnectionException(['name' => $config['name']]); } - $config['driver'] = App::className($config['service'], 'Webservice/Driver'); + $config['driver'] = App::className($config['service'], 'Webservice/Driver', 'Driver'); if (!$config['driver']) { throw new MissingDriverException(['driver' => $config['driver']]); } @@ -77,7 +122,7 @@ protected function _normalizeConfig(array $config): array * @param array $args Arguments to pass-through * @return mixed */ - public function __call($method, $args) + public function __call(string $method, array $args): mixed { return call_user_func_array([$this->_driver, $method], $args); } diff --git a/src/Datasource/Exception/MissingConnectionException.php b/src/Datasource/Exception/MissingConnectionException.php index ace3e6e..259bc33 100644 --- a/src/Datasource/Exception/MissingConnectionException.php +++ b/src/Datasource/Exception/MissingConnectionException.php @@ -12,5 +12,5 @@ class MissingConnectionException extends CakeException * * @var string */ - protected $_messageTemplate = 'No `%s` connection configured.'; + protected string $_messageTemplate = 'No `%s` connection configured.'; } diff --git a/src/Datasource/Marshaller.php b/src/Datasource/Marshaller.php index 0170beb..aad6124 100644 --- a/src/Datasource/Marshaller.php +++ b/src/Datasource/Marshaller.php @@ -9,6 +9,7 @@ use Cake\Datasource\InvalidPropertyInterface; use Muffin\Webservice\Model\Endpoint; use RuntimeException; +use Traversable; /** * Contains logic to convert array data into resources. @@ -22,7 +23,7 @@ class Marshaller * * @var \Muffin\Webservice\Model\Endpoint */ - protected $_endpoint; + protected Endpoint $_endpoint; /** * Constructor. @@ -110,25 +111,20 @@ protected function _validate(array $data, array $options, bool $isNew): array if (!$options['validate']) { return []; } - - $validator = null; if ($options['validate'] === true) { - $validator = $this->_endpoint->getValidator('default'); - } elseif (is_string($options['validate'])) { - $validator = $this->_endpoint->getValidator($options['validate']); - } else { - /** @var \Cake\Validation\Validator $validator */ - $validator = $options['validator']; + $options['validate'] = $this->_endpoint->getValidator('default'); } - if (!is_callable([$validator, 'errors'])) { - throw new RuntimeException(sprintf( - '"validate" must be a boolean, a string or an object with method "errors()". Got %s instead.', - gettype($options['validate']) - )); + if (is_string($options['validate'])) { + $options['validate'] = $this->_endpoint->getValidator($options['validate']); + } + if (!is_object($options['validate'])) { + throw new RuntimeException( + sprintf('validate must be a boolean, a string or an object. Got %s.', gettype($options['validate'])) + ); } - return $validator->validate($data, $isNew); + return $options['validate']->validate($data, $isNew); } /** @@ -165,7 +161,7 @@ protected function _prepareDataAndOptions(array $data, array $options): array * * @param array $data The data to hydrate. * @param array $options List of options - * @return \Cake\Datasource\EntityInterface[] An array of hydrated records. + * @return array<\Cake\Datasource\EntityInterface> An array of hydrated records. * @see \Muffin\Webservice\Model\Endpoint::newEntities() */ public function many(array $data, array $options = []): array @@ -260,13 +256,13 @@ public function merge(EntityInterface $entity, array $data, array $options = []) * the accessible fields list in the entity will be used. * - accessibleFields: A list of fields to allow or deny in entity accessible fields. * - * @param array|\Traversable $entities the entities that will get the + * @param \Traversable|array $entities the entities that will get the * data merged in * @param array $data list of arrays to be merged into the entities * @param array $options List of options. - * @return \Cake\Datasource\EntityInterface[] + * @return array<\Cake\Datasource\EntityInterface> */ - public function mergeMany($entities, array $data, array $options = []): array + public function mergeMany(array|Traversable $entities, array $data, array $options = []): array { $primary = (array)$this->_endpoint->getPrimaryKey(); diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index d8a8814..7f371ce 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -4,21 +4,27 @@ namespace Muffin\Webservice\Datasource; use ArrayObject; +use Cake\Collection\Iterator\MapReduce; +use Cake\Database\ExpressionInterface; use Cake\Datasource\Exception\RecordNotFoundException; +use Cake\Datasource\QueryCacher; use Cake\Datasource\QueryInterface; -use Cake\Datasource\QueryTrait; +use Cake\Datasource\RepositoryInterface; +use Cake\Datasource\ResultSetDecorator; use Cake\Datasource\ResultSetInterface; use Cake\Utility\Hash; +use Closure; use InvalidArgumentException; use IteratorAggregate; use JsonSerializable; use Muffin\Webservice\Model\Endpoint; +use Muffin\Webservice\Model\Resource; use Muffin\Webservice\Webservice\WebserviceInterface; +use Traversable; +use UnexpectedValueException; class Query implements IteratorAggregate, JsonSerializable, QueryInterface { - use QueryTrait; - public const ACTION_CREATE = 1; public const ACTION_READ = 2; public const ACTION_UPDATE = 3; @@ -50,7 +56,14 @@ class Query implements IteratorAggregate, JsonSerializable, QueryInterface * * @var bool */ - protected $_beforeFindFired = false; + protected bool $_beforeFindFired = false; + + /** + * Whether the query is standalone or the product of an eager load operation. + * + * @var bool + */ + protected bool $_eagerLoaded = false; /** * Indicates whether internal state of this query was changed, this is used to @@ -59,33 +72,71 @@ class Query implements IteratorAggregate, JsonSerializable, QueryInterface * * @var bool */ - protected $_dirty = false; + protected bool $_dirty = false; /** * Parts being used to in the query * * @var array */ - protected $_parts = [ + protected array $_parts = [ 'order' => [], 'set' => [], 'where' => [], 'select' => [], ]; + /** + * Holds any custom options passed using applyOptions that could not be processed + * by any method in this class. + * + * @var array + */ + protected array $_options = []; + /** * Instance of the webservice to use * * @var \Muffin\Webservice\Webservice\WebserviceInterface */ - protected $_webservice; + protected WebserviceInterface $_webservice; /** * The result from the webservice * - * @var bool|int|\Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet + * @var \Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet|int|bool + */ + protected mixed $_results = null; + + /** + * Instance of a endpoint object this query is bound to + * + * @var \Cake\Datasource\RepositoryInterface + */ + protected RepositoryInterface $_endpoint; + + /** + * List of map-reduce routines that should be applied over the query + * result + * + * @var array + */ + protected array $_mapReduce = []; + + /** + * List of formatter classes or callbacks that will post-process the + * results when fetched + * + * @var array<\Closure> + */ + protected array $_formatters = []; + + /** + * A query cacher instance if this query has caching enabled. + * + * @var \Cake\Datasource\QueryCacher|null */ - protected $_result; + protected ?QueryCacher $_cache = null; /** * Construct the query @@ -99,6 +150,110 @@ public function __construct(WebserviceInterface $webservice, Endpoint $endpoint) $this->setEndpoint($endpoint); } + /** + * Executes this query and returns a results iterator. This function is required + * for implementing the IteratorAggregate interface and allows the query to be + * iterated without having to call execute() manually, thus making it look like + * a result set instead of the query itself. + * + * @return \Traversable + */ + public function getIterator(): Traversable + { + return $this->all(); + } + + /** + * @inheritDoc + */ + public function aliasFields(array $fields, ?string $defaultAlias = null): array + { + $aliased = []; + foreach ($fields as $alias => $field) { + if (is_numeric($alias) && is_string($field)) { + $aliased += $this->aliasField($field, $defaultAlias); + continue; + } + $aliased[$alias] = $field; + } + + return $aliased; + } + + /** + * Fetch the results for this query. + * + * Will return either the results set through setResult(), or execute this query + * and return the ResultSetDecorator object ready for streaming of results. + * + * ResultSetDecorator is a traversable object that implements the methods found + * on Cake\Collection\Collection. + * + * @return \Cake\Datasource\ResultSetInterface + */ + public function all(): ResultSetInterface + { + if ($this->_results !== null) { + if (!($this->_results instanceof ResultSetInterface)) { + $this->_results = $this->decorateResults($this->_results); + } + + return $this->_results; + } + + $results = null; + if ($this->_cache) { + $results = $this->_cache->fetch($this); + } + if ($results === null) { + $results = $this->decorateResults($this->_execute()); + if ($this->_cache) { + $this->_cache->store($this, $results); + } + } + $this->_results = $results; + + return $this->_results; + } + + public function orderBy(Closure|array|string $fields, bool $overwrite = false): void + { + } + + /** + * Returns an array representation of the results after executing the query. + * + * @return array + */ + public function toArray(): array + { + return $this->all()->toArray(); + } + + /** + * Set the default repository object that will be used by this query. + * + * @param \Cake\Datasource\RepositoryInterface $repository The default repository object to use. + * @return $this + */ + public function setRepository(RepositoryInterface $repository) + { + $this->_endpoint = $repository; + + return $this; + } + + /** + * Returns the default repository object that will be used by this query, + * that is, the table that will appear in the from clause. + * + * @return \Muffin\Webservice\Model\Endpoint + */ + public function getRepository(): ?RepositoryInterface + { + return $this->_endpoint; + } + /** * Mark the query as create * @@ -158,7 +313,7 @@ public function delete() * @param string $name name of the clause to be returned * @return mixed */ - public function clause(string $name) + public function clause(string $name): mixed { if (isset($this->_parts[$name])) { return $this->_parts[$name]; @@ -176,7 +331,7 @@ public function clause(string $name) */ public function setEndpoint(Endpoint $endpoint) { - $this->repository($endpoint); + $this->_endpoint = $endpoint; return $this; } @@ -190,7 +345,7 @@ public function setEndpoint(Endpoint $endpoint) public function getEndpoint(): Endpoint { /** @var \Muffin\Webservice\Model\Endpoint */ - return $this->getRepository(); + return $this->_endpoint; } /** @@ -211,7 +366,7 @@ public function setWebservice(WebserviceInterface $webservice) * * @return \Muffin\Webservice\Webservice\WebserviceInterface */ - public function getWebservice() + public function getWebservice(): WebserviceInterface { return $this->_webservice; } @@ -229,13 +384,12 @@ public function getWebservice() * a single query. * * @param string $finder The finder method to use. - * @param array $options The options for the finder. - * @return $this Returns a modified query. + * @param mixed ...$args Arguments that match up to finder-specific parameters + * @return static Returns a modified query. */ - public function find($finder, array $options = []) + public function find(string $finder, mixed ...$args): static { - /** @psalm-suppress UndefinedInterfaceMethod */ - return $this->getRepository()->callFinder($finder, $this, $options); + return $this->_endpoint->callFinder($finder, $this, $args); } /** @@ -244,7 +398,7 @@ public function find($finder, array $options = []) * @return mixed The first result from the ResultSet. * @throws \Cake\Datasource\Exception\RecordNotFoundException When there is no first record. */ - public function firstOrFail() + public function firstOrFail(): mixed { $entity = $this->first(); if ($entity) { @@ -253,7 +407,7 @@ public function firstOrFail() /** @psalm-suppress UndefinedInterfaceMethod */ throw new RecordNotFoundException(sprintf( 'Record not found in endpoint "%s"', - $this->getRepository()->getName() + $this->_endpoint->getName() )); } @@ -272,14 +426,16 @@ public function aliasField(string $field, ?string $alias = null): array /** * Apply conditions to the query * - * @param array|null $conditions The conditions to apply - * @param array $types Not used - * @param bool $overwrite Whether to overwrite the current conditions + * @param \Closure|array|string|null $conditions The list of conditions. + * @param array $types Not used, required to comply with QueryInterface. + * @param bool $overwrite Whether or not to replace previous queries. * @return $this - * @psalm-suppress MoreSpecificImplementedParamType */ - public function where($conditions = null, array $types = [], bool $overwrite = false) - { + public function where( + Closure|array|string|null $conditions = null, + array $types = [], + bool $overwrite = false + ) { if ($conditions === null) { return $this->clause('where'); } @@ -292,14 +448,14 @@ public function where($conditions = null, array $types = [], bool $overwrite = f /** * Add AND conditions to the query * - * @param string|array $conditions The conditions to add with AND. + * @param array|string $conditions The conditions to add with AND. * @param array $types associative array of type names used to bind values to query * @return $this * @see \Cake\Database\Query::where() * @see \Cake\Database\Type * @psalm-suppress PossiblyInvalidArgument */ - public function andWhere($conditions, array $types = []) + public function andWhere(string|array $conditions, array $types = []) { $this->where($conditions, $types); @@ -360,12 +516,10 @@ public function page(int $num, ?int $limit = null) * $query->limit(10) // generates LIMIT 10 * ``` * - * @param int $limit number of records to be returned + * @param ?int $limit number of records to be returned * @return $this - * @psalm-suppress MoreSpecificImplementedParamType - * @psalm-suppress ParamNameMismatch */ - public function limit($limit) + public function limit(?int $limit) { $this->_parts['limit'] = $limit; @@ -378,14 +532,14 @@ public function limit($limit) * @param array|null $fields The field to set * @return $this|array */ - public function set($fields = null) + public function set(?array $fields = null) { if ($fields === null) { return $this->clause('set'); } if (!in_array($this->clause('action'), [self::ACTION_CREATE, self::ACTION_UPDATE])) { - throw new \UnexpectedValueException(__('The action of this query needs to be either create update')); + throw new UnexpectedValueException('The action of this query needs to be either create update'); } $this->_parts['set'] = $fields; @@ -416,11 +570,11 @@ public function offset($num) * By default this function will append any passed argument to the list of fields * to be selected, unless the second argument is set to true. * - * @param array|\Cake\Database\ExpressionInterface|\Closure|string $fields fields to be added to the list + * @param \Cake\Database\ExpressionInterface|\Closure|array|string $fields fields to be added to the list * @param bool $overwrite whether to reset order with field list or not * @return $this */ - public function order($fields, $overwrite = false) + public function order(array|ExpressionInterface|Closure|string $fields, bool $overwrite = false) { $this->_parts['order'] = !$overwrite ? Hash::merge($this->clause('order'), $fields) : $fields; @@ -473,13 +627,12 @@ public function count(): int return 0; } - if (!$this->_result) { + if (!$this->_results) { $this->_execute(); } - if ($this->_result) { - /** @psalm-suppress PossiblyInvalidMethodCall, PossiblyUndefinedMethod */ - return (int)$this->_result->total(); + if ($this->_results) { + return (int)$this->_results->total(); } return 0; @@ -495,11 +648,11 @@ public function count(): int * $singleUser = $query->first(); * ``` * - * @return \Cake\Datasource\EntityInterface|array|null the first result from the ResultSet + * @return mixed the first result from the ResultSet */ - public function first() + public function first(): mixed { - if (!$this->_result) { + if ($this->_dirty) { $this->limit(1); } @@ -513,7 +666,7 @@ public function first() * * @return void */ - public function triggerBeforeFind() + public function triggerBeforeFind(): void { if (!$this->_beforeFindFired && $this->clause('action') === self::ACTION_READ) { /** @var \Muffin\Webservice\Model\Endpoint $endpoint */ @@ -530,13 +683,11 @@ public function triggerBeforeFind() /** * Execute the query * - * @return bool|int|\Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet - * @psalm-suppress MoreSpecificReturnType + * @return \Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet|int|bool */ - public function execute() + public function execute(): bool|int|Resource|ResultSetInterface { if ($this->clause('action') === self::ACTION_READ) { - /** @psalm-suppress LessSpecificReturnStatement */ return $this->_execute(); } @@ -551,15 +702,14 @@ public function execute() protected function _execute(): ResultSetInterface { $this->triggerBeforeFind(); - if ($this->_result) { - /** @psalm-var class-string<\Cake\Datasource\ResultSetInterface> $decorator */ - $decorator = $this->_decoratorClass(); + if ($this->_results) { + $decorator = $this->decoratorClass(); - return new $decorator($this->_result); + return new $decorator($this->_results); } /** @var \Cake\Datasource\ResultSetInterface */ - return $this->_result = $this->_webservice->execute($this); + return $this->_results = $this->_webservice->execute($this); } /** @@ -567,7 +717,7 @@ protected function _execute(): ResultSetInterface * * @return array */ - public function __debugInfo() + public function __debugInfo(): array { return [ '(help)' => 'This is a Query object, to get the results execute or iterate it.', @@ -592,21 +742,25 @@ public function __debugInfo() * * @return \Cake\Datasource\ResultSetInterface The data to convert to JSON. */ - public function jsonSerialize() + public function jsonSerialize(): ResultSetInterface { return $this->all(); } /** - * Select the fields to include in the query + * Adds fields to be selected from _source. + * + * Calling this function multiple times will append more fields to the + * list of fields to be selected from _source. * - * @param array|\Cake\Database\ExpressionInterface|string|callable $fields fields to be added to the list. - * @param bool $overwrite whether to reset fields with passed list or not + * If `true` is passed in the second argument, any previous selections + * will be overwritten with the list passed in the first argument. + * + * @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields The list of fields to select from _source. + * @param bool $overwrite Whether or not to replace previous selections. * @return $this - * @see \Cake\Database\Query::select - * @psalm-suppress MoreSpecificImplementedParamType */ - public function select($fields = [], bool $overwrite = false) + public function select(ExpressionInterface|Closure|array|string|int|float $fields, bool $overwrite = false) { if (!is_string($fields) && is_callable($fields)) { $fields = $fields($this); @@ -624,4 +778,193 @@ public function select($fields = [], bool $overwrite = false) return $this; } + + /** + * Returns the name of the class to be used for decorating results + * + * @return class-string<\Cake\Datasource\ResultSetInterface> + */ + protected function decoratorClass(): string + { + return ResultSetDecorator::class; + } + + /** + * Decorates the results iterator with MapReduce routines and formatters + * + * @param iterable $result Original results + * @return \Cake\Datasource\ResultSetInterface + */ + protected function decorateResults(iterable $result): ResultSetInterface + { + $decorator = $this->decoratorClass(); + + if (!empty($this->_mapReduce)) { + foreach ($this->_mapReduce as $functions) { + $result = new MapReduce($result, $functions['mapper'], $functions['reducer']); + } + $result = new $decorator($result); + } + + if (!($result instanceof ResultSetInterface)) { + $result = new $decorator($result); + } + + if (!empty($this->_formatters)) { + foreach ($this->_formatters as $formatter) { + $result = $formatter($result, $this); + } + + if (!($result instanceof ResultSetInterface)) { + $result = new $decorator($result); + } + } + + return $result; + } + + /** + * Register a new MapReduce routine to be executed on top of the database results + * + * The MapReduce routing will only be run when the query is executed and the first + * result is attempted to be fetched. + * + * If the third argument is set to true, it will erase previous map reducers + * and replace it with the arguments passed. + * + * @param \Closure|null $mapper The mapper function + * @param \Closure|null $reducer The reducing function + * @param bool $overwrite Set to true to overwrite existing map + reduce functions. + * @return $this + * @see \Cake\Collection\Iterator\MapReduce for details on how to use emit data to the map reducer. + */ + public function mapReduce(?Closure $mapper = null, ?Closure $reducer = null, bool $overwrite = false) + { + if ($overwrite) { + $this->_mapReduce = []; + } + if ($mapper === null) { + if (!$overwrite) { + throw new InvalidArgumentException('$mapper can be null only when $overwrite is true.'); + } + + return $this; + } + $this->_mapReduce[] = compact('mapper', 'reducer'); + + return $this; + } + + /** + * Returns an array with the custom options that were applied to this query + * and that were not already processed by another method in this class. + * + * ### Example: + * + * ``` + * $query->applyOptions(['doABarrelRoll' => true, 'fields' => ['id', 'name']); + * $query->getOptions(); // Returns ['doABarrelRoll' => true] + * ``` + * + * @see \Cake\Datasource\QueryInterface::applyOptions() to read about the options that will + * be processed by this class and not returned by this function + * @return array + * @see applyOptions() + */ + public function getOptions(): array + { + return $this->_options; + } + + /** + * Returns the current configured query `_eagerLoaded` value + * + * @return bool + */ + public function isEagerLoaded(): bool + { + return $this->_eagerLoaded; + } + + /** + * Sets the query instance to be an eager loaded query. If no argument is + * passed, the current configured query `_eagerLoaded` value is returned. + * + * @param bool $value Whether to eager load. + * @return $this + */ + public function eagerLoaded(bool $value) + { + $this->_eagerLoaded = $value; + + return $this; + } + + /** + * Registers a new formatter callback function that is to be executed when trying + * to fetch the results from the database. + * + * If the second argument is set to true, it will erase previous formatters + * and replace them with the passed first argument. + * + * Callbacks are required to return an iterator object, which will be used as + * the return value for this query's result. Formatter functions are applied + * after all the `MapReduce` routines for this query have been executed. + * + * Formatting callbacks will receive two arguments, the first one being an object + * implementing `\Cake\Collection\CollectionInterface`, that can be traversed and + * modified at will. The second one being the query instance on which the formatter + * callback is being applied. + * + * ### Examples: + * + * Return all results from the table indexed by id: + * + * ``` + * $query->select(['id', 'name'])->formatResults(function ($results) { + * return $results->indexBy('id'); + * }); + * ``` + * + * Add a new column to the ResultSet: + * + * ``` + * $query->select(['name', 'birth_date'])->formatResults(function ($results) { + * return $results->map(function ($row) { + * $row['age'] = $row['birth_date']->diff(new DateTime)->y; + * + * return $row; + * }); + * }); + * ``` + * + * @param \Closure|null $formatter The formatting function + * @param int|bool $mode Whether to overwrite, append or prepend the formatter. + * @return $this + * @throws \InvalidArgumentException + */ + public function formatResults(?Closure $formatter = null, int|bool $mode = self::APPEND) + { + if ($mode === self::OVERWRITE) { + $this->_formatters = []; + } + if ($formatter === null) { + /** @psalm-suppress RedundantCondition */ + if ($mode !== self::OVERWRITE) { + throw new InvalidArgumentException('$formatter can be null only when $mode is overwrite.'); + } + + return $this; + } + + if ($mode === self::PREPEND) { + array_unshift($this->_formatters, $formatter); + + return $this; + } + + $this->_formatters[] = $formatter; + + return $this; + } } diff --git a/src/Datasource/ResultSet.php b/src/Datasource/ResultSet.php index 124cf1b..99b79eb 100644 --- a/src/Datasource/ResultSet.php +++ b/src/Datasource/ResultSet.php @@ -5,8 +5,11 @@ use Cake\Collection\CollectionTrait; use Cake\Datasource\ResultSetInterface; +use IteratorIterator; +use Muffin\Webservice\Model\Resource; -class ResultSet implements ResultSetInterface +/** @package Muffin\Webservice\Datasource */ +class ResultSet extends IteratorIterator implements ResultSetInterface { use CollectionTrait; @@ -15,28 +18,28 @@ class ResultSet implements ResultSetInterface * * @var int */ - protected $_index = 0; + protected int $_index = 0; /** * Last record fetched from the statement * - * @var array + * @var Resource */ - protected $_current; + protected Resource $_current; /** * Results that have been fetched or hydrated into the results. * * @var array */ - protected $_results = []; + protected array $_results = []; /** * Total number of results * * @var int|null */ - protected $_total; + protected ?int $_total = null; /** * Construct the ResultSet @@ -55,9 +58,9 @@ public function __construct(array $resources, ?int $total = null) * * Part of Iterator interface. * - * @return array|object + * @return object|array */ - public function current() + public function current(): array|object { return $this->_current; } @@ -70,7 +73,7 @@ public function current() * @throws \Cake\Database\Exception * @return void */ - public function rewind() + public function rewind(): void { $this->_index = 0; } @@ -82,7 +85,7 @@ public function rewind() * * @return string Serialized object */ - public function serialize() + public function serialize(): string { while ($this->valid()) { $this->next(); @@ -98,7 +101,7 @@ public function serialize() * * @return bool */ - public function valid() + public function valid(): bool { if (!isset($this->_results[$this->key()])) { return false; @@ -116,7 +119,7 @@ public function valid() * * @return int */ - public function key() + public function key(): int { return $this->_index; } @@ -128,7 +131,7 @@ public function key() * * @return void */ - public function next() + public function next(): void { $this->_index++; } @@ -141,7 +144,7 @@ public function next() * @param string $serialized Serialized object * @return void */ - public function unserialize($serialized) + public function unserialize(string $serialized): void { $this->_results = unserialize($serialized); } diff --git a/src/Datasource/Schema.php b/src/Datasource/Schema.php index 062f928..2698486 100644 --- a/src/Datasource/Schema.php +++ b/src/Datasource/Schema.php @@ -20,49 +20,49 @@ class Schema implements SchemaInterface * * @var string */ - protected $_repository; + protected string $_repository; /** * Columns in the endpoint. * * @var array */ - protected $_columns = []; + protected array $_columns = []; /** * A map with columns to types * * @var array */ - protected $_typeMap = []; + protected array $_typeMap = []; /** * Indexes in the endpoint. * * @var array */ - protected $_indexes = []; + protected array $_indexes = []; /** * Constraints in the endpoint. * * @var array */ - protected $_constraints = []; + protected array $_constraints = []; /** * Options for the endpoint. * * @var array */ - protected $_options = []; + protected array $_options = []; /** * Whether or not the endpoint is temporary * * @var bool */ - protected $_temporary = false; + protected bool $_temporary = false; /** * The valid keys that can be used in a column @@ -70,7 +70,7 @@ class Schema implements SchemaInterface * * @var array */ - protected static $_columnKeys = [ + protected static array $_columnKeys = [ 'type' => null, 'baseType' => null, 'length' => null, @@ -86,7 +86,7 @@ class Schema implements SchemaInterface * * @var array */ - protected static $_columnExtras = [ + protected static array $_columnExtras = [ 'string' => [ 'fixed' => null, ], @@ -159,7 +159,7 @@ public function name(): string * @param array|string $attrs The attributes for the column. * @return $this */ - public function addColumn(string $name, $attrs) + public function addColumn(string $name, array|string $attrs) { if (is_string($attrs)) { $attrs = ['type' => $attrs]; @@ -178,7 +178,7 @@ public function addColumn(string $name, $attrs) /** * Get the column names in the endpoint. * - * @return string[] + * @return array */ public function columns(): array { @@ -247,7 +247,7 @@ public function setColumnType(string $name, string $type) * Get the type of a column * * @param string $name Column name - * @return null|string + * @return string|null */ public function getColumnType(string $name): ?string { diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index 110d05c..f268af5 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -3,11 +3,14 @@ namespace Muffin\Webservice\Model; +use ArrayAccess; use ArrayObject; use BadMethodCallException; +use Cake\Collection\CollectionInterface; use Cake\Core\App; use Cake\Datasource\EntityInterface; use Cake\Datasource\Exception\InvalidPrimaryKeyException; +use Cake\Datasource\QueryInterface; use Cake\Datasource\RepositoryInterface; use Cake\Datasource\RulesAwareTrait; use Cake\Datasource\RulesChecker; @@ -17,12 +20,16 @@ use Cake\ORM\Exception\PersistenceFailedException; use Cake\Utility\Inflector; use Cake\Validation\ValidatorAwareTrait; +use Closure; use Muffin\Webservice\Datasource\Connection; use Muffin\Webservice\Datasource\Marshaller; use Muffin\Webservice\Datasource\Query; +use Muffin\Webservice\Datasource\ResultSet; use Muffin\Webservice\Datasource\Schema; use Muffin\Webservice\Model\Exception\MissingResourceClassException; use Muffin\Webservice\Webservice\WebserviceInterface; +use Psr\SimpleCache\CacheInterface; +use function Cake\Core\namespaceSplit; /** * The table equivalent of a webservice endpoint @@ -50,75 +57,75 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp public const VALIDATOR_PROVIDER_NAME = 'endpoint'; /** - * Connection instance this endpoint uses + * The webservice instance to call * - * @var \Muffin\Webservice\Datasource\Connection + * @var \Muffin\Webservice\Webservice\WebserviceInterface */ - protected $_connection; + protected ?WebserviceInterface $_webservice = null; /** - * The schema object containing a description of this endpoint fields + * The alias to use for the endpoint * - * @var \Muffin\Webservice\Datasource\Schema + * @var string|null */ - protected $_schema; + protected ?string $_alias = null; /** - * The name of the class that represent a single resource for this endpoint + * Connection instance this endpoint uses * - * @var string - * @psalm-var class-string<\Muffin\Webservice\Model\Resource> + * @var \Muffin\Webservice\Datasource\Connection|null */ - protected $_resourceClass; + protected ?Connection $_connection = null; /** - * Registry key used to create this endpoint object + * The schema object containing a description of this endpoint fields * - * @var string + * @var \Muffin\Webservice\Datasource\Schema */ - protected $_registryAlias; + protected ?Schema $_schema = null; /** - * The name of the endpoint to contact + * The name of the field that represents the primary key in the endpoint * - * @var string + * @var array|string|null */ - protected $_name; + protected array|string|null $_primaryKey = null; /** - * The name of the field that represents the primary key in the endpoint + * The name of the field that represents a human readable representation of a row * - * @var string|array|null + * @var array|string|null */ - protected $_primaryKey; + protected array|string|null $_displayField = null; /** - * The name of the field that represents a human readable representation of a row + * The name of the endpoint to contact * - * @var string|string[] + * @var string */ - protected $_displayField; + protected ?string $_name = null; /** - * The webservice instance to call + * The name of the class that represent a single resource for this endpoint * - * @var \Muffin\Webservice\Webservice\WebserviceInterface + * @var string + * @psalm-var class-string<\Muffin\Webservice\Model\Resource> */ - protected $_webservice; + protected ?string $_resourceClass = null; /** - * The alias to use for the endpoint + * Registry key used to create this endpoint object * * @var string */ - protected $_alias; + protected ?string $_registryAlias = null; /** * The inflect method to use for endpoint routes * * @var string */ - protected $_inflectionMethod = 'underscore'; + protected string $_inflectionMethod = 'underscore'; /** * Initializes a new instance @@ -304,9 +311,9 @@ public function setConnection(Connection $connection) /** * Returns the connection driver. * - * @return \Muffin\Webservice\Datasource\Connection + * @return \Muffin\Webservice\Datasource\Connection|null */ - public function getConnection(): Connection + public function getConnection(): ?Connection { return $this->_connection; } @@ -323,7 +330,7 @@ public function getConnection(): Connection * @param \Muffin\Webservice\Datasource\Schema|array $schema Either an array of fields and config, or a schema object * @return $this */ - public function setSchema($schema) + public function setSchema(Schema|array $schema) { if (is_array($schema)) { $schema = new Schema($this->getName(), $schema); @@ -394,10 +401,10 @@ public function hasField(string $field): bool /** * Returns the primary key field name * - * @param string|array|null $key sets a new name to be used as primary key + * @param array|string|null $key sets a new name to be used as primary key * @return $this */ - public function setPrimaryKey($key) + public function setPrimaryKey(string|array|null $key) { $this->_primaryKey = $key; @@ -410,7 +417,7 @@ public function setPrimaryKey($key) * @return array|string * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no schema exists to fetch the key from */ - public function getPrimaryKey() + public function getPrimaryKey(): array|string { if ($this->_primaryKey === null) { $schema = $this->getSchema(); @@ -427,10 +434,10 @@ public function getPrimaryKey() /** * Sets the endpoint display field * - * @param string|string[] $field The new field to use as the display field + * @param array|string $field The new field to use as the display field * @return $this */ - public function setDisplayField($field) + public function setDisplayField(string|array $field) { $this->_displayField = $field; @@ -440,10 +447,10 @@ public function setDisplayField($field) /** * Get the endpoints current display field * - * @return string|string[] + * @return array|string * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no schema exists to fetch the key from */ - public function getDisplayField() + public function getDisplayField(): string|array { if ($this->_displayField === null) { $primary = (array)$this->getPrimaryKey(); @@ -575,14 +582,14 @@ public function getWebservice(): WebserviceInterface * listeners. Any listener can set a valid result set using $query * * @param string $type the type of query to perform - * @param array $options An array that will be passed to Query::applyOptions() - * @return \Muffin\Webservice\Datasource\Query + * @param mixed ...$args Arguments that match up to finder-specific parameters + * @return \Cake\Datasource\QueryInterface */ - public function find(string $type = 'all', array $options = []): Query + public function find(string $type = 'all', mixed ...$args): QueryInterface { $query = $this->query()->read(); - return $this->callFinder($type, $query, $options); + return $this->callFinder($type, $query, $args); } /** @@ -659,24 +666,29 @@ public function findAll(Query $query, array $options): Query */ public function findList(Query $query, array $options): Query { + + debug($options); $options += [ 'keyField' => $this->getPrimaryKey(), 'valueField' => $this->getDisplayField(), 'groupField' => null, ]; + debug($options); $options = $this->_setFieldMatchers( $options, ['keyField', 'valueField', 'groupField'] ); - return $query->formatResults(function ($results) use ($options) { + debug($options); + return $query->formatResults(function (CollectionInterface $results) use ($options) { return $results->combine( $options['keyField'], $options['valueField'], $options['groupField'] ); }); + } /** @@ -694,6 +706,7 @@ public function findList(Query $query, array $options): Query */ protected function _setFieldMatchers(array $options, array $keys): array { + debug($options); foreach ($keys as $field) { if (!is_array($options[$field])) { continue; @@ -714,6 +727,7 @@ protected function _setFieldMatchers(array $options, array $keys): array return implode(';', $matches); }; } + debug($options); return $options; } @@ -732,13 +746,23 @@ protected function _setFieldMatchers(array $options, array $keys): array * ``` * * @param mixed $primaryKey primary key value to find - * @param array $options Options. - * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id could not be found + * @param array|string $finder The finder to use. Passing an options array is deprecated. + * @param \Psr\SimpleCache\CacheInterface|string|null $cache The cache config to use. + * Defaults to `null`, i.e. no caching. + * @param \Closure|string|null $cacheKey The cache key to use. If not provided + * one will be autogenerated if `$cache` is not null. + * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id + * could not be found * @return \Cake\Datasource\EntityInterface * @see \Cake\Datasource\RepositoryInterface::find() */ - public function get($primaryKey, array $options = []): EntityInterface - { + public function get( + mixed $primaryKey, + array|string $finder = 'all', + CacheInterface|string|null $cache = null, + Closure|string|null $cacheKey = null, + mixed ...$args + ): EntityInterface { $key = (array)$this->getPrimaryKey(); $alias = $this->getAlias(); foreach ($key as $index => $keyname) { @@ -764,7 +788,7 @@ public function get($primaryKey, array $options = []): EntityInterface $finder = $options['finder'] ?? 'all'; unset($options['key'], $options['cache'], $options['finder']); - $query = $this->find($finder, $options)->where($conditions); + $query = $this->find($finder, $args)->where($conditions); if ($cacheConfig) { if (!$cacheKey) { @@ -775,7 +799,7 @@ public function get($primaryKey, array $options = []): EntityInterface json_encode($primaryKey) ); } - $query->cache($cacheKey, $cacheConfig); + $cache($cacheKey, $cacheConfig); } return $query->firstOrFail(); @@ -800,7 +824,7 @@ public function get($primaryKey, array $options = []): EntityInterface * @return \Cake\Datasource\EntityInterface|array An entity. * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved */ - public function findOrCreate($search, ?callable $callback = null) + public function findOrCreate(mixed $search, ?callable $callback = null): EntityInterface|array { $query = $this->find()->where($search); $row = $query->first(); @@ -839,12 +863,12 @@ public function query(): Query * This method will *not* trigger beforeSave/afterSave events. If you need those * first load a collection of records and update them. * - * @param array $fields A hash of field => new value. - * @param mixed $conditions Conditions to be used, accepts anything Query::where() can take. + * @param Closure|array|string = array(); $fields A hash of field => new value. + * @param \Closure|array|string|null $conditions Conditions to be used, accepts anything Query::where() can take. * @return int Count Returns the affected rows. * @psalm-suppress MoreSpecificImplementedParamType */ - public function updateAll($fields, $conditions): int + public function updateAll(Closure|array|string $fields, Closure|array|string|null $conditions): int { /** @psalm-suppress PossiblyInvalidMethodCall, PossiblyUndefinedMethod */ return $this->query()->update()->where($conditions)->set($fields)->execute()->count(); @@ -864,7 +888,7 @@ public function updateAll($fields, $conditions): int * @psalm-suppress InvalidReturnStatement * @psalm-suppress InvalidReturnType */ - public function deleteAll($conditions): int + public function deleteAll(mixed $conditions): int { return $this->query()->delete()->where($conditions)->execute(); } @@ -876,7 +900,7 @@ public function deleteAll($conditions): int * @param mixed $conditions list of conditions to pass to the query * @return bool */ - public function exists($conditions): bool + public function exists(mixed $conditions): bool { return $this->find()->where($conditions)->count() > 0; } @@ -887,10 +911,10 @@ public function exists($conditions): bool * of any error. * * @param \Cake\Datasource\EntityInterface $entity the resource to be saved - * @param array|\ArrayAccess $options The options to use when saving. + * @param \ArrayAccess|array $options The options to use when saving. * @return \Cake\Datasource\EntityInterface|false */ - public function save(EntityInterface $entity, $options = []) + public function save(EntityInterface $entity, array|ArrayAccess $options = []): EntityInterface|false { $options = new ArrayObject((array)$options + [ 'checkRules' => true, @@ -958,10 +982,10 @@ public function save(EntityInterface $entity, $options = []) * Delete a single resource. * * @param \Cake\Datasource\EntityInterface $entity The resource to remove. - * @param array|\ArrayAccess $options The options for the delete. + * @param \ArrayAccess|array $options The options for the delete. * @return bool */ - public function delete(EntityInterface $entity, $options = []): bool + public function delete(EntityInterface $entity, array|ArrayAccess $options = []): bool { $primaryKeys = (array)$this->getPrimaryKey(); $values = $entity->extract($primaryKeys); @@ -1001,7 +1025,7 @@ public function callFinder(string $type, Query $query, array $options = []): Que return $this->{$finder}($query, $options); } - throw new \BadMethodCallException( + throw new BadMethodCallException( sprintf('Unknown finder method "%s"', $type) ); } @@ -1014,7 +1038,7 @@ public function callFinder(string $type, Query $query, array $options = []): Que * @return mixed * @throws \BadMethodCallException when there are missing arguments, or when and & or are combined. */ - protected function _dynamicFinder(string $method, array $args) + protected function _dynamicFinder(string $method, array $args): mixed { $method = Inflector::underscore($method); preg_match('/^find_([\w]+)_by_/', $method, $matches); @@ -1077,7 +1101,7 @@ protected function _dynamicFinder(string $method, array $args) * @return mixed * @throws \BadMethodCallException If the request dynamic finder cannot be found */ - public function __call($method, $args) + public function __call(string $method, array $args): mixed { if (preg_match('/^find(?:\w+)?By/', $method) > 0) { return $this->_dynamicFinder($method, $args); @@ -1182,7 +1206,7 @@ public function patchEntity(EntityInterface $entity, array $data, array $options * $article = $this->Articles->patchEntities($articles, $this->request->data()); * ``` * - * @param array|\Traversable $entities the entities that will get the + * @param \Traversable|array $entities the entities that will get the * data merged in * @param array $data list of arrays to be merged into the entities * @param array $options A list of options for the objects hydration. @@ -1265,15 +1289,20 @@ public function buildRules(RulesChecker $rules): RulesChecker * * @return array */ - public function __debugInfo() + public function __debugInfo(): array { + $connectionName = ''; + if ($this->getConnection() !== null) { + $connectionName = $this->getConnection()->configName() ?? 'None'; + } + return [ 'registryAlias' => $this->getRegistryAlias(), 'alias' => $this->getAlias(), 'endpoint' => $this->getName(), 'resourceClass' => $this->getResourceClass(), 'defaultConnection' => $this->defaultConnectionName(), - 'connectionName' => $this->getConnection()->configName(), + 'connectionName' => $connectionName, 'inflector' => $this->getInflectionMethod(), ]; } @@ -1284,7 +1313,7 @@ public function __debugInfo() * @param string $alias Alias for this endpoint * @return $this */ - public function setAlias($alias) + public function setAlias(string $alias) { $this->_alias = $alias; diff --git a/src/Model/EndpointLocator.php b/src/Model/EndpointLocator.php index 7e8081f..7310549 100644 --- a/src/Model/EndpointLocator.php +++ b/src/Model/EndpointLocator.php @@ -10,6 +10,7 @@ use Cake\Datasource\RepositoryInterface; use Cake\Utility\Inflector; use Muffin\Webservice\Datasource\Connection; +use function Cake\Core\pluginSplit; /** * Class EndpointLocator @@ -40,7 +41,6 @@ public function set(string $alias, RepositoryInterface $repository): Endpoint */ public function get(string $alias, array $options = []): Endpoint { - /** @var \Muffin\Webservice\Model\Endpoint */ return parent::get($alias, $options); } @@ -49,9 +49,9 @@ public function get(string $alias, array $options = []): Endpoint * * @param string $alias Endpoint alias. * @param array $options The alias to check for. - * @return \Muffin\Webservice\Model\Endpoint + * @return \Cake\Datasource\RepositoryInterface */ - protected function createInstance(string $alias, array $options) + protected function createInstance(string $alias, array $options): RepositoryInterface { [, $classAlias] = pluginSplit($alias); $options = ['alias' => $classAlias] + $options; @@ -77,7 +77,6 @@ protected function createInstance(string $alias, array $options) if (strpos($alias, '.') === false) { $connectionName = 'webservice'; } else { - /** @psalm-suppress PossiblyNullArgument */ $pluginParts = explode('/', pluginSplit($alias)[0]); $connectionName = Inflector::underscore(end($pluginParts)); } @@ -111,7 +110,6 @@ protected function getConnection(string $connectionName): Connection $message = $e->getMessage() . ' You can override Endpoint::defaultConnectionName() to return the connection name you want.'; - /** @psalm-suppress PossiblyInvalidArgument */ throw new MissingDatasourceConfigException($message, $e->getCode(), $e->getPrevious()); } } diff --git a/src/Model/Exception/MissingEndpointSchemaException.php b/src/Model/Exception/MissingEndpointSchemaException.php index a1a4891..1ecc202 100644 --- a/src/Model/Exception/MissingEndpointSchemaException.php +++ b/src/Model/Exception/MissingEndpointSchemaException.php @@ -12,5 +12,5 @@ class MissingEndpointSchemaException extends CakeException * * @var string */ - protected $_messageTemplate = 'Missing schema %s or webservice %s describe implementation'; + protected string $_messageTemplate = 'Missing schema %s or webservice %s describe implementation'; } diff --git a/src/Model/Exception/MissingResourceClassException.php b/src/Model/Exception/MissingResourceClassException.php index 4acacd6..5ee6c97 100644 --- a/src/Model/Exception/MissingResourceClassException.php +++ b/src/Model/Exception/MissingResourceClassException.php @@ -7,5 +7,5 @@ class MissingResourceClassException extends CakeException { - protected $_messageTemplate = 'Resource class %s could not be found.'; + protected string $_messageTemplate = 'Resource class %s could not be found.'; } diff --git a/src/Model/ResourceBasedEntityInterface.php b/src/Model/ResourceBasedEntityInterface.php index 66b1d37..cd2d9a4 100644 --- a/src/Model/ResourceBasedEntityInterface.php +++ b/src/Model/ResourceBasedEntityInterface.php @@ -20,5 +20,5 @@ public function applyResource(Resource $resource): void; * @param array $options The options to pass to the constructor * @return self */ - public static function createFromResource(Resource $resource, array $options = []); + public static function createFromResource(Resource $resource, array $options = []): self; } diff --git a/src/Model/ResourceBasedEntityTrait.php b/src/Model/ResourceBasedEntityTrait.php index 45a5576..b173b66 100644 --- a/src/Model/ResourceBasedEntityTrait.php +++ b/src/Model/ResourceBasedEntityTrait.php @@ -23,7 +23,7 @@ public function applyResource(Resource $resource): void * @param array $options The options to pass to the constructor * @return self */ - public static function createFromResource(Resource $resource, array $options = []) + public static function createFromResource(Resource $resource, array $options = []): self { $entity = new self(); diff --git a/src/Plugin.php b/src/Plugin.php index b08531c..687ee86 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -15,21 +15,21 @@ class Plugin extends BasePlugin * * @var bool */ - protected $routesEnabled = false; + protected bool $routesEnabled = false; /** * Disable middleware hook. * * @var bool */ - protected $middlewareEnabled = false; + protected bool $middlewareEnabled = false; /** * Disable console hook. * * @var bool */ - protected $consoleEnabled = false; + protected bool $consoleEnabled = false; /** * @inheritDoc diff --git a/src/Webservice/Driver/AbstractDriver.php b/src/Webservice/Driver/AbstractDriver.php index 1662168..61d323b 100644 --- a/src/Webservice/Driver/AbstractDriver.php +++ b/src/Webservice/Driver/AbstractDriver.php @@ -12,6 +12,7 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; +use function Cake\Core\pluginSplit; abstract class AbstractDriver implements LoggerAwareInterface { @@ -23,28 +24,28 @@ abstract class AbstractDriver implements LoggerAwareInterface * * @var object */ - protected $_client; + protected ?object $_client = null; /** * Default config * * @var array */ - protected $_defaultConfig = []; + protected array $_defaultConfig = []; /** * Whatever queries should be logged * * @var bool */ - protected $_logQueries = false; + protected bool $_logQueries = false; /** * The list of webservices to be used * * @var array */ - protected $_webservices = []; + protected array $_webservices = []; /** * Constructor. @@ -83,9 +84,9 @@ public function setClient(object $client) /** * Get the client instance configured for this driver * - * @return object + * @return object|null */ - public function getClient(): object + public function getClient(): ?object { return $this->_client; } @@ -132,14 +133,12 @@ public function getWebservice(string $name): WebserviceInterface * Sets a logger * * @param \Psr\Log\LoggerInterface $logger Logger object - * @return $this + * @return void * @psalm-suppress ImplementedReturnTypeMismatch */ - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; - - return $this; } /** @@ -205,7 +204,7 @@ public function isQueryLoggingEnabled(): bool * @throws \RuntimeException If the client object has not been initialized. * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException If the method does not exist in the client. */ - public function __call($method, $args) + public function __call(string $method, array $args): mixed { if (!method_exists($this->getClient(), $method)) { throw new UnimplementedWebserviceMethodException([ @@ -222,7 +221,7 @@ public function __call($method, $args) * * @return array */ - public function __debugInfo() + public function __debugInfo(): array { return [ 'client' => $this->getClient(), diff --git a/src/Webservice/Exception/MissingDriverException.php b/src/Webservice/Exception/MissingDriverException.php index be71b8d..debb604 100644 --- a/src/Webservice/Exception/MissingDriverException.php +++ b/src/Webservice/Exception/MissingDriverException.php @@ -12,5 +12,5 @@ class MissingDriverException extends CakeException * * @var string */ - protected $_messageTemplate = 'Webservice driver %s could not be found.'; + protected string $_messageTemplate = 'Webservice driver %s could not be found.'; } diff --git a/src/Webservice/Exception/MissingWebserviceClassException.php b/src/Webservice/Exception/MissingWebserviceClassException.php index 9cdb7bc..0844421 100644 --- a/src/Webservice/Exception/MissingWebserviceClassException.php +++ b/src/Webservice/Exception/MissingWebserviceClassException.php @@ -7,5 +7,5 @@ class MissingWebserviceClassException extends CakeException { - protected $_messageTemplate = 'Webservice class %s (and fallback %s) could not be found.'; + protected string $_messageTemplate = 'Webservice class %s (and fallback %s) could not be found.'; } diff --git a/src/Webservice/Exception/UnexpectedDriverException.php b/src/Webservice/Exception/UnexpectedDriverException.php index 3138a96..f45acb3 100644 --- a/src/Webservice/Exception/UnexpectedDriverException.php +++ b/src/Webservice/Exception/UnexpectedDriverException.php @@ -12,5 +12,5 @@ class UnexpectedDriverException extends CakeException * * @var string */ - protected $_messageTemplate = 'Driver (`%s`) should extend `Muffin\Webservice\Webservice\Driver\AbstractDriver`'; + protected string $_messageTemplate = 'Driver (`%s`) should extend `Muffin\Webservice\Webservice\Driver\AbstractDriver`'; } diff --git a/src/Webservice/Exception/UnimplementedDriverMethodException.php b/src/Webservice/Exception/UnimplementedDriverMethodException.php index 19ea970..333d1bb 100644 --- a/src/Webservice/Exception/UnimplementedDriverMethodException.php +++ b/src/Webservice/Exception/UnimplementedDriverMethodException.php @@ -12,5 +12,5 @@ class UnimplementedDriverMethodException extends CakeException * * @var string */ - protected $_messageTemplate = 'Driver (`%s`) does not implement `%s`'; + protected string $_messageTemplate = 'Driver (`%s`) does not implement `%s`'; } diff --git a/src/Webservice/Exception/UnimplementedWebserviceMethodException.php b/src/Webservice/Exception/UnimplementedWebserviceMethodException.php index 840521e..ff5e5e9 100644 --- a/src/Webservice/Exception/UnimplementedWebserviceMethodException.php +++ b/src/Webservice/Exception/UnimplementedWebserviceMethodException.php @@ -12,5 +12,5 @@ class UnimplementedWebserviceMethodException extends CakeException * * @var string */ - protected $_messageTemplate = 'Webservice %s does not implement %s'; + protected string $_messageTemplate = 'Webservice %s does not implement %s'; } diff --git a/src/Webservice/Webservice.php b/src/Webservice/Webservice.php index 54d3c6f..cf8238b 100644 --- a/src/Webservice/Webservice.php +++ b/src/Webservice/Webservice.php @@ -7,6 +7,7 @@ use Cake\Utility\Inflector; use Cake\Utility\Text; use Muffin\Webservice\Datasource\Query; +use Muffin\Webservice\Datasource\ResultSet; use Muffin\Webservice\Datasource\Schema; use Muffin\Webservice\Model\Endpoint; use Muffin\Webservice\Model\Exception\MissingEndpointSchemaException; @@ -15,6 +16,7 @@ use Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException; use Psr\Log\LoggerInterface; use RuntimeException; +use function Cake\Core\pluginSplit; /** * Basic implementation of a webservice @@ -28,21 +30,21 @@ abstract class Webservice implements WebserviceInterface * * @var \Muffin\Webservice\Webservice\Driver\AbstractDriver */ - protected $_driver; + protected AbstractDriver $_driver; /** * The webservice to call * * @var string */ - protected $_endpoint; + protected ?string $_endpoint = null; /** * A list of nested resources with their path and needed conditions * * @var array */ - protected $_nestedResources = []; + protected array $_nestedResources = []; /** * Construct the webservice @@ -161,9 +163,9 @@ public function nestedResource(array $conditions): ?string * * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return bool|int|\Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet + * @return \Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet|int|bool */ - public function execute(Query $query, array $options = []) + public function execute(Query $query, array $options = []): bool|int|Resource|ResultSet { $result = $this->_executeQuery($query, $options); @@ -206,11 +208,11 @@ public function describe(string $endpoint): Schema * * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return bool|int|\Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet + * @return \Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet|int|bool * @psalm-suppress NullableReturnStatement * @psalm-suppress InvalidNullableReturnType */ - protected function _executeQuery(Query $query, array $options = []) + protected function _executeQuery(Query $query, array $options = []): bool|int|Resource|ResultSet { switch ($query->clause('action')) { case Query::ACTION_CREATE: @@ -231,11 +233,11 @@ protected function _executeQuery(Query $query, array $options = []) * * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return bool|\Muffin\Webservice\Model\Resource + * @return \Muffin\Webservice\Model\Resource|bool * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been * implemented into userland classes */ - protected function _executeCreateQuery(Query $query, array $options = []) + protected function _executeCreateQuery(Query $query, array $options = []): bool|Resource { throw new UnimplementedWebserviceMethodException([ 'name' => static::class, @@ -248,11 +250,11 @@ protected function _executeCreateQuery(Query $query, array $options = []) * * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return bool|\Muffin\Webservice\Datasource\ResultSet + * @return \Muffin\Webservice\Datasource\ResultSet|bool * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been * implemented into userland classes */ - protected function _executeReadQuery(Query $query, array $options = []) + protected function _executeReadQuery(Query $query, array $options = []): bool|ResultSet { throw new UnimplementedWebserviceMethodException([ 'name' => static::class, @@ -265,11 +267,11 @@ protected function _executeReadQuery(Query $query, array $options = []) * * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return int|bool|\Muffin\Webservice\Model\Resource + * @return \Muffin\Webservice\Model\Resource|int|bool * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been * implemented into userland classes */ - protected function _executeUpdateQuery(Query $query, array $options = []) + protected function _executeUpdateQuery(Query $query, array $options = []): int|bool|Resource { throw new UnimplementedWebserviceMethodException([ 'name' => static::class, @@ -286,7 +288,7 @@ protected function _executeUpdateQuery(Query $query, array $options = []) * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been * implemented into userland classes */ - protected function _executeDeleteQuery(Query $query, array $options = []) + protected function _executeDeleteQuery(Query $query, array $options = []): int|bool { throw new UnimplementedWebserviceMethodException([ 'name' => static::class, @@ -335,7 +337,7 @@ protected function _logQuery(Query $query, LoggerInterface $logger): void * * @param \Muffin\Webservice\Model\Endpoint $endpoint The endpoint class to use * @param array $results Array of results from the API - * @return \Muffin\Webservice\Model\Resource[] Array of resource objects + * @return array<\Muffin\Webservice\Model\Resource> Array of resource objects */ protected function _transformResults(Endpoint $endpoint, array $results): array { @@ -370,7 +372,7 @@ protected function _transformResource(Endpoint $endpoint, array $result): Resour * * @return array */ - public function __debugInfo() + public function __debugInfo(): array { return [ 'driver' => $this->_driver, diff --git a/src/Webservice/WebserviceInterface.php b/src/Webservice/WebserviceInterface.php index 6c41616..32d4d9c 100644 --- a/src/Webservice/WebserviceInterface.php +++ b/src/Webservice/WebserviceInterface.php @@ -4,7 +4,9 @@ namespace Muffin\Webservice\Webservice; use Muffin\Webservice\Datasource\Query; +use Muffin\Webservice\Datasource\ResultSet; use Muffin\Webservice\Datasource\Schema; +use Muffin\Webservice\Model\Resource; /** * Describes a webservice used to call a API @@ -18,9 +20,9 @@ interface WebserviceInterface * * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return bool|int|\Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet + * @return \Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet|int|bool */ - public function execute(Query $query, array $options = []); + public function execute(Query $query, array $options = []): bool|int|Resource|ResultSet; /** * Returns a schema for the provided endpoint diff --git a/tests/TestCase/AbstractDriverTest.php b/tests/TestCase/AbstractDriverTest.php index 5f43f40..df9f354 100644 --- a/tests/TestCase/AbstractDriverTest.php +++ b/tests/TestCase/AbstractDriverTest.php @@ -6,7 +6,8 @@ use Cake\Http\Client; use Cake\TestSuite\TestCase; use SomeVendor\SomePlugin\Webservice\Driver\SomePlugin; -use TestApp\Webservice\Driver\Test; +use StdClass; +use TestApp\Webservice\Driver\TestDriver; use TestApp\Webservice\Logger; use TestApp\Webservice\TestWebservice; use TestPlugin\Webservice\Driver\TestPlugin; @@ -37,9 +38,9 @@ public function testWebserviceWithVendor() public function testSetClient() { - $client = new \StdClass(); + $client = new StdClass(); - $driver = new Test(); + $driver = new TestDriver(); $driver->setClient($client); $this->assertSame($client, $driver->getClient()); @@ -47,7 +48,7 @@ public function testSetClient() public function testEnableQueryLogging() { - $driver = new Test(); + $driver = new TestDriver(); $driver->enableQueryLogging(); $this->assertTrue($driver->isQueryLoggingEnabled()); @@ -55,7 +56,7 @@ public function testEnableQueryLogging() public function testDisableQueryLogging() { - $driver = new Test(); + $driver = new TestDriver(); $driver->disableQueryLogging(); $this->assertFalse($driver->isQueryLoggingEnabled()); @@ -73,7 +74,7 @@ public function testDebugInfo() 'webservices' => ['example'], ]; - $driver = new Test(); + $driver = new TestDriver(); $driver->setLogger($logger); $driver ->setClient($client) diff --git a/tests/TestCase/BootstrapTest.php b/tests/TestCase/BootstrapTest.php index d7e070e..88af0af 100644 --- a/tests/TestCase/BootstrapTest.php +++ b/tests/TestCase/BootstrapTest.php @@ -3,7 +3,6 @@ namespace Muffin\Webservice\Test\TestCase; -use Cake\Controller\Controller; use Cake\Datasource\ConnectionManager; use Cake\Datasource\FactoryLocator; use Cake\TestSuite\TestCase; @@ -24,16 +23,15 @@ public function setUp(): void * * @return void */ - public function testLoadingEndpointWithLoadModel() + public function testLoadingEndpointWithLocator() { $connection = new Connection([ 'name' => 'test', 'service' => 'Test', ]); ConnectionManager::setConfig('test_app', $connection); - - $controller = new Controller(); - $endpoint = $controller->loadModel('Test', 'Endpoint'); + $endpointlocator = new EndpointLocator(); + $endpoint = $endpointlocator->get('Test'); $this->assertInstanceOf(TestEndpoint::class, $endpoint); $this->assertEquals('Test', $endpoint->getAlias()); diff --git a/tests/TestCase/ConnectionTest.php b/tests/TestCase/ConnectionTest.php index 7347738..318286b 100644 --- a/tests/TestCase/ConnectionTest.php +++ b/tests/TestCase/ConnectionTest.php @@ -31,7 +31,7 @@ public function testConstructorMissingDriver() new Connection([ 'name' => 'test', - 'service' => 'MissingDriver', + 'service' => 'Missing', ]); } @@ -43,4 +43,9 @@ public function testConstructorNoDriver() 'name' => 'test', ]); } + + public function testConfigName() + { + debug($this->connection->configName()); + } } diff --git a/tests/TestCase/MarshallerTest.php b/tests/TestCase/MarshallerTest.php index fbad5d6..e8124e9 100644 --- a/tests/TestCase/MarshallerTest.php +++ b/tests/TestCase/MarshallerTest.php @@ -45,7 +45,7 @@ public function testOne() [ 'title' => 'Testing one', 'body' => 'Testing the marshaller', - ] + ], ); $this->assertInstanceOf(Resource::class, $result); @@ -63,6 +63,7 @@ public function testOneWithFieldList() ], [ 'fieldList' => ['title'], + 'validate' => false, ] ); @@ -80,6 +81,7 @@ public function testOneWithAccessibleFields() ], [ 'accessibleFields' => ['body' => false], + 'validate' => false, ] ); @@ -97,6 +99,7 @@ public function testOneWithNoFields() ], [ 'fieldList' => [], + 'validate' => false, ] ); @@ -114,6 +117,7 @@ public function testOneWithNoAccessible() ], [ 'accessibleFields' => ['title' => false, 'body' => false], + 'validate' => false, ] ); @@ -137,6 +141,7 @@ public function testOneEnsuringFieldListBeforeAccessible() [ 'fieldList' => ['title', 'body'], 'accessibleFields' => ['title' => false, 'body' => false], + 'validate' => false, ] ); diff --git a/tests/TestCase/Model/EndpointLocatorTest.php b/tests/TestCase/Model/EndpointLocatorTest.php index e64e524..1252854 100644 --- a/tests/TestCase/Model/EndpointLocatorTest.php +++ b/tests/TestCase/Model/EndpointLocatorTest.php @@ -82,7 +82,7 @@ public function testGetException() { $this->expectException(MissingDatasourceConfigException::class); $this->expectExceptionMessage( - 'The datasource configuration "non-existent" was not found.' + 'The datasource configuration `non-existent` was not found.' . ' You can override Endpoint::defaultConnectionName() to return the connection name you want.' ); @@ -93,13 +93,14 @@ public function testGetException() public function testGetWithExistingObject() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('You cannot configure "First", it already exists in the registry.'); + $this->expectExceptionMessage('You cannot configure `First`, it already exists in the registry.'); $result = $this->Locator->get('First', [ 'className' => Endpoint::class, 'registryAlias' => 'First', 'connection' => 'test', ]); + // debug($result); $this->assertInstanceOf(Endpoint::class, $result); $this->Locator->get('First', ['registryAlias' => 'NotFirst']); diff --git a/tests/TestCase/Model/EndpointTest.php b/tests/TestCase/Model/EndpointTest.php index 8d26ce9..4d11bce 100644 --- a/tests/TestCase/Model/EndpointTest.php +++ b/tests/TestCase/Model/EndpointTest.php @@ -50,7 +50,7 @@ public function setUp(): void ]); } - public function providerEndpointNames() + public static function providerEndpointNames(): array { return [ 'No inflector' => ['user-groups', null, 'user_groups'], @@ -102,7 +102,9 @@ public function testFindList() 1 => 'Hello World', 2 => 'New ORM', 3 => 'Webservices', - ], $this->endpoint->find('list')->toArray()); + ], $this->endpoint->find('list')->toArray(), + 'Id => valueField' + ); $this->assertEquals([ 'Hello World' => 'Some text', diff --git a/tests/TestCase/QueryTest.php b/tests/TestCase/QueryTest.php index 6fa31cc..ef14513 100644 --- a/tests/TestCase/QueryTest.php +++ b/tests/TestCase/QueryTest.php @@ -4,6 +4,7 @@ namespace Muffin\Webservice\Test\TestCase; use Cake\Database\Expression\ComparisonExpression; +use Cake\Datasource\ResultSetInterface; use Cake\TestSuite\TestCase; use Muffin\Webservice\Datasource\Query; use Muffin\Webservice\Datasource\ResultSet; @@ -284,7 +285,7 @@ public function testSelectWithCallable() { $fields = ['id', 'username', 'email', 'biography']; - $callable = function (Query $query) use ($fields) { + $callable = function () use ($fields) { return $fields; }; $this->query->select($callable); diff --git a/tests/TestCase/Webservice/WebserviceTest.php b/tests/TestCase/Webservice/WebserviceTest.php index 8dca7d7..fea92e1 100644 --- a/tests/TestCase/Webservice/WebserviceTest.php +++ b/tests/TestCase/Webservice/WebserviceTest.php @@ -8,7 +8,7 @@ use Muffin\Webservice\Model\Endpoint; use Muffin\Webservice\Model\Exception\MissingEndpointSchemaException; use Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException; -use TestApp\Webservice\Driver\Test; +use TestApp\Webservice\Driver\TestDriver; use TestApp\Webservice\TestWebservice; class WebserviceTest extends TestCase @@ -26,7 +26,7 @@ public function setUp(): void parent::setUp(); $this->webservice = new TestWebservice([ - 'driver' => new Test([]), + 'driver' => new TestDriver([]), ]); } @@ -42,7 +42,7 @@ public function tearDown(): void public function testConstructor() { - $testDriver = new Test([]); + $testDriver = new TestDriver([]); $webservice = new TestWebservice([ 'driver' => $testDriver, diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 76891ea..5864ad3 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -18,7 +18,7 @@ use Cake\Datasource\ConnectionManager; use Cake\Log\Log; use Muffin\Webservice\Datasource\Connection; -use TestApp\Webservice\Driver\Test as TestDriver; +use TestApp\Webservice\Driver\TestDriver; require_once 'vendor/autoload.php'; @@ -89,7 +89,7 @@ ConnectionManager::setConfig('test', [ 'className' => Connection::class, 'driver' => TestDriver::class, -] + ConnectionManager::parseDsn(env('DB_DSN'))); +] + ConnectionManager::parseDsn(getenv('DB_DSN'))); Log::setConfig([ 'debug' => [ diff --git a/tests/test_app/src/Webservice/Driver/Test.php b/tests/test_app/src/Webservice/Driver/TestDriver.php similarity index 93% rename from tests/test_app/src/Webservice/Driver/Test.php rename to tests/test_app/src/Webservice/Driver/TestDriver.php index f451e6f..5699610 100644 --- a/tests/test_app/src/Webservice/Driver/Test.php +++ b/tests/test_app/src/Webservice/Driver/TestDriver.php @@ -7,7 +7,7 @@ use Muffin\Webservice\Webservice\WebserviceInterface; use TestApp\Webservice\EndpointTestWebservice; -class Test extends AbstractDriver +class TestDriver extends AbstractDriver { /** * Initialize is used to easily extend the constructor. diff --git a/tests/test_app/src/Webservice/EndpointTestWebservice.php b/tests/test_app/src/Webservice/EndpointTestWebservice.php index 60de65b..4b8e8f8 100644 --- a/tests/test_app/src/Webservice/EndpointTestWebservice.php +++ b/tests/test_app/src/Webservice/EndpointTestWebservice.php @@ -3,6 +3,7 @@ namespace TestApp\Webservice; +use Cake\Utility\Hash; use Muffin\Webservice\Datasource\Query; use Muffin\Webservice\Datasource\ResultSet; use Muffin\Webservice\Model\Resource; @@ -44,7 +45,7 @@ public function initialize(): void ]; } - protected function _executeCreateQuery(Query $query, array $options = []) + protected function _executeCreateQuery(Query $query, array $options = []): bool|Resource { $fields = $query->set(); @@ -52,15 +53,16 @@ protected function _executeCreateQuery(Query $query, array $options = []) return false; } - $this->resources[] = new Resource($fields, [ + $resource = new Resource($fields, [ 'markNew' => false, 'markClean' => true, ]); + $this->resources[] = $resource; - return true; + return $resource; } - protected function _executeReadQuery(Query $query, array $options = []) + protected function _executeReadQuery(Query $query, array $options = []): bool|ResultSet { if (!empty($query->where()['id'])) { $index = $this->conditionsToIndex($query->where()); @@ -73,11 +75,12 @@ protected function _executeReadQuery(Query $query, array $options = []) $this->resources[$index], ], 1); } - if (isset($query->where()[$query->getEndpoint()->aliasField('title')])) { + $conditions = $this->extractConditions($query->getOptions()); + if (isset($conditions[$query->getEndpoint()->aliasField('title')])) { $resources = []; foreach ($this->resources as $resource) { - if ($resource->title !== $query->where()[$query->getEndpoint()->aliasField('title')]) { + if ($resource->title !== $conditions[$query->getEndpoint()->aliasField('title')]) { continue; } @@ -90,7 +93,7 @@ protected function _executeReadQuery(Query $query, array $options = []) return new ResultSet($this->resources, count($this->resources)); } - protected function _executeUpdateQuery(Query $query, array $options = []) + protected function _executeUpdateQuery(Query $query, array $options = []): int|bool|Resource { $this->resources[$this->conditionsToIndex($query->where())]->set($query->set()); @@ -99,7 +102,7 @@ protected function _executeUpdateQuery(Query $query, array $options = []) return 1; } - protected function _executeDeleteQuery(Query $query, array $options = []) + protected function _executeDeleteQuery(Query $query, array $options = []): int|bool { $conditions = $query->where(); @@ -131,4 +134,13 @@ public function conditionsToIndex(array $conditions) { return $conditions['id'] - 1; } + + public function extractConditions(array $options) + { + foreach($options as $option) { + if(isset($option['conditions'])) { + return $option['conditions']; + } + } + } } diff --git a/tests/test_app/src/Webservice/Logger.php b/tests/test_app/src/Webservice/Logger.php index 36e0c55..f323059 100644 --- a/tests/test_app/src/Webservice/Logger.php +++ b/tests/test_app/src/Webservice/Logger.php @@ -4,6 +4,7 @@ namespace TestApp\Webservice; use Psr\Log\LoggerInterface; +use Stringable; /** * @package MuffinWebservice @@ -15,11 +16,11 @@ class Logger implements LoggerInterface /** * System is unusable. * - * @param string $message + * @param \Stringable|string $message * @param array $context * @return void */ - public function emergency($message, array $context = []) + public function emergency(string|Stringable $message, array $context = []): void { } @@ -29,11 +30,11 @@ public function emergency($message, array $context = []) * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * - * @param string $message + * @param \Stringable|string $message * @param array $context * @return void */ - public function alert($message, array $context = []) + public function alert(string|Stringable $message, array $context = []): void { } @@ -42,11 +43,11 @@ public function alert($message, array $context = []) * * Example: Application component unavailable, unexpected exception. * - * @param string $message + * @param \Stringable|string $message * @param array $context * @return void */ - public function critical($message, array $context = []) + public function critical(string|Stringable $message, array $context = []): void { } @@ -54,11 +55,11 @@ public function critical($message, array $context = []) * Runtime errors that do not require immediate action but should typically * be logged and monitored. * - * @param string $message + * @param \Stringable|string $message * @param array $context * @return void */ - public function error($message, array $context = []) + public function error(string|Stringable $message, array $context = []): void { } @@ -68,22 +69,22 @@ public function error($message, array $context = []) * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ - public function warning($message, array $context = []) + public function warning(string|Stringable $message, array $context = []): void { } /** * Normal but significant events. * - * @param string $message + * @param \Stringable|string $message * @param array $context * @return void */ - public function notice($message, array $context = []) + public function notice(string|Stringable $message, array $context = []): void { } @@ -92,22 +93,22 @@ public function notice($message, array $context = []) * * Example: User logs in, SQL logs. * - * @param string $message + * @param \Stringable|string $message * @param array $context * @return void */ - public function info($message, array $context = []) + public function info(string|Stringable $message, array $context = []): void { } /** * Detailed debug information. * - * @param string $message + * @param \Stringable|string $message * @param array $context * @return void */ - public function debug($message, array $context = []) + public function debug(string|Stringable $message, array $context = []): void { } @@ -115,11 +116,11 @@ public function debug($message, array $context = []) * Logs with an arbitrary level. * * @param mixed $level - * @param string $message + * @param \Stringable|string $message * @param array $context * @return void */ - public function log($level, $message, array $context = []) + public function log($level, string|Stringable $message, array $context = []): void { } } diff --git a/tests/test_app/src/Webservice/StaticWebservice.php b/tests/test_app/src/Webservice/StaticWebservice.php index 2265fa8..95d9d1e 100644 --- a/tests/test_app/src/Webservice/StaticWebservice.php +++ b/tests/test_app/src/Webservice/StaticWebservice.php @@ -11,7 +11,7 @@ class StaticWebservice implements WebserviceInterface { - public function execute(Query $query, array $options = []) + public function execute(Query $query, array $options = []): ResultSet { return new ResultSet([ new Resource([ From defe09748543785145de44e69b0e54f2ef5640dd Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 6 Nov 2023 21:53:10 +0100 Subject: [PATCH 2/3] Done --- src/Model/Endpoint.php | 17 +++++++---------- tests/TestCase/ConnectionTest.php | 2 +- tests/TestCase/Model/EndpointTest.php | 11 ++++++++++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index f268af5..b790dae 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -666,21 +666,20 @@ public function findAll(Query $query, array $options): Query */ public function findList(Query $query, array $options): Query { - - debug($options); + if (isset($options[0])) { + $options = $options[0]; + } $options += [ 'keyField' => $this->getPrimaryKey(), 'valueField' => $this->getDisplayField(), 'groupField' => null, ]; - debug($options); $options = $this->_setFieldMatchers( $options, ['keyField', 'valueField', 'groupField'] ); - debug($options); return $query->formatResults(function (CollectionInterface $results) use ($options) { return $results->combine( $options['keyField'], @@ -706,7 +705,6 @@ public function findList(Query $query, array $options): Query */ protected function _setFieldMatchers(array $options, array $keys): array { - debug($options); foreach ($keys as $field) { if (!is_array($options[$field])) { continue; @@ -727,7 +725,6 @@ protected function _setFieldMatchers(array $options, array $keys): array return implode(';', $matches); }; } - debug($options); return $options; } @@ -783,10 +780,10 @@ public function get( } $conditions = array_combine($key, $primaryKey); - $cacheConfig = $options['cache'] ?? false; - $cacheKey = $options['key'] ?? false; - $finder = $options['finder'] ?? 'all'; - unset($options['key'], $options['cache'], $options['finder']); + $cacheConfig = $args['cache'] ?? false; + $cacheKey = $args['key'] ?? false; + $finder = $args['finder'] ?? 'all'; + unset($args['key'], $args['cache'], $args['finder']); $query = $this->find($finder, $args)->where($conditions); diff --git a/tests/TestCase/ConnectionTest.php b/tests/TestCase/ConnectionTest.php index 318286b..b7e4a1f 100644 --- a/tests/TestCase/ConnectionTest.php +++ b/tests/TestCase/ConnectionTest.php @@ -46,6 +46,6 @@ public function testConstructorNoDriver() public function testConfigName() { - debug($this->connection->configName()); + $this->assertEquals('test', $this->connection->configName()); } } diff --git a/tests/TestCase/Model/EndpointTest.php b/tests/TestCase/Model/EndpointTest.php index 4d11bce..f6b364a 100644 --- a/tests/TestCase/Model/EndpointTest.php +++ b/tests/TestCase/Model/EndpointTest.php @@ -113,7 +113,16 @@ public function testFindList() ], $this->endpoint->find('list', [ 'keyField' => 'title', 'valueField' => 'body', - ])->toArray()); + ])->toArray(), 'Find with options array'); + + $this->assertEquals([ + 'Hello World' => 'Some text', + 'New ORM' => 'Some more text', + 'Webservices' => 'Even more text', + ], $this->endpoint->find('list', + keyField: 'title', + valueField: 'body', + )->toArray(), 'Find with named parameters'); } public function testGet() From 3e45745be4753e00dacc5bfb76486b1216491ea7 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 7 Nov 2023 22:13:22 +0100 Subject: [PATCH 3/3] cs-fixed --- src/Datasource/Connection.php | 13 +++++++++++-- src/Datasource/Query.php | 5 +++++ src/Model/Endpoint.php | 3 --- .../Exception/UnexpectedDriverException.php | 3 ++- src/Webservice/Webservice.php | 2 +- tests/TestCase/Model/EndpointTest.php | 9 ++++++--- tests/TestCase/QueryTest.php | 1 - .../src/Webservice/EndpointTestWebservice.php | 5 ++--- 8 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index 4a47d34..410cc44 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -56,9 +56,18 @@ public function __construct(array $config) } } - public function setCacher(CacheInterface $cacher) { } + /** + * @param \Psr\SimpleCache\CacheInterface $cacher + * @return void + */ + public function setCacher(CacheInterface $cacher): void + { + } - public function getCacher(): CacheInterface { } + /** @return \Psr\SimpleCache\CacheInterface */ + public function getCacher(): CacheInterface + { + } /** * {@inheritDoc} diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index 7f371ce..1ec43c9 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -216,6 +216,11 @@ public function all(): ResultSetInterface return $this->_results; } + /** + * @param \Closure|array|string $fields + * @param bool $overwrite + * @return void + */ public function orderBy(Closure|array|string $fields, bool $overwrite = false): void { } diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index b790dae..ace0ba1 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -24,7 +24,6 @@ use Muffin\Webservice\Datasource\Connection; use Muffin\Webservice\Datasource\Marshaller; use Muffin\Webservice\Datasource\Query; -use Muffin\Webservice\Datasource\ResultSet; use Muffin\Webservice\Datasource\Schema; use Muffin\Webservice\Model\Exception\MissingResourceClassException; use Muffin\Webservice\Webservice\WebserviceInterface; @@ -687,7 +686,6 @@ public function findList(Query $query, array $options): Query $options['groupField'] ); }); - } /** @@ -761,7 +759,6 @@ public function get( mixed ...$args ): EntityInterface { $key = (array)$this->getPrimaryKey(); - $alias = $this->getAlias(); foreach ($key as $index => $keyname) { $key[$index] = $keyname; } diff --git a/src/Webservice/Exception/UnexpectedDriverException.php b/src/Webservice/Exception/UnexpectedDriverException.php index f45acb3..b4dc137 100644 --- a/src/Webservice/Exception/UnexpectedDriverException.php +++ b/src/Webservice/Exception/UnexpectedDriverException.php @@ -12,5 +12,6 @@ class UnexpectedDriverException extends CakeException * * @var string */ - protected string $_messageTemplate = 'Driver (`%s`) should extend `Muffin\Webservice\Webservice\Driver\AbstractDriver`'; + protected string $_messageTemplate + = 'Driver (`%s`) should extend `Muffin\Webservice\Webservice\Driver\AbstractDriver`'; } diff --git a/src/Webservice/Webservice.php b/src/Webservice/Webservice.php index cf8238b..eee8458 100644 --- a/src/Webservice/Webservice.php +++ b/src/Webservice/Webservice.php @@ -187,7 +187,7 @@ public function execute(Query $query, array $options = []): bool|int|Resource|Re public function describe(string $endpoint): Schema { $shortName = App::shortName(static::class, 'Webservice', 'Webservice'); - [$plugin, $name] = pluginSplit($shortName); + [$plugin] = pluginSplit($shortName); $endpoint = Inflector::classify(str_replace('-', '_', $endpoint)); $schemaShortName = implode('.', array_filter([$plugin, $endpoint])); diff --git a/tests/TestCase/Model/EndpointTest.php b/tests/TestCase/Model/EndpointTest.php index f6b364a..ffc4e56 100644 --- a/tests/TestCase/Model/EndpointTest.php +++ b/tests/TestCase/Model/EndpointTest.php @@ -98,11 +98,13 @@ public function testFindByTitle() public function testFindList() { - $this->assertEquals([ + $this->assertEquals( + [ 1 => 'Hello World', 2 => 'New ORM', 3 => 'Webservices', - ], $this->endpoint->find('list')->toArray(), + ], + $this->endpoint->find('list')->toArray(), 'Id => valueField' ); @@ -119,7 +121,8 @@ public function testFindList() 'Hello World' => 'Some text', 'New ORM' => 'Some more text', 'Webservices' => 'Even more text', - ], $this->endpoint->find('list', + ], $this->endpoint->find( + 'list', keyField: 'title', valueField: 'body', )->toArray(), 'Find with named parameters'); diff --git a/tests/TestCase/QueryTest.php b/tests/TestCase/QueryTest.php index ef14513..b07765d 100644 --- a/tests/TestCase/QueryTest.php +++ b/tests/TestCase/QueryTest.php @@ -4,7 +4,6 @@ namespace Muffin\Webservice\Test\TestCase; use Cake\Database\Expression\ComparisonExpression; -use Cake\Datasource\ResultSetInterface; use Cake\TestSuite\TestCase; use Muffin\Webservice\Datasource\Query; use Muffin\Webservice\Datasource\ResultSet; diff --git a/tests/test_app/src/Webservice/EndpointTestWebservice.php b/tests/test_app/src/Webservice/EndpointTestWebservice.php index 4b8e8f8..a652009 100644 --- a/tests/test_app/src/Webservice/EndpointTestWebservice.php +++ b/tests/test_app/src/Webservice/EndpointTestWebservice.php @@ -3,7 +3,6 @@ namespace TestApp\Webservice; -use Cake\Utility\Hash; use Muffin\Webservice\Datasource\Query; use Muffin\Webservice\Datasource\ResultSet; use Muffin\Webservice\Model\Resource; @@ -137,8 +136,8 @@ public function conditionsToIndex(array $conditions) public function extractConditions(array $options) { - foreach($options as $option) { - if(isset($option['conditions'])) { + foreach ($options as $option) { + if (isset($option['conditions'])) { return $option['conditions']; } }