From dd6dfb25c44f7b58a5bcbdace188664576deb35e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 5 Nov 2023 21:50:32 +0100 Subject: [PATCH 01/21] First try --- composer.json | 12 +- phpstan.neon | 3 +- phpunit.xml.dist | 25 +- psalm.xml | 18 +- src/Datasource/Connection.php | 57 ++- .../Exception/MissingConnectionException.php | 2 +- src/Datasource/Marshaller.php | 32 +- src/Datasource/Query.php | 465 +++++++++++++++--- src/Datasource/ResultSet.php | 26 +- src/Datasource/Schema.php | 24 +- src/Model/Endpoint.php | 159 +++--- 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, 743 insertions(+), 303 deletions(-) rename tests/test_app/src/Webservice/Driver/{Test.php => TestDriver.php} (93%) diff --git a/composer.json b/composer.json index 1bee8e4..d64f3bd 100644 --- a/composer.json +++ b/composer.json @@ -39,12 +39,16 @@ "irc": "irc://irc.freenode.org/muffin" }, "require": { - "cakephp/orm": "^4.5" + "cakephp/orm": "^5.0" }, "require-dev": { - "cakephp/cakephp": "^4.5", - "cakephp/cakephp-codesniffer": "^4.2", - "phpunit/phpunit": "^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": { 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 f92600e..36952e4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,23 +1,20 @@ - - + - + ./tests/ - - + - - - + + + + + + ./src/ - - + + diff --git a/psalm.xml b/psalm.xml index edd7691..6f36d46 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,9 +1,12 @@ @@ -13,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 071fe06..333ef8c 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 @@ -264,9 +260,9 @@ public function merge(EntityInterface $entity, array $data, array $options = []) * 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 0417431..e5210ad 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->setRepository($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($offset) * 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 5978fe7..a9ef4f4 100644 --- a/src/Datasource/ResultSet.php +++ b/src/Datasource/ResultSet.php @@ -5,7 +5,10 @@ use Cake\Collection\CollectionTrait; use Cake\Datasource\ResultSetInterface; +use IteratorIterator; +use Muffin\Webservice\Model\Resource; +/** @package Muffin\Webservice\Datasource */ /** * @template T of \Cake\Datasource\EntityInterface|array * @implements \Cake\Datasource\ResultSetInterface @@ -19,29 +22,28 @@ class ResultSet implements ResultSetInterface * * @var int */ - protected $_index = 0; + protected int $_index = 0; /** * Last record fetched from the statement * - * @var \Cake\Datasource\EntityInterface|array - * @psalm-var T + * @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 @@ -76,7 +78,7 @@ public function current() * * @return void */ - public function rewind() + public function rewind(): void { $this->_index = 0; } @@ -88,7 +90,7 @@ public function rewind() * * @return string Serialized object */ - public function serialize() + public function serialize(): string { while ($this->valid()) { $this->next(); @@ -104,7 +106,7 @@ public function serialize() * * @return bool */ - public function valid() + public function valid(): bool { if (!isset($this->_results[$this->key()])) { return false; @@ -122,7 +124,7 @@ public function valid() * * @return int */ - public function key() + public function key(): int { return $this->_index; } @@ -134,7 +136,7 @@ public function key() * * @return void */ - public function next() + public function next(): void { $this->_index++; } @@ -147,7 +149,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 310d1b3..74b9668 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 34c36f6..1b61ee7 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); @@ -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 a6bb912..b4be0f2 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 0d6e66f213f6b51c7ab1b1f4d26c730760f6f7a0 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 6 Nov 2023 21:53:10 +0100 Subject: [PATCH 02/21] 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 1b61ee7..23628b6 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 6228952de3873af4acd7249ec7a18b224436aa7d Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 7 Nov 2023 22:13:22 +0100 Subject: [PATCH 03/21] 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 e5210ad..7c220e8 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 23628b6..cdcefbe 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']; } } From 5a0ed24574fcb6497eb4cf5617e7a3eaaa7caf3e Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Mon, 25 Mar 2024 15:46:06 +0100 Subject: [PATCH 04/21] Fixed deprecation issues in php 8.2 and the current phpunit version --- .gitignore | 1 + src/Datasource/Query.php | 2 +- tests/TestCase/Model/Endpoint/Schema/SchemaTest.php | 2 +- tests/TestCase/Model/EndpointTest.php | 2 +- tests/TestCase/QueryTest.php | 4 ++-- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 654564b..a072370 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /plugins /vendor .phpunit.result.cache +/.idea diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index 7c220e8..c5f674c 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -696,7 +696,7 @@ public function execute(): bool|int|Resource|ResultSetInterface return $this->_execute(); } - return $this->_result = $this->_webservice->execute($this); + return $this->_webservice->execute($this); } /** diff --git a/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php b/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php index b040ce4..69063bd 100644 --- a/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php +++ b/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php @@ -20,7 +20,7 @@ public function setUp(): void public function testName() { - $this->assertEquals($this->schema->name(), 'test'); + $this->assertEquals('test', $this->schema->name()); } public function testAddColumn() diff --git a/tests/TestCase/Model/EndpointTest.php b/tests/TestCase/Model/EndpointTest.php index ffc4e56..ff4771d 100644 --- a/tests/TestCase/Model/EndpointTest.php +++ b/tests/TestCase/Model/EndpointTest.php @@ -190,7 +190,7 @@ public function testUpdatingSave() $this->assertFalse($savedResource->isNew()); $newResource = $this->endpoint->get(2); - $this->assertEquals($newResource->title, 'New ORM for webservices'); + $this->assertEquals('New ORM for webservices', $newResource->title); } public function testDelete() diff --git a/tests/TestCase/QueryTest.php b/tests/TestCase/QueryTest.php index b07765d..df2d807 100644 --- a/tests/TestCase/QueryTest.php +++ b/tests/TestCase/QueryTest.php @@ -174,7 +174,7 @@ public function testExecuteTwice() $mockWebservice->expects($this->once()) ->method('execute') - ->will($this->returnValue(new ResultSet([ + ->willReturn(new ResultSet([ new Resource([ 'id' => 1, 'title' => 'Hello World', @@ -187,7 +187,7 @@ public function testExecuteTwice() 'id' => 3, 'title' => 'Webservices', ]), - ], 3))); + ], 3)); $this->query ->setWebservice($mockWebservice) From 4d306134d141a74389ab73f1ec68977952de38f2 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Mon, 25 Mar 2024 17:02:49 +0100 Subject: [PATCH 05/21] Simple warnings and typing issues resolved for php 8.2 --- composer.json | 3 +- src/Datasource/Connection.php | 17 +++--- src/Datasource/Marshaller.php | 2 +- src/Datasource/Query.php | 61 +++++++++---------- src/Datasource/Schema.php | 8 +-- src/Model/Endpoint.php | 50 ++++++++------- src/Model/EndpointLocator.php | 8 +-- src/Webservice/Driver/AbstractDriver.php | 10 +-- src/Webservice/Webservice.php | 28 ++++----- tests/TestCase/ConnectionTest.php | 4 +- tests/TestCase/MarshallerTest.php | 4 +- .../Model/Endpoint/Schema/SchemaTest.php | 5 +- tests/TestCase/Model/EndpointLocatorTest.php | 4 +- tests/TestCase/Model/EndpointTest.php | 12 ++-- tests/TestCase/QueryTest.php | 4 +- tests/TestCase/ResultSetTest.php | 4 +- tests/TestCase/Webservice/WebserviceTest.php | 6 +- tests/bootstrap.php | 22 +++---- .../src/Webservice/SomePluginWebservice.php | 8 ++- .../src/Webservice/TestPluginWebservice.php | 8 ++- .../src/Model/Endpoint/TestEndpoint.php | 2 +- .../src/Webservice/EndpointTestWebservice.php | 9 ++- .../src/Webservice/TestWebservice.php | 8 ++- 23 files changed, 151 insertions(+), 136 deletions(-) diff --git a/composer.json b/composer.json index d64f3bd..b80a7a2 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "require-dev": { "cakephp/cakephp": "^5.0", "cakephp/cakephp-codesniffer": "^5.0", - "phpunit/phpunit": "^10.1" + "phpunit/phpunit": "^10.1", + "ext-mbstring": "*" }, "scripts": { "cs-check": "phpcs --colors --parallel=16 -p src/ tests/", diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index 410cc44..f501313 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -9,14 +9,14 @@ use Muffin\Webservice\Webservice\Driver\AbstractDriver; use Muffin\Webservice\Webservice\Exception\MissingDriverException; use Muffin\Webservice\Webservice\Exception\UnexpectedDriverException; +use Muffin\Webservice\Webservice\WebserviceInterface; use Psr\SimpleCache\CacheInterface; /** * Class Connection * - * @method \Muffin\Webservice\Webservice\Driver\AbstractDriver setWebservice(string $name, \Muffin\Webservice\Webservice\WebserviceInterface $webservice) Proxy method through to the Driver - * @method \Muffin\Webservice\Webservice\WebserviceInterface getWebservice(string $name) Proxy method through to the Driver - * @method string configName() Proxy method through to the Driver + * @method AbstractDriver setWebservice(string $name, WebserviceInterface $webservice) Proxy method through to the Driver + * @method WebserviceInterface getWebservice(string $name) Proxy method through to the Driver */ class Connection implements ConnectionInterface { @@ -25,7 +25,7 @@ class Connection implements ConnectionInterface * * @var \Muffin\Webservice\Webservice\Driver\AbstractDriver */ - protected ?AbstractDriver $_driver = null; + protected AbstractDriver $_driver; protected CacheInterface $cacher; @@ -49,11 +49,12 @@ public function __construct(array $config) $driver = $config['driver']; unset($config['driver'], $config['service']); - $this->_driver = new $driver($config); + $tempDriver = new $driver($config); - if (!($this->_driver instanceof AbstractDriver)) { + if (!($tempDriver instanceof AbstractDriver)) { throw new UnexpectedDriverException(['driver' => $driver]); } + $this->_driver = $tempDriver; } /** @@ -62,11 +63,13 @@ public function __construct(array $config) */ public function setCacher(CacheInterface $cacher): void { + $this->cacher = $cacher; } /** @return \Psr\SimpleCache\CacheInterface */ public function getCacher(): CacheInterface { + return $this->cacher; } /** @@ -75,7 +78,7 @@ public function getCacher(): CacheInterface * @see \Cake\Datasource\ConnectionInterface::getDriver() * @return \Muffin\Webservice\Webservice\Driver\AbstractDriver */ - public function getDriver(string $role = self::ROLE_WRITE): object + public function getDriver(string $role = self::ROLE_WRITE): AbstractDriver { return $this->_driver; } diff --git a/src/Datasource/Marshaller.php b/src/Datasource/Marshaller.php index 333ef8c..0c03cdb 100644 --- a/src/Datasource/Marshaller.php +++ b/src/Datasource/Marshaller.php @@ -262,7 +262,7 @@ public function merge(EntityInterface $entity, array $data, array $options = []) * @param array $options List of options. * @return array<\Cake\Datasource\EntityInterface> */ - public function mergeMany(array|Traversable $entities, array $data, array $options = []): array + public function mergeMany(iterable $entities, array $data, array $options = []): array { $primary = (array)$this->_endpoint->getPrimaryKey(); diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index c5f674c..a4e02c5 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -202,14 +202,10 @@ public function all(): ResultSetInterface } $results = null; - if ($this->_cache) { - $results = $this->_cache->fetch($this); - } + $results = $this->_cache?->fetch($this); if ($results === null) { $results = $this->decorateResults($this->_execute()); - if ($this->_cache) { - $this->_cache->store($this, $results); - } + $this->_cache?->store($this, $results); } $this->_results = $results; @@ -241,7 +237,7 @@ public function toArray(): array * @param \Cake\Datasource\RepositoryInterface $repository The default repository object to use. * @return $this */ - public function setRepository(RepositoryInterface $repository) + public function setRepository(RepositoryInterface $repository): Query { $this->_endpoint = $repository; @@ -252,9 +248,9 @@ public function setRepository(RepositoryInterface $repository) * 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 + * @return \Cake\Datasource\RepositoryInterface */ - public function getRepository(): ?RepositoryInterface + public function getRepository(): RepositoryInterface { return $this->_endpoint; } @@ -264,7 +260,7 @@ public function getRepository(): ?RepositoryInterface * * @return $this */ - public function create() + public function create(): Query { $this->action(self::ACTION_CREATE); @@ -276,7 +272,7 @@ public function create() * * @return $this */ - public function read() + public function read(): Query { $this->action(self::ACTION_READ); @@ -288,7 +284,7 @@ public function read() * * @return $this */ - public function update() + public function update(): Query { $this->action(self::ACTION_UPDATE); @@ -300,7 +296,7 @@ public function update() * * @return $this */ - public function delete() + public function delete(): Query { $this->action(self::ACTION_DELETE); @@ -334,7 +330,7 @@ public function clause(string $name): mixed * @return $this * @psalm-suppress LessSpecificReturnStatement */ - public function setEndpoint(Endpoint $endpoint) + public function setEndpoint(Endpoint $endpoint): Query { $this->_endpoint = $endpoint; @@ -359,7 +355,7 @@ public function getEndpoint(): Endpoint * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice The webservice to use * @return $this */ - public function setWebservice(WebserviceInterface $webservice) + public function setWebservice(WebserviceInterface $webservice): Query { $this->_webservice = $webservice; @@ -433,14 +429,15 @@ public function aliasField(string $field, ?string $alias = null): array * * @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 + * @param bool $overwrite Whether to replace previous queries. + * @return Query|null|array */ public function where( Closure|array|string|null $conditions = null, array $types = [], bool $overwrite = false - ) { + ): Query|null|array + { if ($conditions === null) { return $this->clause('where'); } @@ -460,7 +457,7 @@ public function where( * @see \Cake\Database\Type * @psalm-suppress PossiblyInvalidArgument */ - public function andWhere(string|array $conditions, array $types = []) + public function andWhere(string|array $conditions, array $types = []): Query { $this->where($conditions, $types); @@ -473,7 +470,7 @@ public function andWhere(string|array $conditions, array $types = []) * @param int $action Action to use * @return $this */ - public function action(int $action) + public function action(int $action): Query { $this->_parts['action'] = $action; @@ -490,11 +487,11 @@ public function action(int $action) * Pages should start at 1. * * @param int $num The page number you want. - * @param int $limit The number of rows you want in the page. If null + * @param int|null $limit The number of rows you want in the page. If null * the current limit clause will be used. * @return $this */ - public function page(int $num, ?int $limit = null) + public function page(int $num, ?int $limit = null): Query { if ($num < 1) { throw new InvalidArgumentException('Pages must start at 1.'); @@ -524,7 +521,7 @@ public function page(int $num, ?int $limit = null) * @param ?int $limit number of records to be returned * @return $this */ - public function limit(?int $limit) + public function limit(?int $limit): Query { $this->_parts['limit'] = $limit; @@ -535,9 +532,9 @@ public function limit(?int $limit) * Set fields to save in resources * * @param array|null $fields The field to set - * @return $this|array + * @return $this|null|array */ - public function set(?array $fields = null) + public function set(?array $fields = null): Query|null|array { if ($fields === null) { return $this->clause('set'); @@ -555,7 +552,7 @@ public function set(?array $fields = null) /** * @inheritDoc */ - public function offset($offset) + public function offset(?int $offset): Query|QueryInterface { $this->_parts['offset'] = $offset; @@ -579,7 +576,7 @@ public function offset($offset) * @param bool $overwrite whether to reset order with field list or not * @return $this */ - public function order(array|ExpressionInterface|Closure|string $fields, bool $overwrite = false) + public function order(array|ExpressionInterface|Closure|string $fields, bool $overwrite = false): Query { $this->_parts['order'] = !$overwrite ? Hash::merge($this->clause('order'), $fields) : $fields; @@ -593,7 +590,7 @@ public function order(array|ExpressionInterface|Closure|string $fields, bool $ov * @param array $options the options to be applied * @return $this This object */ - public function applyOptions(array $options) + public function applyOptions(array $options): Query { if (isset($options['page'])) { $this->page($options['page']); @@ -765,7 +762,7 @@ public function jsonSerialize(): ResultSetInterface * @param bool $overwrite Whether or not to replace previous selections. * @return $this */ - public function select(ExpressionInterface|Closure|array|string|int|float $fields, bool $overwrite = false) + public function select(ExpressionInterface|Closure|array|string|int|float $fields, bool $overwrite = false): Query { if (!is_string($fields) && is_callable($fields)) { $fields = $fields($this); @@ -843,7 +840,7 @@ protected function decorateResults(iterable $result): ResultSetInterface * @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) + public function mapReduce(?Closure $mapper = null, ?Closure $reducer = null, bool $overwrite = false): Query { if ($overwrite) { $this->_mapReduce = []; @@ -898,7 +895,7 @@ public function isEagerLoaded(): bool * @param bool $value Whether to eager load. * @return $this */ - public function eagerLoaded(bool $value) + public function eagerLoaded(bool $value): Query { $this->_eagerLoaded = $value; @@ -948,7 +945,7 @@ public function eagerLoaded(bool $value) * @return $this * @throws \InvalidArgumentException */ - public function formatResults(?Closure $formatter = null, int|bool $mode = self::APPEND) + public function formatResults(?Closure $formatter = null, int|bool $mode = self::APPEND): Query { if ($mode === self::OVERWRITE) { $this->_formatters = []; diff --git a/src/Datasource/Schema.php b/src/Datasource/Schema.php index 74b9668..54e116e 100644 --- a/src/Datasource/Schema.php +++ b/src/Datasource/Schema.php @@ -159,7 +159,7 @@ public function name(): string * @param array|string $attrs The attributes for the column. * @return $this */ - public function addColumn(string $name, array|string $attrs) + public function addColumn(string $name, array|string $attrs): Schema { if (is_string($attrs)) { $attrs = ['type' => $attrs]; @@ -221,7 +221,7 @@ public function hasColumn(string $name): bool * @param string $name The name of the column * @return $this */ - public function removeColumn(string $name) + public function removeColumn(string $name): Schema { unset($this->_columns[$name], $this->_typeMap[$name]); @@ -235,7 +235,7 @@ public function removeColumn(string $name) * @param string $type Type to set for the column * @return $this */ - public function setColumnType(string $name, string $type) + public function setColumnType(string $name, string $type): Schema { $this->_columns[$name]['type'] = $type; $this->_typeMap[$name] = $type; @@ -360,7 +360,7 @@ public function getPrimaryKey(): array * @param array $options Array of options to set * @return $this */ - public function setOptions(array $options) + public function setOptions(array $options): Schema { $this->_options = array_merge($this->_options, $options); diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index cdcefbe..91972d9 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -58,7 +58,7 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp /** * The webservice instance to call * - * @var \Muffin\Webservice\Webservice\WebserviceInterface + * @var \Muffin\Webservice\Webservice\WebserviceInterface|null */ protected ?WebserviceInterface $_webservice = null; @@ -79,7 +79,7 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp /** * The schema object containing a description of this endpoint fields * - * @var \Muffin\Webservice\Datasource\Schema + * @var \Muffin\Webservice\Datasource\Schema|null */ protected ?Schema $_schema = null; @@ -100,14 +100,14 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp /** * The name of the endpoint to contact * - * @var string + * @var string|null */ protected ?string $_name = null; /** * The name of the class that represent a single resource for this endpoint * - * @var string + * @var string|null * @psalm-var class-string<\Muffin\Webservice\Model\Resource> */ protected ?string $_resourceClass = null; @@ -115,7 +115,7 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp /** * Registry key used to create this endpoint object * - * @var string + * @var string|null */ protected ?string $_registryAlias = null; @@ -230,7 +230,7 @@ public function initialize(array $config): void * @param string $name The name for this endpoint instance * @return $this */ - public function setName(string $name) + public function setName(string $name) : Endpoint { $inflectMethod = $this->getInflectionMethod(); $this->_name = Inflector::{$inflectMethod}($name); @@ -273,7 +273,7 @@ public function aliasField(string $field): string * @param string $registryAlias The key used to access this object. * @return $this */ - public function setRegistryAlias(string $registryAlias) + public function setRegistryAlias(string $registryAlias) : Endpoint { $this->_registryAlias = $registryAlias; @@ -300,7 +300,7 @@ public function getRegistryAlias(): string * @param \Muffin\Webservice\Datasource\Connection $connection Connection instance * @return $this */ - public function setConnection(Connection $connection) + public function setConnection(Connection $connection) : Endpoint { $this->_connection = $connection; @@ -329,7 +329,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|array $schema) + public function setSchema(Schema|array $schema) : Endpoint { if (is_array($schema)) { $schema = new Schema($this->getName(), $schema); @@ -403,7 +403,7 @@ public function hasField(string $field): bool * @param array|string|null $key sets a new name to be used as primary key * @return $this */ - public function setPrimaryKey(string|array|null $key) + public function setPrimaryKey(string|array|null $key): Endpoint { $this->_primaryKey = $key; @@ -436,7 +436,7 @@ public function getPrimaryKey(): array|string * @param array|string $field The new field to use as the display field * @return $this */ - public function setDisplayField(string|array $field) + public function setDisplayField(string|array $field): Endpoint { $this->_displayField = $field; @@ -474,9 +474,9 @@ public function getDisplayField(): string|array * @return $this * @throws \Muffin\Webservice\Model\Exception\MissingResourceClassException If the resource class specified does not exist */ - public function setResourceClass(string $name) + public function setResourceClass(string $name): Endpoint { - /** @psalm-var class-string<\Muffin\Webservice\Model\Resource>|null */ + /** @psalm-var class-string<\Muffin\Webservice\Model\Resource>|null $className */ $className = App::className($name, 'Model/Resource'); if (!$className) { throw new MissingResourceClassException([$name]); @@ -505,7 +505,7 @@ public function getResourceClass(): string } $alias = Inflector::singularize(substr(array_pop($parts), 0, -8)); - /** @psalm-var class-string<\Muffin\Webservice\Model\Resource> */ + /** @psalm-var class-string<\Muffin\Webservice\Model\Resource> $alias */ $name = implode('\\', array_slice($parts, 0, -1)) . '\Resource\\' . $alias; if (!class_exists($name)) { return $this->_resourceClass = $default; @@ -523,7 +523,7 @@ public function getResourceClass(): string * @param string $method The name of the inflection method * @return $this */ - public function setInflectionMethod(string $method) + public function setInflectionMethod(string $method): Endpoint { $this->_inflectionMethod = $method; @@ -548,7 +548,7 @@ public function getInflectionMethod(): string * @return $this * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no driver exists for the endpoint */ - public function setWebservice(string $alias, WebserviceInterface $webservice) + public function setWebservice(string $alias, WebserviceInterface $webservice): Endpoint { $connection = $this->getConnection(); $connection->setWebservice($alias, $webservice); @@ -746,8 +746,7 @@ protected function _setFieldMatchers(array $options, array $keys): array * 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 + * @param mixed ...$args * @return \Cake\Datasource\EntityInterface * @see \Cake\Datasource\RepositoryInterface::find() */ @@ -857,7 +856,7 @@ 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 Closure|array|string = array(); $fields A hash of field => new value. + * @param Closure|array|string $fields = 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 @@ -1029,10 +1028,10 @@ public function callFinder(string $type, Query $query, array $options = []): Que * * @param string $method The method name that was fired. * @param array $args List of arguments passed to the function. - * @return mixed + * @return \Cake\Datasource\QueryInterface * @throws \BadMethodCallException when there are missing arguments, or when and & or are combined. */ - protected function _dynamicFinder(string $method, array $args): mixed + protected function _dynamicFinder(string $method, array $args): QueryInterface { $method = Inflector::underscore($method); preg_match('/^find_([\w]+)_by_/', $method, $matches); @@ -1092,10 +1091,10 @@ protected function _dynamicFinder(string $method, array $args): mixed * * @param string $method name of the method to be invoked * @param array $args List of arguments passed to the function - * @return mixed + * @return QueryInterface * @throws \BadMethodCallException If the request dynamic finder cannot be found */ - public function __call(string $method, array $args): mixed + public function __call(string $method, array $args): QueryInterface { if (preg_match('/^find(?:\w+)?By/', $method) > 0) { return $this->_dynamicFinder($method, $args); @@ -1149,9 +1148,8 @@ public function newEntity(array $data = [], array $options = []): EntityInterfac public function newEmptyEntity(): EntityInterface { $class = $this->getResourceClass(); - $entity = new $class([], ['source' => $this->getRegistryAlias()]); - return $entity; + return new $class([], ['source' => $this->getRegistryAlias()]); } /** @@ -1307,7 +1305,7 @@ public function __debugInfo(): array * @param string $alias Alias for this endpoint * @return $this */ - public function setAlias(string $alias) + public function setAlias(string $alias): Endpoint { $this->_alias = $alias; diff --git a/src/Model/EndpointLocator.php b/src/Model/EndpointLocator.php index 7310549..803c2af 100644 --- a/src/Model/EndpointLocator.php +++ b/src/Model/EndpointLocator.php @@ -36,10 +36,10 @@ public function set(string $alias, RepositoryInterface $repository): Endpoint * * @param string $alias The alias name you want to get. * @param array $options The options you want to build the endpoint with. - * @return \Muffin\Webservice\Model\Endpoint + * @return \Cake\Datasource\RepositoryInterface * @throws \RuntimeException If the registry alias is already in use. */ - public function get(string $alias, array $options = []): Endpoint + public function get(string $alias, array $options = []): RepositoryInterface { return parent::get($alias, $options); } @@ -63,7 +63,7 @@ protected function createInstance(string $alias, array $options): RepositoryInte if ($className) { $options['className'] = $className; } else { - if (!isset($options['endpoint']) && strpos($options['className'], '\\') === false) { + if (!isset($options['endpoint']) && !str_contains($options['className'], '\\')) { [, $endpoint] = pluginSplit($options['className']); $options['endpoint'] = Inflector::underscore($endpoint); } @@ -74,7 +74,7 @@ protected function createInstance(string $alias, array $options): RepositoryInte if ($options['className'] !== Endpoint::class) { $connectionName = $options['className']::defaultConnectionName(); } else { - if (strpos($alias, '.') === false) { + if (!str_contains($alias, '.')) { $connectionName = 'webservice'; } else { $pluginParts = explode('/', pluginSplit($alias)[0]); diff --git a/src/Webservice/Driver/AbstractDriver.php b/src/Webservice/Driver/AbstractDriver.php index 61d323b..7a15325 100644 --- a/src/Webservice/Driver/AbstractDriver.php +++ b/src/Webservice/Driver/AbstractDriver.php @@ -24,7 +24,7 @@ abstract class AbstractDriver implements LoggerAwareInterface * * @var object */ - protected ?object $_client = null; + protected object $_client; /** * Default config @@ -74,7 +74,7 @@ abstract public function initialize(): void; * @param object $client Client instance * @return $this */ - public function setClient(object $client) + public function setClient(object $client): AbstractDriver { $this->_client = $client; @@ -98,7 +98,7 @@ public function getClient(): ?object * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice Instance of the webservice * @return $this */ - public function setWebservice(string $name, WebserviceInterface $webservice) + public function setWebservice(string $name, WebserviceInterface $webservice) : AbstractDriver { $this->_webservices[$name] = $webservice; @@ -166,7 +166,7 @@ public function configName(): string * * @return $this */ - public function enableQueryLogging() + public function enableQueryLogging() : AbstractDriver { $this->_logQueries = true; @@ -178,7 +178,7 @@ public function enableQueryLogging() * * @return $this */ - public function disableQueryLogging() + public function disableQueryLogging() : AbstractDriver { $this->_logQueries = false; diff --git a/src/Webservice/Webservice.php b/src/Webservice/Webservice.php index eee8458..2e636c8 100644 --- a/src/Webservice/Webservice.php +++ b/src/Webservice/Webservice.php @@ -28,14 +28,14 @@ abstract class Webservice implements WebserviceInterface /** * The driver to use to communicate with the webservice * - * @var \Muffin\Webservice\Webservice\Driver\AbstractDriver + * @var \Muffin\Webservice\Webservice\Driver\AbstractDriver|null */ - protected AbstractDriver $_driver; + protected ?AbstractDriver $_driver = null; /** * The webservice to call * - * @var string + * @var string|null */ protected ?string $_endpoint = null; @@ -78,7 +78,7 @@ public function initialize(): void * @param \Muffin\Webservice\Webservice\Driver\AbstractDriver $driver Instance of the driver * @return $this */ - public function setDriver(AbstractDriver $driver) + public function setDriver(AbstractDriver $driver): Webservice { $this->_driver = $driver; @@ -105,7 +105,7 @@ public function getDriver(): AbstractDriver * @param string $endpoint Endpoint path * @return $this */ - public function setEndpoint(string $endpoint) + public function setEndpoint(string $endpoint): Webservice { $this->_endpoint = $endpoint; @@ -214,18 +214,14 @@ public function describe(string $endpoint): Schema */ protected function _executeQuery(Query $query, array $options = []): bool|int|Resource|ResultSet { - switch ($query->clause('action')) { - case Query::ACTION_CREATE: - return $this->_executeCreateQuery($query, $options); - case Query::ACTION_READ: - return $this->_executeReadQuery($query, $options); - case Query::ACTION_UPDATE: - return $this->_executeUpdateQuery($query, $options); - case Query::ACTION_DELETE: - return $this->_executeDeleteQuery($query, $options); - } + return match ($query->clause('action')) { + Query::ACTION_CREATE => $this->_executeCreateQuery($query, $options), + Query::ACTION_READ => $this->_executeReadQuery($query, $options), + Query::ACTION_UPDATE => $this->_executeUpdateQuery($query, $options), + Query::ACTION_DELETE => $this->_executeDeleteQuery($query, $options), + default => false, + }; - return false; } /** diff --git a/tests/TestCase/ConnectionTest.php b/tests/TestCase/ConnectionTest.php index b7e4a1f..65c49ef 100644 --- a/tests/TestCase/ConnectionTest.php +++ b/tests/TestCase/ConnectionTest.php @@ -11,9 +11,9 @@ class ConnectionTest extends TestCase { /** - * @var Connection + * @var Connection|null */ - public $connection; + public ?Connection $connection; public function setUp(): void { diff --git a/tests/TestCase/MarshallerTest.php b/tests/TestCase/MarshallerTest.php index e8124e9..9de7bfe 100644 --- a/tests/TestCase/MarshallerTest.php +++ b/tests/TestCase/MarshallerTest.php @@ -14,9 +14,9 @@ class MarshallerTest extends TestCase { /** - * @var Marshaller + * @var Marshaller|null */ - private $marshaller; + private ?Marshaller $marshaller; /** * Create a marshaller instance for testing diff --git a/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php b/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php index 69063bd..48a55e2 100644 --- a/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php +++ b/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php @@ -4,14 +4,15 @@ namespace Muffin\Webservice\Test\TestCase\Model\Endpoint\Schema; use Cake\TestSuite\TestCase; +use Muffin\Webservice\Model\Schema; use TestApp\Model\Endpoint\Schema\TestSchema; class SchemaTest extends TestCase { /** - * @var \Muffin\Webservice\Schema + * @var Schema|null */ - private $schema; + private ?Schema $schema; public function setUp(): void { diff --git a/tests/TestCase/Model/EndpointLocatorTest.php b/tests/TestCase/Model/EndpointLocatorTest.php index 1252854..fa4c796 100644 --- a/tests/TestCase/Model/EndpointLocatorTest.php +++ b/tests/TestCase/Model/EndpointLocatorTest.php @@ -11,9 +11,9 @@ class EndpointLocatorTest extends TestCase { /** - * @var \Muffin\Webservice\Model\EndpointLocator + * @var \Muffin\Webservice\Model\EndpointLocator|null */ - private $Locator; + private ?EndpointLocator $Locator; public function setUp(): void { diff --git a/tests/TestCase/Model/EndpointTest.php b/tests/TestCase/Model/EndpointTest.php index ff4771d..ab13cd7 100644 --- a/tests/TestCase/Model/EndpointTest.php +++ b/tests/TestCase/Model/EndpointTest.php @@ -3,6 +3,7 @@ namespace Muffin\Webservice\Test\TestCase\Model; +use AllowDynamicProperties; use BadMethodCallException; use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Event\EventManager; @@ -20,17 +21,18 @@ use TestApp\Model\Endpoint\TestEndpoint; use TestApp\Webservice\TestWebservice; +#[AllowDynamicProperties] class EndpointTest extends TestCase { /** - * @var \Muffin\Webservice\Connection + * @var Connection|null */ - protected $connection; + protected ?Connection $connection; /** - * @var Endpoint + * @var Endpoint|null */ - protected $endpoint; + protected ?Endpoint $endpoint; /** * @inheritDoc @@ -65,7 +67,7 @@ public static function providerEndpointNames(): array * @param string|null $inflector * @param string $expected */ - public function testEndpointName($name, $inflector, $expected) + public function testEndpointName(string $name, ?string $inflector, string $expected) { $endpoint = new Endpoint(['name' => $name, 'inflect' => $inflector]); $this->assertSame($expected, $endpoint->getName()); diff --git a/tests/TestCase/QueryTest.php b/tests/TestCase/QueryTest.php index df2d807..8f5e352 100644 --- a/tests/TestCase/QueryTest.php +++ b/tests/TestCase/QueryTest.php @@ -15,9 +15,9 @@ class QueryTest extends TestCase { /** - * @var Query + * @var Query|null */ - public $query; + public ?Query $query; /** * @inheritDoc diff --git a/tests/TestCase/ResultSetTest.php b/tests/TestCase/ResultSetTest.php index eb72e53..727f506 100644 --- a/tests/TestCase/ResultSetTest.php +++ b/tests/TestCase/ResultSetTest.php @@ -10,9 +10,9 @@ class ResultSetTest extends TestCase { /** - * @var ResultSet + * @var ResultSet|null */ - public $resultSet; + public ?ResultSet $resultSet; /** * @inheritDoc diff --git a/tests/TestCase/Webservice/WebserviceTest.php b/tests/TestCase/Webservice/WebserviceTest.php index fea92e1..8204580 100644 --- a/tests/TestCase/Webservice/WebserviceTest.php +++ b/tests/TestCase/Webservice/WebserviceTest.php @@ -8,15 +8,16 @@ use Muffin\Webservice\Model\Endpoint; use Muffin\Webservice\Model\Exception\MissingEndpointSchemaException; use Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException; +use Muffin\Webservice\Webservice\Webservice; use TestApp\Webservice\Driver\TestDriver; use TestApp\Webservice\TestWebservice; class WebserviceTest extends TestCase { /** - * @var \Muffin\Webservice\Webservice\Webservice + * @var \Muffin\Webservice\Webservice\Webservice|null */ - public $webservice; + public ?Webservice $webservice; /** * @inheritDoc @@ -159,7 +160,6 @@ public function testCreateResource() /** @var \Muffin\Webservice\Model\Resource $resource */ $resource = $this->webservice->createResource('\Muffin\Webservice\Model\Resource', []); - $this->assertInstanceOf('\Muffin\Webservice\Model\Resource', $resource); $this->assertFalse($resource->isNew()); $this->assertFalse($resource->isDirty()); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b4be0f2..430d3d3 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -24,18 +24,18 @@ // Path constants to a few helpful things. define('ROOT', dirname(__DIR__) . DS); -define('CAKE_CORE_INCLUDE_PATH', ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp'); -define('CORE_PATH', ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp' . DS); -define('CAKE', CORE_PATH . 'src' . DS); -define('TESTS', ROOT . 'tests'); -define('APP', ROOT . 'tests' . DS . 'test_app' . DS); -define('APP_DIR', 'test_app'); -define('WEBROOT_DIR', 'webroot'); -define('WWW_ROOT', APP . 'webroot' . DS); +const CAKE_CORE_INCLUDE_PATH = ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp'; +const CORE_PATH = ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp' . DS; +const CAKE = CORE_PATH . 'src' . DS; +const TESTS = ROOT . 'tests'; +const APP = ROOT . 'tests' . DS . 'test_app' . DS; +const APP_DIR = 'test_app'; +const WEBROOT_DIR = 'webroot'; +const WWW_ROOT = APP . 'webroot' . DS; define('TMP', sys_get_temp_dir() . DS); -define('CONFIG', APP . 'config' . DS); -define('CACHE', TMP); -define('LOGS', TMP); +const CONFIG = APP . 'config' . DS; +const CACHE = TMP; +const LOGS = TMP; require_once CORE_PATH . 'config/bootstrap.php'; diff --git a/tests/test_app/plugins/SomeVendor/SomePlugin/src/Webservice/SomePluginWebservice.php b/tests/test_app/plugins/SomeVendor/SomePlugin/src/Webservice/SomePluginWebservice.php index 41d20e3..73b8553 100644 --- a/tests/test_app/plugins/SomeVendor/SomePlugin/src/Webservice/SomePluginWebservice.php +++ b/tests/test_app/plugins/SomeVendor/SomePlugin/src/Webservice/SomePluginWebservice.php @@ -4,16 +4,20 @@ namespace SomeVendor\SomePlugin\Webservice; use Muffin\Webservice\Model\Endpoint; +use Muffin\Webservice\Model\Resource; use Muffin\Webservice\Webservice\Webservice; class SomePluginWebservice extends Webservice { - public function createResource($resourceClass, array $properties = []) + public function createResource($resourceClass, array $properties = []): Resource { return $this->_createResource($resourceClass, $properties); } - public function transformResults(Endpoint $endpoint, array $results) + /** + * @return Resource[] + */ + public function transformResults(Endpoint $endpoint, array $results): array { return $this->_transformResults($endpoint, $results); } diff --git a/tests/test_app/plugins/TestPlugin/src/Webservice/TestPluginWebservice.php b/tests/test_app/plugins/TestPlugin/src/Webservice/TestPluginWebservice.php index df2a152..ef5c5b8 100644 --- a/tests/test_app/plugins/TestPlugin/src/Webservice/TestPluginWebservice.php +++ b/tests/test_app/plugins/TestPlugin/src/Webservice/TestPluginWebservice.php @@ -4,16 +4,20 @@ namespace TestPlugin\Webservice; use Muffin\Webservice\Model\Endpoint; +use Muffin\Webservice\Model\Resource; use Muffin\Webservice\Webservice\Webservice; class TestPluginWebservice extends Webservice { - public function createResource($resourceClass, array $properties = []) + public function createResource($resourceClass, array $properties = []): Resource { return $this->_createResource($resourceClass, $properties); } - public function transformResults(Endpoint $endpoint, array $results) + /** + * @return Resource[] + */ + public function transformResults(Endpoint $endpoint, array $results): array { return $this->_transformResults($endpoint, $results); } diff --git a/tests/test_app/src/Model/Endpoint/TestEndpoint.php b/tests/test_app/src/Model/Endpoint/TestEndpoint.php index f48fbcd..eb800f2 100644 --- a/tests/test_app/src/Model/Endpoint/TestEndpoint.php +++ b/tests/test_app/src/Model/Endpoint/TestEndpoint.php @@ -32,7 +32,7 @@ public function validationDefault(Validator $validator): Validator * * @return true */ - public function findExamples() + public function findExamples(): true { return true; } diff --git a/tests/test_app/src/Webservice/EndpointTestWebservice.php b/tests/test_app/src/Webservice/EndpointTestWebservice.php index a652009..e2cd6a2 100644 --- a/tests/test_app/src/Webservice/EndpointTestWebservice.php +++ b/tests/test_app/src/Webservice/EndpointTestWebservice.php @@ -10,7 +10,10 @@ class EndpointTestWebservice extends Webservice { - protected $resources; + /** + * @var Resource[] + */ + protected array $resources; public function initialize(): void { @@ -129,7 +132,7 @@ protected function _executeDeleteQuery(Query $query, array $options = []): int|b return 0; } - public function conditionsToIndex(array $conditions) + public function conditionsToIndex(array $conditions): int { return $conditions['id'] - 1; } @@ -141,5 +144,7 @@ public function extractConditions(array $options) return $option['conditions']; } } + + return null; } } diff --git a/tests/test_app/src/Webservice/TestWebservice.php b/tests/test_app/src/Webservice/TestWebservice.php index c4e8e59..b27a616 100644 --- a/tests/test_app/src/Webservice/TestWebservice.php +++ b/tests/test_app/src/Webservice/TestWebservice.php @@ -4,16 +4,20 @@ namespace TestApp\Webservice; use Muffin\Webservice\Model\Endpoint; +use Muffin\Webservice\Model\Resource; use Muffin\Webservice\Webservice\Webservice; class TestWebservice extends Webservice { - public function createResource($resourceClass, array $properties = []) + public function createResource($resourceClass, array $properties = []): Resource { return $this->_createResource($resourceClass, $properties); } - public function transformResults(Endpoint $endpoint, array $results) + /** + * @return Resource[] + */ + public function transformResults(Endpoint $endpoint, array $results): array { return $this->_transformResults($endpoint, $results); } From d38121fe64aa3d7e94291541867cebf9475d0773 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Mon, 25 Mar 2024 17:09:53 +0100 Subject: [PATCH 06/21] PSR project structure/namespace issues fixed --- tests/TestCase/AbstractDriverTest.php | 2 +- tests/TestCase/Model/EndpointLocatorTest.php | 4 +++- tests/TestCase/Model/ResourceTest.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/TestCase/AbstractDriverTest.php b/tests/TestCase/AbstractDriverTest.php index df9f354..a0d7594 100644 --- a/tests/TestCase/AbstractDriverTest.php +++ b/tests/TestCase/AbstractDriverTest.php @@ -1,7 +1,7 @@ Date: Mon, 25 Mar 2024 17:18:21 +0100 Subject: [PATCH 07/21] Github pipeline updated to only support 8.1, 8.2 and 8.3 (in line with cakephp 5) --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26d00f0..1e4d381 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,11 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['7.4', '8.0', '8.3'] + php-version: ['8.1', '8.2', '8.3'] db-type: ['mysql', 'pgsql'] prefer-lowest: [''] include: - - php-version: '7.4' + - php-version: '8.1' db-type: 'sqlite' prefer-lowest: 'prefer-lowest' @@ -61,14 +61,14 @@ jobs: if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi - if [[ ${{ matrix.php-version }} == '7.4' && ${{ matrix.db-type }} == 'mysql' ]]; then + if [[ ${{ matrix.php-version }} == '8.1' && ${{ matrix.db-type }} == 'mysql' ]]; then vendor/bin/phpunit --coverage-clover=coverage.xml else vendor/bin/phpunit fi - name: Code Coverage Report - if: success() && matrix.php-version == '7.4' && matrix.db-type == 'mysql' + if: success() && matrix.php-version == '8.1' && matrix.db-type == 'mysql' uses: codecov/codecov-action@v1 cs-stan: @@ -81,10 +81,10 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.2' extensions: mbstring, intl coverage: none - tools: psalm:4, phpstan:1 + tools: psalm:5.23.1, phpstan:1.0.65 - name: Composer Install run: composer require cakephp/cakephp-codesniffer:^4.2 From afc4ed81cb4612fd86506dc3deaa67a1b80d89c1 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Mon, 25 Mar 2024 18:01:57 +0100 Subject: [PATCH 08/21] Added composer dev requirements for easier debugging, linting fixed, part of the psalm errors fixed --- .github/workflows/ci.yml | 2 +- composer.json | 4 +++- src/Datasource/Connection.php | 13 +++++++------ src/Datasource/Marshaller.php | 2 -- src/Datasource/Query.php | 16 ++++++++++------ src/Model/Endpoint.php | 16 ++++++++-------- src/Webservice/Driver/AbstractDriver.php | 6 +++--- src/Webservice/Webservice.php | 1 - 8 files changed, 32 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e4d381..f7de380 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: php-version: '8.2' extensions: mbstring, intl coverage: none - tools: psalm:5.23.1, phpstan:1.0.65 + tools: psalm:5.23.1, phpstan:1.10.65 - name: Composer Install run: composer require cakephp/cakephp-codesniffer:^4.2 diff --git a/composer.json b/composer.json index b80a7a2..5937c3e 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,9 @@ "cakephp/cakephp": "^5.0", "cakephp/cakephp-codesniffer": "^5.0", "phpunit/phpunit": "^10.1", - "ext-mbstring": "*" + "ext-mbstring": "*", + "vimeo/psalm": "5.23.1", + "phpstan/phpstan": "1.10.65" }, "scripts": { "cs-check": "phpcs --colors --parallel=16 -p src/ tests/", diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index f501313..3f8a48b 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -9,14 +9,13 @@ use Muffin\Webservice\Webservice\Driver\AbstractDriver; use Muffin\Webservice\Webservice\Exception\MissingDriverException; use Muffin\Webservice\Webservice\Exception\UnexpectedDriverException; -use Muffin\Webservice\Webservice\WebserviceInterface; use Psr\SimpleCache\CacheInterface; /** * Class Connection * - * @method AbstractDriver setWebservice(string $name, WebserviceInterface $webservice) Proxy method through to the Driver - * @method WebserviceInterface getWebservice(string $name) Proxy method through to the Driver + * @method \Muffin\Webservice\Webservice\Driver\AbstractDriver setWebservice(string $name, \Muffin\Webservice\Datasource\WebserviceInterface $webservice) Proxy method through to the Driver + * @method \Muffin\Webservice\Datasource\WebserviceInterface getWebservice(string $name) Proxy method through to the Driver */ class Connection implements ConnectionInterface { @@ -59,11 +58,13 @@ public function __construct(array $config) /** * @param \Psr\SimpleCache\CacheInterface $cacher - * @return void + * @return $this */ - public function setCacher(CacheInterface $cacher): void + public function setCacher(CacheInterface $cacher): ConnectionInterface { $this->cacher = $cacher; + + return $this; } /** @return \Psr\SimpleCache\CacheInterface */ @@ -96,7 +97,7 @@ public function configName(): string /** * Get the config data for this connection. * - * @return array + * @return array */ public function config(): array { diff --git a/src/Datasource/Marshaller.php b/src/Datasource/Marshaller.php index 0c03cdb..abc3d36 100644 --- a/src/Datasource/Marshaller.php +++ b/src/Datasource/Marshaller.php @@ -9,7 +9,6 @@ use Cake\Datasource\InvalidPropertyInterface; use Muffin\Webservice\Model\Endpoint; use RuntimeException; -use Traversable; /** * Contains logic to convert array data into resources. @@ -54,7 +53,6 @@ public function one(array $data, array $options = []): EntityInterface [$data, $options] = $this->_prepareDataAndOptions($data, $options); $primaryKey = (array)$this->_endpoint->getPrimaryKey(); - /** @psalm-var class-string<\Muffin\Webservice\Model\Resource> */ $resourceClass = $this->_endpoint->getResourceClass(); $entity = new $resourceClass(); $entity->setSource($this->_endpoint->getRegistryAlias()); diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index a4e02c5..332689c 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -23,6 +23,11 @@ use Traversable; use UnexpectedValueException; +/** + * @template TKey + * @template-covariant TValue + * @template-implements IteratorAggregate + */ class Query implements IteratorAggregate, JsonSerializable, QueryInterface { public const ACTION_CREATE = 1; @@ -104,7 +109,7 @@ class Query implements IteratorAggregate, JsonSerializable, QueryInterface /** * The result from the webservice * - * @var \Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet|int|bool + * @var ResultSetInterface|null */ protected mixed $_results = null; @@ -430,14 +435,13 @@ public function aliasField(string $field, ?string $alias = null): array * @param \Closure|array|string|null $conditions The list of conditions. * @param array $types Not used, required to comply with QueryInterface. * @param bool $overwrite Whether to replace previous queries. - * @return Query|null|array + * @return \Muffin\Webservice\Datasource\Query|array|null */ public function where( Closure|array|string|null $conditions = null, array $types = [], bool $overwrite = false - ): Query|null|array - { + ): Query|array|null { if ($conditions === null) { return $this->clause('where'); } @@ -532,9 +536,9 @@ public function limit(?int $limit): Query * Set fields to save in resources * * @param array|null $fields The field to set - * @return $this|null|array + * @return $this|array|null */ - public function set(?array $fields = null): Query|null|array + public function set(?array $fields = null): Query|array|null { if ($fields === null) { return $this->clause('set'); diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index 91972d9..f3dc6e3 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -230,7 +230,7 @@ public function initialize(array $config): void * @param string $name The name for this endpoint instance * @return $this */ - public function setName(string $name) : Endpoint + public function setName(string $name): Endpoint { $inflectMethod = $this->getInflectionMethod(); $this->_name = Inflector::{$inflectMethod}($name); @@ -273,7 +273,7 @@ public function aliasField(string $field): string * @param string $registryAlias The key used to access this object. * @return $this */ - public function setRegistryAlias(string $registryAlias) : Endpoint + public function setRegistryAlias(string $registryAlias): Endpoint { $this->_registryAlias = $registryAlias; @@ -300,7 +300,7 @@ public function getRegistryAlias(): string * @param \Muffin\Webservice\Datasource\Connection $connection Connection instance * @return $this */ - public function setConnection(Connection $connection) : Endpoint + public function setConnection(Connection $connection): Endpoint { $this->_connection = $connection; @@ -329,7 +329,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|array $schema) : Endpoint + public function setSchema(Schema|array $schema): Endpoint { if (is_array($schema)) { $schema = new Schema($this->getName(), $schema); @@ -413,10 +413,10 @@ public function setPrimaryKey(string|array|null $key): Endpoint /** * Get the endpoints primary key, if one is not set, fetch it from the schema * - * @return array|string + * @return list|string|null * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no schema exists to fetch the key from */ - public function getPrimaryKey(): array|string + public function getPrimaryKey(): array|string|null { if ($this->_primaryKey === null) { $schema = $this->getSchema(); @@ -856,7 +856,7 @@ 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 Closure|array|string $fields = array(); $fields A hash of field => new value. + * @param \Closure|array|string $fields = 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 @@ -1091,7 +1091,7 @@ protected function _dynamicFinder(string $method, array $args): QueryInterface * * @param string $method name of the method to be invoked * @param array $args List of arguments passed to the function - * @return QueryInterface + * @return \Cake\Datasource\QueryInterface * @throws \BadMethodCallException If the request dynamic finder cannot be found */ public function __call(string $method, array $args): QueryInterface diff --git a/src/Webservice/Driver/AbstractDriver.php b/src/Webservice/Driver/AbstractDriver.php index 7a15325..f1d54c8 100644 --- a/src/Webservice/Driver/AbstractDriver.php +++ b/src/Webservice/Driver/AbstractDriver.php @@ -98,7 +98,7 @@ public function getClient(): ?object * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice Instance of the webservice * @return $this */ - public function setWebservice(string $name, WebserviceInterface $webservice) : AbstractDriver + public function setWebservice(string $name, WebserviceInterface $webservice): AbstractDriver { $this->_webservices[$name] = $webservice; @@ -166,7 +166,7 @@ public function configName(): string * * @return $this */ - public function enableQueryLogging() : AbstractDriver + public function enableQueryLogging(): AbstractDriver { $this->_logQueries = true; @@ -178,7 +178,7 @@ public function enableQueryLogging() : AbstractDriver * * @return $this */ - public function disableQueryLogging() : AbstractDriver + public function disableQueryLogging(): AbstractDriver { $this->_logQueries = false; diff --git a/src/Webservice/Webservice.php b/src/Webservice/Webservice.php index 2e636c8..98cb554 100644 --- a/src/Webservice/Webservice.php +++ b/src/Webservice/Webservice.php @@ -221,7 +221,6 @@ protected function _executeQuery(Query $query, array $options = []): bool|int|Re Query::ACTION_DELETE => $this->_executeDeleteQuery($query, $options), default => false, }; - } /** From baf0470275b2afca96ab4b71a76952b69cc9a676 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Mon, 25 Mar 2024 18:01:57 +0100 Subject: [PATCH 09/21] Added composer dev requirements for easier debugging, linting fixed, part of the psalm errors fixed --- src/Model/Resource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Resource.php b/src/Model/Resource.php index 17e3cec..1c11b9c 100644 --- a/src/Model/Resource.php +++ b/src/Model/Resource.php @@ -30,7 +30,7 @@ class Resource implements EntityInterface, InvalidPropertyInterface * @param array $properties hash of properties to set in this resource * @param array $options list of options to use when creating this resource */ - public function __construct(array $properties = [], array $options = []) + final public function __construct(array $properties = [], array $options = []) { $options += [ 'useSetters' => true, From 072739539ddb4913236260be2136a8efa9484177 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Tue, 26 Mar 2024 17:54:19 +0100 Subject: [PATCH 10/21] Psalm, linting and typing fixed --- .github/workflows/ci.yml | 3 +- src/Datasource/Connection.php | 4 +- src/Datasource/Query.php | 254 ++++++++++++++---- src/Datasource/ResultSet.php | 9 +- src/Datasource/Schema.php | 10 +- src/Model/Endpoint.php | 115 ++++---- src/Model/EndpointLocator.php | 1 + src/Webservice/Driver/AbstractDriver.php | 5 +- src/Webservice/Webservice.php | 6 +- tests/TestCase/QueryTest.php | 2 +- .../src/Webservice/EndpointTestWebservice.php | 13 +- 11 files changed, 285 insertions(+), 137 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7de380..1f48e9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,8 @@ jobs: run: composer require cakephp/cakephp-codesniffer:^4.2 - name: Run phpcs - run: vendor/bin/phpcs --standard=CakePHP src/ tests/ + # Exclude Type hint sniffing, as it interferes with psalm + run: vendor/bin/phpcs --standard=CakePHP --exclude=CakePHP.Classes.ReturnTypeHint src/ tests/ - name: Run psalm if: success() || failure() diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index 3f8a48b..b5d5b52 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -14,8 +14,8 @@ /** * Class Connection * - * @method \Muffin\Webservice\Webservice\Driver\AbstractDriver setWebservice(string $name, \Muffin\Webservice\Datasource\WebserviceInterface $webservice) Proxy method through to the Driver - * @method \Muffin\Webservice\Datasource\WebserviceInterface getWebservice(string $name) Proxy method through to the Driver + * @method \Muffin\Webservice\Webservice\Driver\AbstractDriver setWebservice(string $name, \Muffin\Webservice\Webservice\WebserviceInterface $webservice) Proxy method through to the Driver + * @method \Muffin\Webservice\Webservice\WebserviceInterface getWebservice(string $name) Proxy method through to the Driver */ class Connection implements ConnectionInterface { diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index 332689c..a1e2478 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -5,7 +5,11 @@ use ArrayObject; use Cake\Collection\Iterator\MapReduce; +use Cake\Database\Expression\OrderByExpression; +use Cake\Database\Expression\QueryExpression; use Cake\Database\ExpressionInterface; +use Cake\Database\TypeMap; +use Cake\Database\TypeMapTrait; use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Datasource\QueryCacher; use Cake\Datasource\QueryInterface; @@ -26,10 +30,12 @@ /** * @template TKey * @template-covariant TValue - * @template-implements IteratorAggregate + * @template-implements \IteratorAggregate */ class Query implements IteratorAggregate, JsonSerializable, QueryInterface { + use TypeMapTrait; + public const ACTION_CREATE = 1; public const ACTION_READ = 2; public const ACTION_UPDATE = 3; @@ -109,16 +115,16 @@ class Query implements IteratorAggregate, JsonSerializable, QueryInterface /** * The result from the webservice * - * @var ResultSetInterface|null + * @var Resource|\Cake\Datasource\ResultSetInterface|int|bool|null */ - protected mixed $_results = null; + protected bool|int|Resource|ResultSetInterface|null $_results = null; /** * Instance of a endpoint object this query is bound to * - * @var \Cake\Datasource\RepositoryInterface + * @var \Muffin\Webservice\Model\Endpoint */ - protected RepositoryInterface $_endpoint; + protected Endpoint $_endpoint; /** * List of map-reduce routines that should be applied over the query @@ -198,18 +204,23 @@ public function aliasFields(array $fields, ?string $defaultAlias = null): array */ public function all(): ResultSetInterface { - if ($this->_results !== null) { - if (!($this->_results instanceof ResultSetInterface)) { - $this->_results = $this->decorateResults($this->_results); - } + if (is_iterable($this->_results)) { + $this->_results = $this->decorateResults($this->_results); return $this->_results; } - $results = null; + /** @psalm-suppress InternalMethod Could not find a better way apart from implementing it as a custom class **/ $results = $this->_cache?->fetch($this); if ($results === null) { - $results = $this->decorateResults($this->_execute()); + $res = $this->_execute(); + + if (!is_iterable($res)) { + return new ResultSet([], 0); + } + + $results = $this->decorateResults($res); + /** @psalm-suppress InternalMethod Could not find a better way apart from implementing it as a custom class **/ $this->_cache?->store($this, $results); } $this->_results = $results; @@ -220,10 +231,133 @@ public function all(): ResultSetInterface /** * @param \Closure|array|string $fields * @param bool $overwrite + * @return $this + */ + public function orderBy(Closure|array|string $fields, bool $overwrite = false): Query + { + if ($overwrite) { + $this->_parts['order'] = null; + } + + if (is_array($fields) && empty($fields)) { + return $this; + } + + $this->_parts['order'] ??= new OrderByExpression(); + $this->_conjugate('order', $fields, '', []); + + return $this; + } + + /** + * Helper function used to build conditions by composing QueryExpression objects. + * + * @param string $part Name of the query part to append the new part to + * @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $append Expression or builder function to append. + * to append. + * @param string $conjunction type of conjunction to be used to operate part + * @param array $types Associative array of type names used to bind values to query + * @return void + */ + protected function _conjugate( + string $part, + ExpressionInterface|Closure|array|string|null $append, + string $conjunction, + array $types + ): void { + /** @var \Cake\Database\Expression\QueryExpression $expression */ + $expression = $this->_parts[$part] ?: $this->newExpr(); + if ((is_array($append) && empty($append)) || $append === null) { + $this->_parts[$part] = $expression; + + return; + } + + if ($append instanceof Closure) { + $append = $append($this->newExpr(), $this); + } + + if ($expression->getConjunction() === $conjunction) { + $expression->add($append, $types); + } else { + $expression = $this->newExpr() + ->setConjunction($conjunction) + ->add([$expression, $append], $types); + } + + $this->_parts[$part] = $expression; + $this->_dirty(); + } + + /** + * Marks a query as dirty, removing any preprocessed information + * from in memory caching. + * * @return void */ - public function orderBy(Closure|array|string $fields, bool $overwrite = false): void + protected function _dirty(): void { + $this->_dirty = true; + } + + /** + * Returns a new QueryExpression object. This is a handy function when + * building complex queries using a fluent interface. You can also override + * this function in subclasses to use a more specialized QueryExpression class + * if required. + * + * You can optionally pass a single raw SQL string or an array or expressions in + * any format accepted by \Cake\Database\Expression\QueryExpression: + * + * ``` + * $expression = $query->expr(); // Returns an empty expression object + * $expression = $query->expr('Table.column = Table2.column'); // Return a raw SQL expression + * ``` + * + * @param \Cake\Database\ExpressionInterface|array|string|null $rawExpression A string, array or anything you want wrapped in an expression object + * @return \Cake\Database\Expression\QueryExpression + */ + public function newExpr(ExpressionInterface|array|string|null $rawExpression = null): QueryExpression + { + return $this->expr($rawExpression); + } + + /** + * Returns a new QueryExpression object. This is a handy function when + * building complex queries using a fluent interface. You can also override + * this function in subclasses to use a more specialized QueryExpression class + * if required. + * + * You can optionally pass a single raw SQL string or an array or expressions in + * any format accepted by \Cake\Database\Expression\QueryExpression: + * + * ``` + * $expression = $query->expr(); // Returns an empty expression object + * $expression = $query->expr('Table.column = Table2.column'); // Return a raw SQL expression + * ``` + * + * @param \Cake\Database\ExpressionInterface|array|string|null $rawExpression A string, array or anything you want wrapped in an expression object + * @return \Cake\Database\Expression\QueryExpression + */ + public function expr(ExpressionInterface|array|string|null $rawExpression = null): QueryExpression + { + $expression = new QueryExpression([], $this->getTypeMap()); + + if ($rawExpression !== null) { + $expression->add($rawExpression); + } + + return $expression; + } + + /** + * Returns the existing type map. + * + * @return \Cake\Database\TypeMap + */ + public function getTypeMap(): TypeMap + { + return $this->_typeMap ??= new TypeMap(); } /** @@ -244,7 +378,9 @@ public function toArray(): array */ public function setRepository(RepositoryInterface $repository): Query { - $this->_endpoint = $repository; + if ($repository instanceof Endpoint) { + $this->_endpoint = $repository; + } return $this; } @@ -350,7 +486,6 @@ public function setEndpoint(Endpoint $endpoint): Query */ public function getEndpoint(): Endpoint { - /** @var \Muffin\Webservice\Model\Endpoint */ return $this->_endpoint; } @@ -377,27 +512,6 @@ public function getWebservice(): WebserviceInterface return $this->_webservice; } - /** - * Apply custom finds to against an existing query object. - * - * Allows custom find methods to be combined and applied to each other. - * - * ``` - * $repository->find('all')->find('recent'); - * ``` - * - * The above is an example of stacking multiple finder methods onto - * a single query. - * - * @param string $finder The finder method to use. - * @param mixed ...$args Arguments that match up to finder-specific parameters - * @return static Returns a modified query. - */ - public function find(string $finder, mixed ...$args): static - { - return $this->_endpoint->callFinder($finder, $this, $args); - } - /** * Get the first result from the executing query or raise an exception. * @@ -435,17 +549,18 @@ public function aliasField(string $field, ?string $alias = null): array * @param \Closure|array|string|null $conditions The list of conditions. * @param array $types Not used, required to comply with QueryInterface. * @param bool $overwrite Whether to replace previous queries. - * @return \Muffin\Webservice\Datasource\Query|array|null + * @return \Cake\Datasource\QueryInterface|array */ + + /** @psalm-suppress ImplementedReturnTypeMismatch Not the nicest solution, but wishing to keep the functionality backwards compatible **/ public function where( Closure|array|string|null $conditions = null, array $types = [], bool $overwrite = false - ): Query|array|null { - if ($conditions === null) { - return $this->clause('where'); + ): QueryInterface { + if ($overwrite) { + $this->_parts['where'] = $conditions; } - $this->_parts['where'] = !$overwrite ? Hash::merge($this->clause('where'), $conditions) : $conditions; return $this; @@ -535,15 +650,11 @@ public function limit(?int $limit): Query /** * Set fields to save in resources * - * @param array|null $fields The field to set - * @return $this|array|null + * @param \Closure|array|string $fields The field to set + * @return $this */ - public function set(?array $fields = null): Query|array|null + public function set(Closure|array|string $fields): Query { - 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'); } @@ -633,15 +744,22 @@ public function count(): int return 0; } - if (!$this->_results) { + if ($this->_results === null || $this->_results === false) { $this->_execute(); } - if ($this->_results) { + if ($this->_results instanceof ResultSet) { return (int)$this->_results->total(); } + if ($this->_results instanceof ResultSetInterface) { + return $this->_results->count(); + } + if ($this->_results === null) { + return 0; + } - return 0; + // There is a single integer or boolean value + return 1; } /** @@ -689,7 +807,7 @@ public function triggerBeforeFind(): void /** * Execute the query * - * @return \Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet|int|bool + * @return Resource|\Cake\Datasource\ResultSetInterface|int|bool */ public function execute(): bool|int|Resource|ResultSetInterface { @@ -703,18 +821,17 @@ public function execute(): bool|int|Resource|ResultSetInterface /** * Executes this query and returns a traversable object containing the results * - * @return \Cake\Datasource\ResultSetInterface + * @return \Muffin\Webservice\Model\Resource|\Cake\Datasource\ResultSetInterface|int|bool */ - protected function _execute(): ResultSetInterface + protected function _execute(): bool|int|Resource|ResultSetInterface { $this->triggerBeforeFind(); - if ($this->_results) { + if (is_iterable($this->_results)) { $decorator = $this->decoratorClass(); return new $decorator($this->_results); } - /** @var \Cake\Datasource\ResultSetInterface */ return $this->_results = $this->_webservice->execute($this); } @@ -732,10 +849,10 @@ public function __debugInfo(): array 'offset' => $this->clause('offset'), 'page' => $this->clause('page'), 'limit' => $this->clause('limit'), - 'set' => $this->set(), + 'set' => $this->clause('set'), 'sort' => $this->clause('order'), 'extraOptions' => $this->getOptions(), - 'conditions' => $this->where(), + 'conditions' => $this->clause('where'), 'repository' => $this->getEndpoint(), 'webservice' => $this->getWebservice(), ]; @@ -973,4 +1090,27 @@ public function formatResults(?Closure $formatter = null, int|bool $mode = self: return $this; } + + /** + * Apply custom finds to against an existing query object. + * + * Allows custom find methods to be combined and applied to each other. + * + * ``` + * $repository->find('all')->find('recent'); + * ``` + * + * The above is an example of stacking multiple finder methods onto + * a single query. + * + * @param string $finder The finder method to use. + * @param mixed ...$args Arguments that match up to finder-specific parameters + * @return static Returns a modified query. + */ + /** @psalm-suppress MoreSpecificReturnType Couldn't get it to work with the interface and has no impact **/ + public function find(string $finder, mixed ...$args): static + { + /** @psalm-suppress LessSpecificReturnStatement Couldn't get it to work with the interface and has no impact **/ + return $this->_endpoint->callFinder($finder, $this, $args); + } } diff --git a/src/Datasource/ResultSet.php b/src/Datasource/ResultSet.php index a9ef4f4..6ad9aba 100644 --- a/src/Datasource/ResultSet.php +++ b/src/Datasource/ResultSet.php @@ -5,7 +5,6 @@ use Cake\Collection\CollectionTrait; use Cake\Datasource\ResultSetInterface; -use IteratorIterator; use Muffin\Webservice\Model\Resource; /** @package Muffin\Webservice\Datasource */ @@ -62,11 +61,9 @@ public function __construct(array $resources, ?int $total = null) * * Part of Iterator interface. * - * @return \Cake\Datasource\EntityInterface|array - * @psalm-return T + * @return Resource */ - #[\ReturnTypeWillChange] - public function current() + public function current(): Resource { return $this->_current; } @@ -124,6 +121,8 @@ public function valid(): bool * * @return int */ + + /** @psalm-suppress ImplementedReturnTypeMismatch This seems to be implemented with the key as an integer everywhere **/ public function key(): int { return $this->_index; diff --git a/src/Datasource/Schema.php b/src/Datasource/Schema.php index 54e116e..a21b351 100644 --- a/src/Datasource/Schema.php +++ b/src/Datasource/Schema.php @@ -32,7 +32,7 @@ class Schema implements SchemaInterface /** * A map with columns to types * - * @var array + * @var array */ protected array $_typeMap = []; @@ -53,7 +53,7 @@ class Schema implements SchemaInterface /** * Options for the endpoint. * - * @var array + * @var array */ protected array $_options = []; @@ -178,7 +178,7 @@ public function addColumn(string $name, array|string $attrs): Schema /** * Get the column names in the endpoint. * - * @return array + * @return list */ public function columns(): array { @@ -278,7 +278,7 @@ public function baseColumnType(string $column): ?string return null; } - if (TypeFactory::getMap($type)) { + if (TypeFactory::getMap($type) !== null) { $type = TypeFactory::build($type)->getBaseType(); } @@ -337,7 +337,7 @@ public function defaultValues(): array /** * Get the column(s) used for the primary key. * - * @return array Column name(s) for the primary key. An + * @return list Column name(s) for the primary key. An * empty list will be returned when the endpoint has no primary key. */ public function getPrimaryKey(): array diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index f3dc6e3..49cd35d 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -10,8 +10,10 @@ use Cake\Core\App; use Cake\Datasource\EntityInterface; use Cake\Datasource\Exception\InvalidPrimaryKeyException; +use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Datasource\QueryInterface; use Cake\Datasource\RepositoryInterface; +use Cake\Datasource\ResultSetInterface; use Cake\Datasource\RulesAwareTrait; use Cake\Datasource\RulesChecker; use Cake\Event\EventDispatcherInterface; @@ -21,6 +23,7 @@ use Cake\Utility\Inflector; use Cake\Validation\ValidatorAwareTrait; use Closure; +use Exception; use Muffin\Webservice\Datasource\Connection; use Muffin\Webservice\Datasource\Marshaller; use Muffin\Webservice\Datasource\Query; @@ -34,9 +37,14 @@ * The table equivalent of a webservice endpoint * * @package Muffin\Webservice\Model + * @template TSubject of object + * @implements \Cake\Event\EventDispatcherInterface */ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDispatcherInterface { + /** + * @use \Cake\Event\EventDispatcherTrait<\Muffin\Webservice\Model\Endpoint> + */ use EventDispatcherTrait; use RulesAwareTrait; use ValidatorAwareTrait; @@ -86,7 +94,7 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp /** * The name of the field that represents the primary key in the endpoint * - * @var array|string|null + * @var list|string|null */ protected array|string|null $_primaryKey = null; @@ -142,7 +150,7 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp * * @param array $config List of options for this endpoint */ - public function __construct(array $config = []) + final public function __construct(array $config = []) { if (!empty($config['alias'])) { $this->setAlias($config['alias']); @@ -343,44 +351,17 @@ public function setSchema(Schema|array $schema): Endpoint /** * Returns the schema endpoint object describing this endpoint's properties. * - * @return \Muffin\Webservice\Datasource\Schema + * @return \Muffin\Webservice\Datasource\Schema|null */ - public function getSchema(): Schema + public function getSchema(): ?Schema { if ($this->_schema === null) { - $this->_schema = $this->_initializeSchema($this->getWebservice()->describe($this->getName())); + $this->_schema = $this->getWebservice()?->describe($this->getName()); } return $this->_schema; } - /** - * Override this function in order to alter the schema used by this endpoint. - * This function is only called after fetching the schema out of the webservice. - * If you wish to provide your own schema to this table without touching the - * database, you can override schema() or inject the definitions though that - * method. - * - * ### Example: - * - * ``` - * protected function _initializeSchema(\Muffin\Webservice\Schema $schema) { - * $schema->addColumn('preferences', [ - * 'type' => 'string' - * ]); - * return $schema; - * } - * ``` - * - * @param \Muffin\Webservice\Datasource\Schema $schema The schema definition fetched from webservice. - * @return \Muffin\Webservice\Datasource\Schema the altered schema - * @api - */ - protected function _initializeSchema(Schema $schema): Schema - { - return $schema; - } - /** * Test to see if a Table has a specific field/column. * @@ -394,13 +375,13 @@ public function hasField(string $field): bool { $schema = $this->getSchema(); - return $schema->getColumn($field) !== null; + return $schema?->getColumn($field) !== null; } /** - * Returns the primary key field name + * Returns the current endpoint * - * @param array|string|null $key sets a new name to be used as primary key + * @param list|string|null $key sets a new name to be used as primary key * @return $this */ public function setPrimaryKey(string|array|null $key): Endpoint @@ -420,8 +401,8 @@ public function getPrimaryKey(): array|string|null { if ($this->_primaryKey === null) { $schema = $this->getSchema(); - $key = $schema->getPrimaryKey(); - if (count($key) === 1) { + $key = $schema?->getPrimaryKey(); + if ($key !== null && count($key) === 1) { $key = $key[0]; } $this->_primaryKey = $key; @@ -456,10 +437,10 @@ public function getDisplayField(): string|array $this->_displayField = array_shift($primary); $schema = $this->getSchema(); - if ($schema->getColumn('title')) { + if ($schema?->getColumn('title') !== null) { $this->_displayField = 'title'; } - if ($schema->getColumn('name')) { + if ($schema?->getColumn('name') !== null) { $this->_displayField = 'name'; } } @@ -507,6 +488,7 @@ public function getResourceClass(): string $alias = Inflector::singularize(substr(array_pop($parts), 0, -8)); /** @psalm-var class-string<\Muffin\Webservice\Model\Resource> $alias */ $name = implode('\\', array_slice($parts, 0, -1)) . '\Resource\\' . $alias; + /** @psalm-var class-string<\Muffin\Webservice\Model\Resource> $name */ if (!class_exists($name)) { return $this->_resourceClass = $default; } @@ -551,8 +533,10 @@ public function getInflectionMethod(): string public function setWebservice(string $alias, WebserviceInterface $webservice): Endpoint { $connection = $this->getConnection(); - $connection->setWebservice($alias, $webservice); - $this->_webservice = $connection->getWebservice($alias); + if ($connection !== null) { + $connection->setWebservice($alias, $webservice); + $this->_webservice = $connection->getWebservice($alias); + } return $this; } @@ -560,12 +544,12 @@ public function setWebservice(string $alias, WebserviceInterface $webservice): E /** * Get this endpoints associated webservice * - * @return \Muffin\Webservice\Webservice\WebserviceInterface + * @return \Muffin\Webservice\Webservice\WebserviceInterface|null */ - public function getWebservice(): WebserviceInterface + public function getWebservice(): ?WebserviceInterface { if ($this->_webservice === null) { - $this->_webservice = $this->getConnection()->getWebservice($this->getName()); + $this->_webservice = $this->getConnection()?->getWebservice($this->getName()); } return $this->_webservice; @@ -747,6 +731,7 @@ protected function _setFieldMatchers(array $options, array $keys): array * @param \Closure|string|null $cacheKey The cache key to use. If not provided * one will be autogenerated if `$cache` is not null. * @param mixed ...$args + * @psalm-suppress InvalidReturnType For backwards compatibility. This function can also return array * @return \Cake\Datasource\EntityInterface * @see \Cake\Datasource\RepositoryInterface::find() */ @@ -783,11 +768,11 @@ public function get( $query = $this->find($finder, $args)->where($conditions); - if ($cacheConfig) { - if (!$cacheKey) { + if (($cacheConfig !== false && $cacheConfig !== null) && is_callable($cache)) { + if ($cacheKey !== null) { $cacheKey = sprintf( 'get:%s.%s%s', - $this->getConnection()->configName(), + $this->getConnection()?->configName() ?? 'None', $this->getName(), json_encode($primaryKey) ); @@ -795,7 +780,12 @@ public function get( $cache($cacheKey, $cacheConfig); } - return $query->firstOrFail(); + $result = $query->firstOrFail(); + if ($result instanceof EntityInterface) { + return $result; + } + + throw new RecordNotFoundException(); } /** @@ -827,7 +817,7 @@ public function findOrCreate(mixed $search, ?callable $callback = null): EntityI $entity = $this->newEntity(); $entity->set($search, ['guard' => false]); - if ($callback) { + if (is_callable($callback)) { $callback($entity); } @@ -843,10 +833,16 @@ public function findOrCreate(mixed $search, ?callable $callback = null): EntityI * Creates a new Query instance for this repository * * @return \Muffin\Webservice\Datasource\Query + * @throws \Exception When non webservice is set */ public function query(): Query { - return new Query($this->getWebservice(), $this); + $webservice = $this->getWebservice(); + if ($webservice === null) { + throw new Exception('Webservice not initialized, cannot create query'); + } + + return new Query($webservice, $this); } /** @@ -863,8 +859,17 @@ public function query(): Query */ 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(); + $res = $this->query()->update()->where($conditions)->set($fields)->execute(); + + if ($res instanceof ResultSetInterface) { + return $res->count(); + } + if ($res === false) { + return 0; + } + + // The other datatypes indicate only a single entity is updated + return 1; } /** @@ -944,7 +949,7 @@ public function save(EntityInterface $entity, array|ArrayAccess $options = []): return $event->getResult(); } - $data = $entity->extract($this->getSchema()->columns(), true); + $data = $entity->extract($this->getSchema()?->columns() ?? [], true); if ($entity->isNew()) { $query = $this->query()->create(); @@ -954,7 +959,7 @@ public function save(EntityInterface $entity, array|ArrayAccess $options = []): $query->set($data); $result = $query->execute(); - if (!$result) { + if ($result === false) { return false; } @@ -1285,7 +1290,7 @@ public function __debugInfo(): array { $connectionName = ''; if ($this->getConnection() !== null) { - $connectionName = $this->getConnection()->configName() ?? 'None'; + $connectionName = $this->getConnection()?->configName() ?? 'None'; } return [ diff --git a/src/Model/EndpointLocator.php b/src/Model/EndpointLocator.php index 803c2af..9d639be 100644 --- a/src/Model/EndpointLocator.php +++ b/src/Model/EndpointLocator.php @@ -77,6 +77,7 @@ protected function createInstance(string $alias, array $options): RepositoryInte if (!str_contains($alias, '.')) { $connectionName = 'webservice'; } else { + /** @psalm-suppress PossiblyNullArgument Not clean, but cannot happen with incorrect configuration and was not a problem before **/ $pluginParts = explode('/', pluginSplit($alias)[0]); $connectionName = Inflector::underscore(end($pluginParts)); } diff --git a/src/Webservice/Driver/AbstractDriver.php b/src/Webservice/Driver/AbstractDriver.php index f1d54c8..7e9c669 100644 --- a/src/Webservice/Driver/AbstractDriver.php +++ b/src/Webservice/Driver/AbstractDriver.php @@ -206,7 +206,8 @@ public function isQueryLoggingEnabled(): bool */ public function __call(string $method, array $args): mixed { - if (!method_exists($this->getClient(), $method)) { + /** @psalm-suppress PossiblyNullArgument Only the left expression is executed if the getClient returns null **/ + if ($this->getClient() === null || !method_exists($this->getClient(), $method)) { throw new UnimplementedWebserviceMethodException([ 'name' => $this->getConfig('name'), 'method' => $method, @@ -253,7 +254,7 @@ protected function _createWebservice(string $className, array $options = []): We $fallbackWebserviceClass = end($namespaceParts); [$pluginName] = pluginSplit($className); - if ($pluginName) { + if ($pluginName !== null) { $fallbackWebserviceClass = $pluginName . '.' . $fallbackWebserviceClass; } diff --git a/src/Webservice/Webservice.php b/src/Webservice/Webservice.php index 98cb554..95ad1da 100644 --- a/src/Webservice/Webservice.php +++ b/src/Webservice/Webservice.php @@ -115,9 +115,9 @@ public function setEndpoint(string $endpoint): Webservice /** * Get the endpoint path for this webservice * - * @return string + * @return string|null */ - public function getEndpoint(): string + public function getEndpoint(): ?string { return $this->_endpoint; } @@ -323,7 +323,7 @@ protected function _logQuery(Query $query, LoggerInterface $logger): void } $logger->debug($query->getEndpoint()->getName(), [ - 'params' => $query->where(), + 'params' => $query->clause('where'), ]); } diff --git a/tests/TestCase/QueryTest.php b/tests/TestCase/QueryTest.php index 8f5e352..5312aff 100644 --- a/tests/TestCase/QueryTest.php +++ b/tests/TestCase/QueryTest.php @@ -127,7 +127,7 @@ public function testSet() ])); $this->assertEquals([ 'field' => 'value', - ], $this->query->set()); + ], $this->query->clause('set')); } public function testPage() diff --git a/tests/test_app/src/Webservice/EndpointTestWebservice.php b/tests/test_app/src/Webservice/EndpointTestWebservice.php index e2cd6a2..158a559 100644 --- a/tests/test_app/src/Webservice/EndpointTestWebservice.php +++ b/tests/test_app/src/Webservice/EndpointTestWebservice.php @@ -49,7 +49,7 @@ public function initialize(): void protected function _executeCreateQuery(Query $query, array $options = []): bool|Resource { - $fields = $query->set(); + $fields = $query->clause('set'); if (!is_numeric($fields['id'])) { return false; @@ -66,8 +66,9 @@ protected function _executeCreateQuery(Query $query, array $options = []): bool| protected function _executeReadQuery(Query $query, array $options = []): bool|ResultSet { - if (!empty($query->where()['id'])) { - $index = $this->conditionsToIndex($query->where()); + $whereConditions = $query->clause('where'); + if (!empty($whereConditions['id'])) { + $index = $this->conditionsToIndex($whereConditions); if (!isset($this->resources[$index])) { return new ResultSet([], 0); @@ -97,16 +98,16 @@ protected function _executeReadQuery(Query $query, array $options = []): bool|Re protected function _executeUpdateQuery(Query $query, array $options = []): int|bool|Resource { - $this->resources[$this->conditionsToIndex($query->where())]->set($query->set()); + $this->resources[$this->conditionsToIndex($query->clause('where'))]->set($query->clause('set')); - $this->resources[$this->conditionsToIndex($query->where())]->clean(); + $this->resources[$this->conditionsToIndex($query->clause('where'))]->clean(); return 1; } protected function _executeDeleteQuery(Query $query, array $options = []): int|bool { - $conditions = $query->where(); + $conditions = $query->clause('where'); if (is_int($conditions['id'])) { $exists = isset($this->resources[$this->conditionsToIndex($conditions)]); From 03d58b6b1b94951d5f7ed3f6b3b112e0adf5957d Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Tue, 26 Mar 2024 17:57:50 +0100 Subject: [PATCH 11/21] Pipeline now runs on PHP 8.2 by default --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f48e9d..6883142 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,11 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.1', '8.2', '8.3'] + php-version: ['8.2', '8.3'] db-type: ['mysql', 'pgsql'] prefer-lowest: [''] include: - - php-version: '8.1' + - php-version: '8.3' db-type: 'sqlite' prefer-lowest: 'prefer-lowest' @@ -61,14 +61,14 @@ jobs: if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi - if [[ ${{ matrix.php-version }} == '8.1' && ${{ matrix.db-type }} == 'mysql' ]]; then + if [[ ${{ matrix.php-version }} == '8.3' && ${{ matrix.db-type }} == 'mysql' ]]; then vendor/bin/phpunit --coverage-clover=coverage.xml else vendor/bin/phpunit fi - name: Code Coverage Report - if: success() && matrix.php-version == '8.1' && matrix.db-type == 'mysql' + if: success() && matrix.php-version == '8.3' && matrix.db-type == 'mysql' uses: codecov/codecov-action@v1 cs-stan: @@ -81,7 +81,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.3' extensions: mbstring, intl coverage: none tools: psalm:5.23.1, phpstan:1.10.65 From be135072c157786d305d633a6b7ccb008a4f7b3b Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Tue, 26 Mar 2024 18:30:02 +0100 Subject: [PATCH 12/21] PHPStan issues and issues it created fixed --- phpstan.neon | 15 --------- src/Datasource/Connection.php | 3 +- src/Datasource/Marshaller.php | 1 + src/Datasource/Query.php | 6 ++-- src/Model/Endpoint.php | 43 ++++++++++++++++++++---- src/Model/EndpointLocator.php | 2 +- src/Webservice/Driver/AbstractDriver.php | 1 + src/Webservice/Webservice.php | 1 + 8 files changed, 46 insertions(+), 26 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 16597c9..12e8698 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,18 +5,3 @@ parameters: treatPhpDocTypesAsCertain: false paths: - src/ - ignoreErrors: - - - message: "#^Call to an undefined method Cake\\\\Datasource\\\\RepositoryInterface\\:\\:callFinder\\(\\)\\.$#" - count: 1 - path: src/Datasource/Query.php - - - - message: "#^Call to an undefined method Cake\\\\Datasource\\\\RepositoryInterface\\:\\:getName\\(\\)\\.$#" - count: 1 - path: src/Datasource/Query.php - - - - message: "#^Method Muffin\\\\Webservice\\\\Datasource\\\\Query\\:\\:execute\\(\\) should return bool\\|int\\|Muffin\\\\Webservice\\\\Datasource\\\\ResultSet\\|Muffin\\\\Webservice\\\\Model\\\\Resource but returns Cake\\\\Datasource\\\\ResultSetInterface\\.$#" - count: 1 - path: src/Datasource/Query.php diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index b5d5b52..df8b069 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -57,7 +57,7 @@ public function __construct(array $config) } /** - * @param \Psr\SimpleCache\CacheInterface $cacher + * @param \Psr\SimpleCache\CacheInterface $cacher The cacher instance to use for query caching. * @return $this */ public function setCacher(CacheInterface $cacher): ConnectionInterface @@ -137,6 +137,7 @@ protected function _normalizeConfig(array $config): array */ public function __call(string $method, array $args): mixed { + /* @phpstan-ignore-next-line This is supported behavior for now: https://www.php.net/manual/en/function.call-user-func-array.php (example 1) */ return call_user_func_array([$this->_driver, $method], $args); } } diff --git a/src/Datasource/Marshaller.php b/src/Datasource/Marshaller.php index abc3d36..5045ab2 100644 --- a/src/Datasource/Marshaller.php +++ b/src/Datasource/Marshaller.php @@ -122,6 +122,7 @@ protected function _validate(array $data, array $options, bool $isNew): array ); } + /* @phpstan-ignore-next-line Magic method */ return $options['validate']->validate($data, $isNew); } diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index a1e2478..90d6f3d 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -229,8 +229,8 @@ public function all(): ResultSetInterface } /** - * @param \Closure|array|string $fields - * @param bool $overwrite + * @param \Closure|array|string $fields The field configuration for the order by clause + * @param bool $overwrite Whether to overwrite the existing conditions * @return $this */ public function orderBy(Closure|array|string $fields, bool $overwrite = false): Query @@ -1111,6 +1111,6 @@ public function formatResults(?Closure $formatter = null, int|bool $mode = self: public function find(string $finder, mixed ...$args): static { /** @psalm-suppress LessSpecificReturnStatement Couldn't get it to work with the interface and has no impact **/ - return $this->_endpoint->callFinder($finder, $this, $args); + return $this->_endpoint->callFinder($finder, $this, $args); /* @phpstan-ignore-line */ } } diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index 49cd35d..170ff22 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -199,6 +199,7 @@ final public function __construct(array $config = []) * instance is created through the EndpointRegistry without a connection. * * @return string + * @throws \Exception When the plugin name cannot be found * @see \Muffin\Webservice\Model\EndpointRegistry::get() */ public static function defaultConnectionName(): string @@ -206,6 +207,10 @@ public static function defaultConnectionName(): string $namespaceParts = explode('\\', static::class); $plugin = current(array_slice(array_reverse($namespaceParts), 3, 2)); + if ($plugin === false) { + throw new Exception('Could not find plugin name'); + } + if ($plugin === 'App') { return 'webservice'; } @@ -250,12 +255,19 @@ public function setName(string $name): Endpoint * Get the name of this endpoint * * @return string + * @throws \Exception When the endpoint could not be determined from the name */ public function getName(): string { if ($this->_name === null) { $endpoint = namespaceSplit(static::class); - $endpoint = substr(end($endpoint), 0, -8); + $name = end($endpoint); + + if ($name === false) { + throw new Exception('Could not find the name of the endpoint'); + } + + $endpoint = substr($name, 0, -8); $inflectMethod = $this->getInflectionMethod(); $this->_name = Inflector::{$inflectMethod}($endpoint); @@ -427,10 +439,10 @@ public function setDisplayField(string|array $field): Endpoint /** * Get the endpoints current display field * - * @return array|string + * @return array|string|null * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no schema exists to fetch the key from */ - public function getDisplayField(): string|array + public function getDisplayField(): string|array|null { if ($this->_displayField === null) { $primary = (array)$this->getPrimaryKey(); @@ -730,7 +742,7 @@ protected function _setFieldMatchers(array $options, array $keys): array * 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. - * @param mixed ...$args + * @param mixed ...$args Additional arguments for configuring things like caching. * @psalm-suppress InvalidReturnType For backwards compatibility. This function can also return array * @return \Cake\Datasource\EntityInterface * @see \Cake\Datasource\RepositoryInterface::find() @@ -867,6 +879,9 @@ public function updateAll(Closure|array|string $fields, Closure|array|string|nul if ($res === false) { return 0; } + if (is_integer($res)) { + return $res; + } // The other datatypes indicate only a single entity is updated return 1; @@ -882,13 +897,27 @@ public function updateAll(Closure|array|string $fields, Closure|array|string|nul * * @param mixed $conditions Conditions to be used, accepts anything Query::where() can take. * @return int Count of affected rows. + * @throws \Exception When the delete action could not be executed * @see \Muffin\Webservice\Endpoint::delete() * @psalm-suppress InvalidReturnStatement * @psalm-suppress InvalidReturnType */ public function deleteAll(mixed $conditions): int { - return $this->query()->delete()->where($conditions)->execute(); + $res = $this->query()->delete()->where($conditions)->execute(); + + if ($res instanceof ResultSetInterface) { + return $res->count(); + } + if ($res === false) { + return 0; + } + if (is_integer($res)) { + return $res; + } + + // The other datatypes indicate only a single entity is updated + return 1; } /** @@ -1285,12 +1314,14 @@ public function buildRules(RulesChecker $rules): RulesChecker * Returns a handy representation of this endpoint * * @return array + * @throws \Exception When the name of this endpoint could not be determined */ public function __debugInfo(): array { $connectionName = ''; if ($this->getConnection() !== null) { - $connectionName = $this->getConnection()?->configName() ?? 'None'; + /** @psalm-suppress PossiblyNullReference getConnection cannot be null, as checked before entering this scope **/ + $connectionName = $this->getConnection()->configName(); } return [ diff --git a/src/Model/EndpointLocator.php b/src/Model/EndpointLocator.php index 9d639be..dd7a8a9 100644 --- a/src/Model/EndpointLocator.php +++ b/src/Model/EndpointLocator.php @@ -78,7 +78,7 @@ protected function createInstance(string $alias, array $options): RepositoryInte $connectionName = 'webservice'; } else { /** @psalm-suppress PossiblyNullArgument Not clean, but cannot happen with incorrect configuration and was not a problem before **/ - $pluginParts = explode('/', pluginSplit($alias)[0]); + $pluginParts = explode('/', pluginSplit($alias)[0]); /* @phpstan-ignore-line */ $connectionName = Inflector::underscore(end($pluginParts)); } } diff --git a/src/Webservice/Driver/AbstractDriver.php b/src/Webservice/Driver/AbstractDriver.php index 7e9c669..d1e5b37 100644 --- a/src/Webservice/Driver/AbstractDriver.php +++ b/src/Webservice/Driver/AbstractDriver.php @@ -214,6 +214,7 @@ public function __call(string $method, array $args): mixed ]); } + /* @phpstan-ignore-next-line This is supported behavior for now: https://www.php.net/manual/en/function.call-user-func-array.php (example 1) */ return call_user_func_array([$this->_client, $method], $args); } diff --git a/src/Webservice/Webservice.php b/src/Webservice/Webservice.php index 95ad1da..b72bf89 100644 --- a/src/Webservice/Webservice.php +++ b/src/Webservice/Webservice.php @@ -303,6 +303,7 @@ protected function _executeDeleteQuery(Query $query, array $options = []): int|b */ protected function _createResource(string $resourceClass, array $properties = []): Resource { + /* @phpstan-ignore-next-line See psalm suppress comments */ return new $resourceClass($properties, [ 'markClean' => true, 'markNew' => false, From 64007a11f284ef55f51d175318f16eeb915cca94 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Tue, 26 Mar 2024 18:32:01 +0100 Subject: [PATCH 13/21] Psalm suppress statements included in docblocks --- src/Datasource/Query.php | 9 +++------ src/Datasource/ResultSet.php | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index 90d6f3d..9263e04 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -549,10 +549,8 @@ public function aliasField(string $field, ?string $alias = null): array * @param \Closure|array|string|null $conditions The list of conditions. * @param array $types Not used, required to comply with QueryInterface. * @param bool $overwrite Whether to replace previous queries. - * @return \Cake\Datasource\QueryInterface|array - */ - - /** @psalm-suppress ImplementedReturnTypeMismatch Not the nicest solution, but wishing to keep the functionality backwards compatible **/ + * @return \Cake\Datasource\QueryInterface + * @psalm-suppress ImplementedReturnTypeMismatch Not the nicest solution, but wishing to keep the functionality backwards compatible **/ public function where( Closure|array|string|null $conditions = null, array $types = [], @@ -1106,8 +1104,7 @@ public function formatResults(?Closure $formatter = null, int|bool $mode = self: * @param string $finder The finder method to use. * @param mixed ...$args Arguments that match up to finder-specific parameters * @return static Returns a modified query. - */ - /** @psalm-suppress MoreSpecificReturnType Couldn't get it to work with the interface and has no impact **/ + * @psalm-suppress MoreSpecificReturnType Couldn't get it to work with the interface and has no impact **/ public function find(string $finder, mixed ...$args): static { /** @psalm-suppress LessSpecificReturnStatement Couldn't get it to work with the interface and has no impact **/ diff --git a/src/Datasource/ResultSet.php b/src/Datasource/ResultSet.php index 6ad9aba..3a0b733 100644 --- a/src/Datasource/ResultSet.php +++ b/src/Datasource/ResultSet.php @@ -120,9 +120,7 @@ public function valid(): bool * Part of Iterator interface. * * @return int - */ - - /** @psalm-suppress ImplementedReturnTypeMismatch This seems to be implemented with the key as an integer everywhere **/ + * @psalm-suppress ImplementedReturnTypeMismatch This seems to be implemented with the key as an integer everywhere **/ public function key(): int { return $this->_index; From 89b3dc37e79eabef53c5b3628dbe5fe19039d23b Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Tue, 26 Mar 2024 18:40:01 +0100 Subject: [PATCH 14/21] Psalm suppress statements included in docblocks --- src/Datasource/Connection.php | 1 + src/Datasource/Query.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index df8b069..13b89df 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -76,6 +76,7 @@ public function getCacher(): CacheInterface /** * {@inheritDoc} * + * @param string $role Parameter is not used * @see \Cake\Datasource\ConnectionInterface::getDriver() * @return \Muffin\Webservice\Webservice\Driver\AbstractDriver */ diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index 9263e04..bb75c03 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -549,13 +549,13 @@ public function aliasField(string $field, ?string $alias = null): array * @param \Closure|array|string|null $conditions The list of conditions. * @param array $types Not used, required to comply with QueryInterface. * @param bool $overwrite Whether to replace previous queries. - * @return \Cake\Datasource\QueryInterface + * @return Query * @psalm-suppress ImplementedReturnTypeMismatch Not the nicest solution, but wishing to keep the functionality backwards compatible **/ public function where( Closure|array|string|null $conditions = null, array $types = [], bool $overwrite = false - ): QueryInterface { + ): Query { if ($overwrite) { $this->_parts['where'] = $conditions; } From 1c355c8c76a8d5beb284cb24abf4cf94d6a3667e Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Tue, 26 Mar 2024 18:41:52 +0100 Subject: [PATCH 15/21] Query where replaced QueryInterface with Query --- src/Datasource/Query.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index bb75c03..1645e9b 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -549,8 +549,9 @@ public function aliasField(string $field, ?string $alias = null): array * @param \Closure|array|string|null $conditions The list of conditions. * @param array $types Not used, required to comply with QueryInterface. * @param bool $overwrite Whether to replace previous queries. - * @return Query - * @psalm-suppress ImplementedReturnTypeMismatch Not the nicest solution, but wishing to keep the functionality backwards compatible **/ + * @return \Muffin\Webservice\Datasource\Query + * @psalm-suppress ImplementedReturnTypeMismatch Not the nicest solution, but wishing to keep the functionality backwards compatible + */ public function where( Closure|array|string|null $conditions = null, array $types = [], From 8eb7baf70fc0500191cf9bc67e3ba907dbc2a2e7 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Tue, 26 Mar 2024 18:43:55 +0100 Subject: [PATCH 16/21] Docblock fixed --- src/Datasource/Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index 1645e9b..98cd211 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -549,7 +549,7 @@ public function aliasField(string $field, ?string $alias = null): array * @param \Closure|array|string|null $conditions The list of conditions. * @param array $types Not used, required to comply with QueryInterface. * @param bool $overwrite Whether to replace previous queries. - * @return \Muffin\Webservice\Datasource\Query + * @return $this * @psalm-suppress ImplementedReturnTypeMismatch Not the nicest solution, but wishing to keep the functionality backwards compatible */ public function where( From aed6bc415cc4d79a0f9eff75fcf7abef090dad5d Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Wed, 3 Apr 2024 17:02:17 +0200 Subject: [PATCH 17/21] Implemented ADmad's suggestions about dependencies, phpunit, psalm and phpstan isues --- .github/workflows/ci.yml | 13 +++---- composer.json | 2 +- phpunit.xml.dist | 2 +- src/Datasource/Connection.php | 3 +- src/Datasource/Query.php | 48 +++++++++++++----------- src/Datasource/ResultSet.php | 2 +- src/Datasource/Schema.php | 12 ++++-- src/Model/Endpoint.php | 26 +++++++------ src/Model/EndpointLocator.php | 2 +- src/Webservice/Driver/AbstractDriver.php | 8 ++-- src/Webservice/Webservice.php | 6 +-- 11 files changed, 66 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6883142..85e56fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,11 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.2', '8.3'] + php-version: ['8.1', '8.2', '8.3'] db-type: ['mysql', 'pgsql'] prefer-lowest: [''] include: - - php-version: '8.3' + - php-version: '8.1' db-type: 'sqlite' prefer-lowest: 'prefer-lowest' @@ -61,14 +61,14 @@ jobs: if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi - if [[ ${{ matrix.php-version }} == '8.3' && ${{ matrix.db-type }} == 'mysql' ]]; then + if [[ ${{ matrix.php-version }} == '8.1' && ${{ matrix.db-type }} == 'mysql' ]]; then vendor/bin/phpunit --coverage-clover=coverage.xml else vendor/bin/phpunit fi - name: Code Coverage Report - if: success() && matrix.php-version == '8.3' && matrix.db-type == 'mysql' + if: success() && matrix.php-version == '8.1' && matrix.db-type == 'mysql' uses: codecov/codecov-action@v1 cs-stan: @@ -81,7 +81,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.1' extensions: mbstring, intl coverage: none tools: psalm:5.23.1, phpstan:1.10.65 @@ -90,8 +90,7 @@ jobs: run: composer require cakephp/cakephp-codesniffer:^4.2 - name: Run phpcs - # Exclude Type hint sniffing, as it interferes with psalm - run: vendor/bin/phpcs --standard=CakePHP --exclude=CakePHP.Classes.ReturnTypeHint src/ tests/ + run: vendor/bin/phpcs --standard=CakePHP src/ tests/ - name: Run psalm if: success() || failure() diff --git a/composer.json b/composer.json index 5937c3e..5361dbb 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "require-dev": { "cakephp/cakephp": "^5.0", "cakephp/cakephp-codesniffer": "^5.0", - "phpunit/phpunit": "^10.1", + "phpunit/phpunit": "^10.5.5", "ext-mbstring": "*", "vimeo/psalm": "5.23.1", "phpstan/phpstan": "1.10.65" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 36952e4..4891fcf 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ - + ./tests/ diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index 13b89df..39b492a 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -58,7 +58,8 @@ public function __construct(array $config) /** * @param \Psr\SimpleCache\CacheInterface $cacher The cacher instance to use for query caching. - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function setCacher(CacheInterface $cacher): ConnectionInterface { diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index 98cd211..f688e8f 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -231,7 +231,8 @@ public function all(): ResultSetInterface /** * @param \Closure|array|string $fields The field configuration for the order by clause * @param bool $overwrite Whether to overwrite the existing conditions - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function orderBy(Closure|array|string $fields, bool $overwrite = false): Query { @@ -374,7 +375,8 @@ public function toArray(): array * 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 + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function setRepository(RepositoryInterface $repository): Query { @@ -399,7 +401,7 @@ public function getRepository(): RepositoryInterface /** * Mark the query as create * - * @return $this + * @return self */ public function create(): Query { @@ -411,7 +413,7 @@ public function create(): Query /** * Mark the query as read * - * @return $this + * @return self */ public function read(): Query { @@ -423,7 +425,7 @@ public function read(): Query /** * Mark the query as update * - * @return $this + * @return self */ public function update(): Query { @@ -435,7 +437,7 @@ public function update(): Query /** * Mark the query as delete * - * @return $this + * @return self */ public function delete(): Query { @@ -468,8 +470,7 @@ public function clause(string $name): mixed * Set the endpoint to be used * * @param \Muffin\Webservice\Model\Endpoint $endpoint The endpoint to use - * @return $this - * @psalm-suppress LessSpecificReturnStatement + * @return self */ public function setEndpoint(Endpoint $endpoint): Query { @@ -482,7 +483,6 @@ public function setEndpoint(Endpoint $endpoint): Query * Set the endpoint to be used * * @return \Muffin\Webservice\Model\Endpoint - * @psalm-suppress MoreSpecificReturnType */ public function getEndpoint(): Endpoint { @@ -493,7 +493,7 @@ public function getEndpoint(): Endpoint * Set the webservice to be used * * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice The webservice to use - * @return $this + * @return self */ public function setWebservice(WebserviceInterface $webservice): Query { @@ -549,8 +549,8 @@ public function aliasField(string $field, ?string $alias = null): array * @param \Closure|array|string|null $conditions The list of conditions. * @param array $types Not used, required to comply with QueryInterface. * @param bool $overwrite Whether to replace previous queries. - * @return $this - * @psalm-suppress ImplementedReturnTypeMismatch Not the nicest solution, but wishing to keep the functionality backwards compatible + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function where( Closure|array|string|null $conditions = null, @@ -570,7 +570,7 @@ public function where( * * @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 + * @return self * @see \Cake\Database\Query::where() * @see \Cake\Database\Type * @psalm-suppress PossiblyInvalidArgument @@ -586,7 +586,7 @@ public function andWhere(string|array $conditions, array $types = []): Query * Charge this query's action * * @param int $action Action to use - * @return $this + * @return self */ public function action(int $action): Query { @@ -607,7 +607,8 @@ public function action(int $action): Query * @param int $num The page number you want. * @param int|null $limit The number of rows you want in the page. If null * the current limit clause will be used. - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function page(int $num, ?int $limit = null): Query { @@ -637,7 +638,8 @@ public function page(int $num, ?int $limit = null): Query * ``` * * @param ?int $limit number of records to be returned - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function limit(?int $limit): Query { @@ -650,7 +652,7 @@ public function limit(?int $limit): Query * Set fields to save in resources * * @param \Closure|array|string $fields The field to set - * @return $this + * @return self */ public function set(Closure|array|string $fields): Query { @@ -688,7 +690,8 @@ public function offset(?int $offset): Query|QueryInterface * * @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 + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function order(array|ExpressionInterface|Closure|string $fields, bool $overwrite = false): Query { @@ -880,7 +883,8 @@ public function jsonSerialize(): ResultSetInterface * * @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 + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function select(ExpressionInterface|Closure|array|string|int|float $fields, bool $overwrite = false): Query { @@ -957,7 +961,7 @@ protected function decorateResults(iterable $result): ResultSetInterface * @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 + * @return self * @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): Query @@ -1013,7 +1017,7 @@ public function isEagerLoaded(): bool * passed, the current configured query `_eagerLoaded` value is returned. * * @param bool $value Whether to eager load. - * @return $this + * @return self */ public function eagerLoaded(bool $value): Query { @@ -1062,7 +1066,7 @@ public function eagerLoaded(bool $value): Query * * @param \Closure|null $formatter The formatting function * @param int|bool $mode Whether to overwrite, append or prepend the formatter. - * @return $this + * @return self * @throws \InvalidArgumentException */ public function formatResults(?Closure $formatter = null, int|bool $mode = self::APPEND): Query diff --git a/src/Datasource/ResultSet.php b/src/Datasource/ResultSet.php index 3a0b733..9e224e4 100644 --- a/src/Datasource/ResultSet.php +++ b/src/Datasource/ResultSet.php @@ -120,7 +120,7 @@ public function valid(): bool * Part of Iterator interface. * * @return int - * @psalm-suppress ImplementedReturnTypeMismatch This seems to be implemented with the key as an integer everywhere **/ + */ public function key(): int { return $this->_index; diff --git a/src/Datasource/Schema.php b/src/Datasource/Schema.php index a21b351..b303e56 100644 --- a/src/Datasource/Schema.php +++ b/src/Datasource/Schema.php @@ -157,7 +157,8 @@ public function name(): string * * @param string $name The name of the column * @param array|string $attrs The attributes for the column. - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function addColumn(string $name, array|string $attrs): Schema { @@ -219,7 +220,8 @@ public function hasColumn(string $name): bool * If the column is not defined in the table, no error will be raised. * * @param string $name The name of the column - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function removeColumn(string $name): Schema { @@ -233,7 +235,8 @@ public function removeColumn(string $name): Schema * * @param string $name Column name * @param string $type Type to set for the column - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function setColumnType(string $name, string $type): Schema { @@ -358,7 +361,8 @@ public function getPrimaryKey(): array * Set the schema options for an endpoint * * @param array $options Array of options to set - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function setOptions(array $options): Schema { diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index 170ff22..1ebe23f 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -241,7 +241,7 @@ public function initialize(array $config): void * Set the name of this endpoint * * @param string $name The name for this endpoint instance - * @return $this + * @return self */ public function setName(string $name): Endpoint { @@ -291,7 +291,8 @@ public function aliasField(string $field): string * Sets the table registry key used to create this table instance. * * @param string $registryAlias The key used to access this object. - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function setRegistryAlias(string $registryAlias): Endpoint { @@ -318,7 +319,7 @@ public function getRegistryAlias(): string * Sets the connection driver. * * @param \Muffin\Webservice\Datasource\Connection $connection Connection instance - * @return $this + * @return self */ public function setConnection(Connection $connection): Endpoint { @@ -347,7 +348,8 @@ public function getConnection(): ?Connection * out of it and used as the schema for this endpoint. * * @param \Muffin\Webservice\Datasource\Schema|array $schema Either an array of fields and config, or a schema object - * @return $this + * @return self + * @throws \Exception */ public function setSchema(Schema|array $schema): Endpoint { @@ -364,6 +366,7 @@ public function setSchema(Schema|array $schema): Endpoint * Returns the schema endpoint object describing this endpoint's properties. * * @return \Muffin\Webservice\Datasource\Schema|null + * @throws \Exception */ public function getSchema(): ?Schema { @@ -394,7 +397,7 @@ public function hasField(string $field): bool * Returns the current endpoint * * @param list|string|null $key sets a new name to be used as primary key - * @return $this + * @return self */ public function setPrimaryKey(string|array|null $key): Endpoint { @@ -427,7 +430,7 @@ public function getPrimaryKey(): array|string|null * Sets the endpoint display field * * @param array|string $field The new field to use as the display field - * @return $this + * @return self */ public function setDisplayField(string|array $field): Endpoint { @@ -464,7 +467,7 @@ public function getDisplayField(): string|array|null * Set the resource class name used to hydrate resources for this endpoint * * @param string $name Name of the class to use - * @return $this + * @return self * @throws \Muffin\Webservice\Model\Exception\MissingResourceClassException If the resource class specified does not exist */ public function setResourceClass(string $name): Endpoint @@ -515,7 +518,7 @@ public function getResourceClass(): string * Set a new inflection method * * @param string $method The name of the inflection method - * @return $this + * @return self */ public function setInflectionMethod(string $method): Endpoint { @@ -539,7 +542,7 @@ public function getInflectionMethod(): string * * @param string $alias Alias for the webservice * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice The webservice instance - * @return $this + * @return self * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no driver exists for the endpoint */ public function setWebservice(string $alias, WebserviceInterface $webservice): Endpoint @@ -899,8 +902,6 @@ public function updateAll(Closure|array|string $fields, Closure|array|string|nul * @return int Count of affected rows. * @throws \Exception When the delete action could not be executed * @see \Muffin\Webservice\Endpoint::delete() - * @psalm-suppress InvalidReturnStatement - * @psalm-suppress InvalidReturnType */ public function deleteAll(mixed $conditions): int { @@ -1339,7 +1340,8 @@ public function __debugInfo(): array * Set the endpoint alias * * @param string $alias Alias for this endpoint - * @return $this + * @return self + * @psalm-suppress LessSpecificImplementedReturnType */ public function setAlias(string $alias): Endpoint { diff --git a/src/Model/EndpointLocator.php b/src/Model/EndpointLocator.php index dd7a8a9..2d6d470 100644 --- a/src/Model/EndpointLocator.php +++ b/src/Model/EndpointLocator.php @@ -23,8 +23,8 @@ class EndpointLocator extends AbstractLocator * @param string $alias The alias to set. * @param \Muffin\Webservice\Model\Endpoint $repository The repository to set. * @return \Muffin\Webservice\Model\Endpoint - * @psalm-suppress MoreSpecificImplementedParamType * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress MoreSpecificImplementedParamType Not a nice solution, but in this plugin, we only support Endpoints */ public function set(string $alias, RepositoryInterface $repository): Endpoint { diff --git a/src/Webservice/Driver/AbstractDriver.php b/src/Webservice/Driver/AbstractDriver.php index d1e5b37..a955407 100644 --- a/src/Webservice/Driver/AbstractDriver.php +++ b/src/Webservice/Driver/AbstractDriver.php @@ -72,7 +72,7 @@ abstract public function initialize(): void; * Set the client instance this driver will use to make requests * * @param object $client Client instance - * @return $this + * @return self */ public function setClient(object $client): AbstractDriver { @@ -96,7 +96,7 @@ public function getClient(): ?object * * @param string $name The registry alias for the webservice instance * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice Instance of the webservice - * @return $this + * @return self */ public function setWebservice(string $name, WebserviceInterface $webservice): AbstractDriver { @@ -164,7 +164,7 @@ public function configName(): string /** * Enable query logging for the driver * - * @return $this + * @return self */ public function enableQueryLogging(): AbstractDriver { @@ -176,7 +176,7 @@ public function enableQueryLogging(): AbstractDriver /** * Disable query logging for the driver * - * @return $this + * @return self */ public function disableQueryLogging(): AbstractDriver { diff --git a/src/Webservice/Webservice.php b/src/Webservice/Webservice.php index b72bf89..08d2637 100644 --- a/src/Webservice/Webservice.php +++ b/src/Webservice/Webservice.php @@ -76,7 +76,7 @@ public function initialize(): void * Set the webservice driver and return the instance for chaining * * @param \Muffin\Webservice\Webservice\Driver\AbstractDriver $driver Instance of the driver - * @return $this + * @return self */ public function setDriver(AbstractDriver $driver): Webservice { @@ -103,7 +103,7 @@ public function getDriver(): AbstractDriver * Set the endpoint path this webservice uses * * @param string $endpoint Endpoint path - * @return $this + * @return self */ public function setEndpoint(string $endpoint): Webservice { @@ -209,8 +209,6 @@ public function describe(string $endpoint): Schema * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use * @return \Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet|int|bool - * @psalm-suppress NullableReturnStatement - * @psalm-suppress InvalidNullableReturnType */ protected function _executeQuery(Query $query, array $options = []): bool|int|Resource|ResultSet { From a089d8d6af2051169c9108f2aece34c0a6d7f1f6 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Thu, 4 Apr 2024 15:15:46 +0200 Subject: [PATCH 18/21] Implemented the second part of the smaller suggestions --- .github/workflows/ci.yml | 4 +- src/Datasource/Connection.php | 10 ++-- src/Datasource/Marshaller.php | 24 ++++---- src/Datasource/Query.php | 60 ++++++++++--------- src/Model/Endpoint.php | 41 +++++++------ src/Model/EndpointLocator.php | 17 ++++-- src/{Plugin.php => WebservicePlugin.php} | 2 +- tests/TestCase/AbstractDriverTest.php | 10 ++-- tests/TestCase/Webservice/WebserviceTest.php | 6 +- tests/bootstrap.php | 4 +- .../Driver/{TestDriver.php => Test.php} | 2 +- 11 files changed, 98 insertions(+), 82 deletions(-) rename src/{Plugin.php => WebservicePlugin.php} (94%) rename tests/test_app/src/Webservice/Driver/{TestDriver.php => Test.php} (93%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85e56fe..c127b53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,9 +62,9 @@ jobs: if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi if [[ ${{ matrix.php-version }} == '8.1' && ${{ matrix.db-type }} == 'mysql' ]]; then - vendor/bin/phpunit --coverage-clover=coverage.xml + vendor/bin/phpunit tests/TestCase --coverage-clover=coverage.xml else - vendor/bin/phpunit + vendor/bin/phpunit tests/TestCase fi - name: Code Coverage Report diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php index 39b492a..63a161b 100644 --- a/src/Datasource/Connection.php +++ b/src/Datasource/Connection.php @@ -8,7 +8,6 @@ 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; /** @@ -50,9 +49,10 @@ public function __construct(array $config) $tempDriver = new $driver($config); - if (!($tempDriver instanceof AbstractDriver)) { - throw new UnexpectedDriverException(['driver' => $driver]); - } + assert( + $tempDriver instanceof AbstractDriver, + '`$config[\'driver\']` must be an instance of `' . AbstractDriver::class . '`.' + ); $this->_driver = $tempDriver; } @@ -121,7 +121,7 @@ protected function _normalizeConfig(array $config): array throw new MissingConnectionException(['name' => $config['name']]); } - $config['driver'] = App::className($config['service'], 'Webservice/Driver', 'Driver'); + $config['driver'] = App::className($config['service'], 'Webservice/Driver'); if (!$config['driver']) { throw new MissingDriverException(['driver' => $config['driver']]); } diff --git a/src/Datasource/Marshaller.php b/src/Datasource/Marshaller.php index 5045ab2..becb2fd 100644 --- a/src/Datasource/Marshaller.php +++ b/src/Datasource/Marshaller.php @@ -109,21 +109,25 @@ protected function _validate(array $data, array $options, bool $isNew): array if (!$options['validate']) { return []; } + + $validator = null; if ($options['validate'] === true) { - $options['validate'] = $this->_endpoint->getValidator('default'); + $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']; } - 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'])) - ); + if (!is_callable([$validator, 'validate'])) { + throw new RuntimeException(sprintf( + '"validate" must be a boolean, a string or an object with method "errors()". Got %s instead.', + gettype($options['validate']) + )); } - /* @phpstan-ignore-next-line Magic method */ - return $options['validate']->validate($data, $isNew); + return $validator->validate($data, $isNew); } /** diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index f688e8f..88918a5 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -380,9 +380,11 @@ public function toArray(): array */ public function setRepository(RepositoryInterface $repository): Query { - if ($repository instanceof Endpoint) { - $this->_endpoint = $repository; - } + assert( + $repository instanceof Endpoint, + '`$repository` must be an instance of `' . Endpoint::class . '`.' + ); + $this->_endpoint = $repository; return $this; } @@ -391,9 +393,9 @@ public function setRepository(RepositoryInterface $repository): Query * Returns the default repository object that will be used by this query, * that is, the table that will appear in the from clause. * - * @return \Cake\Datasource\RepositoryInterface + * @return \Muffin\Webservice\Model\Endpoint */ - public function getRepository(): RepositoryInterface + public function getRepository(): Endpoint { return $this->_endpoint; } @@ -512,11 +514,33 @@ public function getWebservice(): WebserviceInterface return $this->_webservice; } + /** + * Apply custom finds to against an existing query object. + * + * Allows custom find methods to be combined and applied to each other. + * + * ``` + * $repository->find('all')->find('recent'); + * ``` + * + * The above is an example of stacking multiple finder methods onto + * a single query. + * + * @param string $finder The finder method to use. + * @param mixed ...$args Arguments that match up to finder-specific parameters + * @return static Returns a modified query. + * @psalm-suppress MoreSpecificReturnType Couldn't get it to work with the interface and has no impact **/ + public function find(string $finder, mixed ...$args): static + { + /** @psalm-suppress LessSpecificReturnStatement Couldn't get it to work with the interface and has no impact **/ + return $this->_endpoint->callFinder($finder, $this, $args); /* @phpstan-ignore-line */ + } + /** * Get the first result from the executing query or raise an exception. * * @return mixed The first result from the ResultSet. - * @throws \Cake\Datasource\Exception\RecordNotFoundException When there is no first record. + * @throws \Cake\Datasource\Exception\RecordNotFoundException|\Exception When there is no first record. */ public function firstOrFail(): mixed { @@ -668,7 +692,7 @@ public function set(Closure|array|string $fields): Query /** * @inheritDoc */ - public function offset(?int $offset): Query|QueryInterface + public function offset(?int $offset): QueryInterface { $this->_parts['offset'] = $offset; @@ -1093,26 +1117,4 @@ public function formatResults(?Closure $formatter = null, int|bool $mode = self: return $this; } - - /** - * Apply custom finds to against an existing query object. - * - * Allows custom find methods to be combined and applied to each other. - * - * ``` - * $repository->find('all')->find('recent'); - * ``` - * - * The above is an example of stacking multiple finder methods onto - * a single query. - * - * @param string $finder The finder method to use. - * @param mixed ...$args Arguments that match up to finder-specific parameters - * @return static Returns a modified query. - * @psalm-suppress MoreSpecificReturnType Couldn't get it to work with the interface and has no impact **/ - public function find(string $finder, mixed ...$args): static - { - /** @psalm-suppress LessSpecificReturnStatement Couldn't get it to work with the interface and has no impact **/ - return $this->_endpoint->callFinder($finder, $this, $args); /* @phpstan-ignore-line */ - } } diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index 1ebe23f..94841c5 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -149,6 +149,7 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp * passed to it. * * @param array $config List of options for this endpoint + * @throws \Exception */ final public function __construct(array $config = []) { @@ -331,10 +332,15 @@ public function setConnection(Connection $connection): Endpoint /** * Returns the connection driver. * - * @return \Muffin\Webservice\Datasource\Connection|null + * @return \Muffin\Webservice\Datasource\Connection */ - public function getConnection(): ?Connection + public function getConnection(): Connection { + assert( + $this->_connection !== null, + 'Connection is null, there is no connection to return.' + ); + return $this->_connection; } @@ -371,7 +377,7 @@ public function setSchema(Schema|array $schema): Endpoint public function getSchema(): ?Schema { if ($this->_schema === null) { - $this->_schema = $this->getWebservice()?->describe($this->getName()); + $this->_schema = $this->getWebservice()->describe($this->getName()); } return $this->_schema; @@ -385,6 +391,7 @@ public function getSchema(): ?Schema * * @param string $field The field to check for. * @return bool True if the field exists, false if it does not. + * @throws \Exception */ public function hasField(string $field): bool { @@ -548,10 +555,8 @@ public function getInflectionMethod(): string public function setWebservice(string $alias, WebserviceInterface $webservice): Endpoint { $connection = $this->getConnection(); - if ($connection !== null) { - $connection->setWebservice($alias, $webservice); - $this->_webservice = $connection->getWebservice($alias); - } + $connection->setWebservice($alias, $webservice); + $this->_webservice = $connection->getWebservice($alias); return $this; } @@ -559,12 +564,14 @@ public function setWebservice(string $alias, WebserviceInterface $webservice): E /** * Get this endpoints associated webservice * - * @return \Muffin\Webservice\Webservice\WebserviceInterface|null + * @return \Muffin\Webservice\Webservice\WebserviceInterface + * @throws \Exception */ - public function getWebservice(): ?WebserviceInterface + public function getWebservice(): WebserviceInterface { + // If no webservice is found, get it from the connection if ($this->_webservice === null) { - $this->_webservice = $this->getConnection()?->getWebservice($this->getName()); + $this->_webservice = $this->getConnection()->getWebservice($this->getName()); } return $this->_webservice; @@ -582,6 +589,7 @@ public function getWebservice(): ?WebserviceInterface * @param string $type the type of query to perform * @param mixed ...$args Arguments that match up to finder-specific parameters * @return \Cake\Datasource\QueryInterface + * @throws \Exception */ public function find(string $type = 'all', mixed ...$args): QueryInterface { @@ -748,6 +756,7 @@ protected function _setFieldMatchers(array $options, array $keys): array * @param mixed ...$args Additional arguments for configuring things like caching. * @psalm-suppress InvalidReturnType For backwards compatibility. This function can also return array * @return \Cake\Datasource\EntityInterface + * @throws \Exception * @see \Cake\Datasource\RepositoryInterface::find() */ public function get( @@ -787,7 +796,7 @@ public function get( if ($cacheKey !== null) { $cacheKey = sprintf( 'get:%s.%s%s', - $this->getConnection()?->configName() ?? 'None', + $this->getConnection()->configName(), $this->getName(), json_encode($primaryKey) ); @@ -853,9 +862,6 @@ public function findOrCreate(mixed $search, ?callable $callback = null): EntityI public function query(): Query { $webservice = $this->getWebservice(); - if ($webservice === null) { - throw new Exception('Webservice not initialized, cannot create query'); - } return new Query($webservice, $this); } @@ -871,6 +877,7 @@ public function query(): Query * @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 + * @throws \Exception */ public function updateAll(Closure|array|string $fields, Closure|array|string|null $conditions): int { @@ -1319,11 +1326,7 @@ public function buildRules(RulesChecker $rules): RulesChecker */ public function __debugInfo(): array { - $connectionName = ''; - if ($this->getConnection() !== null) { - /** @psalm-suppress PossiblyNullReference getConnection cannot be null, as checked before entering this scope **/ - $connectionName = $this->getConnection()->configName(); - } + $connectionName = $this->getConnection()->configName(); return [ 'registryAlias' => $this->getRegistryAlias(), diff --git a/src/Model/EndpointLocator.php b/src/Model/EndpointLocator.php index 2d6d470..9ac8c92 100644 --- a/src/Model/EndpointLocator.php +++ b/src/Model/EndpointLocator.php @@ -36,12 +36,19 @@ public function set(string $alias, RepositoryInterface $repository): Endpoint * * @param string $alias The alias name you want to get. * @param array $options The options you want to build the endpoint with. - * @return \Cake\Datasource\RepositoryInterface + * @return \Muffin\Webservice\Model\Endpoint * @throws \RuntimeException If the registry alias is already in use. */ - public function get(string $alias, array $options = []): RepositoryInterface + public function get(string $alias, array $options = []): Endpoint { - return parent::get($alias, $options); + $parentRes = parent::get($alias, $options); + + assert( + $parentRes instanceof Endpoint, + 'The repository found is not of type Endpoint, but a different type implementing RepositoryInterface' + ); + + return $parentRes; } /** @@ -49,9 +56,9 @@ public function get(string $alias, array $options = []): RepositoryInterface * * @param string $alias Endpoint alias. * @param array $options The alias to check for. - * @return \Cake\Datasource\RepositoryInterface + * @return \Muffin\Webservice\Model\Endpoint */ - protected function createInstance(string $alias, array $options): RepositoryInterface + protected function createInstance(string $alias, array $options): Endpoint { [, $classAlias] = pluginSplit($alias); $options = ['alias' => $classAlias] + $options; diff --git a/src/Plugin.php b/src/WebservicePlugin.php similarity index 94% rename from src/Plugin.php rename to src/WebservicePlugin.php index 687ee86..4200825 100644 --- a/src/Plugin.php +++ b/src/WebservicePlugin.php @@ -8,7 +8,7 @@ use Cake\Datasource\FactoryLocator; use Muffin\Webservice\Model\EndpointLocator; -class Plugin extends BasePlugin +class WebservicePlugin extends BasePlugin { /** * Disable routes hook. diff --git a/tests/TestCase/AbstractDriverTest.php b/tests/TestCase/AbstractDriverTest.php index a0d7594..43ded93 100644 --- a/tests/TestCase/AbstractDriverTest.php +++ b/tests/TestCase/AbstractDriverTest.php @@ -7,7 +7,7 @@ use Cake\TestSuite\TestCase; use SomeVendor\SomePlugin\Webservice\Driver\SomePlugin; use StdClass; -use TestApp\Webservice\Driver\TestDriver; +use TestApp\Webservice\Driver\Test; use TestApp\Webservice\Logger; use TestApp\Webservice\TestWebservice; use TestPlugin\Webservice\Driver\TestPlugin; @@ -40,7 +40,7 @@ public function testSetClient() { $client = new StdClass(); - $driver = new TestDriver(); + $driver = new Test(); $driver->setClient($client); $this->assertSame($client, $driver->getClient()); @@ -48,7 +48,7 @@ public function testSetClient() public function testEnableQueryLogging() { - $driver = new TestDriver(); + $driver = new Test(); $driver->enableQueryLogging(); $this->assertTrue($driver->isQueryLoggingEnabled()); @@ -56,7 +56,7 @@ public function testEnableQueryLogging() public function testDisableQueryLogging() { - $driver = new TestDriver(); + $driver = new Test(); $driver->disableQueryLogging(); $this->assertFalse($driver->isQueryLoggingEnabled()); @@ -74,7 +74,7 @@ public function testDebugInfo() 'webservices' => ['example'], ]; - $driver = new TestDriver(); + $driver = new Test(); $driver->setLogger($logger); $driver ->setClient($client) diff --git a/tests/TestCase/Webservice/WebserviceTest.php b/tests/TestCase/Webservice/WebserviceTest.php index 8204580..4ab6c9b 100644 --- a/tests/TestCase/Webservice/WebserviceTest.php +++ b/tests/TestCase/Webservice/WebserviceTest.php @@ -9,7 +9,7 @@ use Muffin\Webservice\Model\Exception\MissingEndpointSchemaException; use Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException; use Muffin\Webservice\Webservice\Webservice; -use TestApp\Webservice\Driver\TestDriver; +use TestApp\Webservice\Driver\Test; use TestApp\Webservice\TestWebservice; class WebserviceTest extends TestCase @@ -27,7 +27,7 @@ public function setUp(): void parent::setUp(); $this->webservice = new TestWebservice([ - 'driver' => new TestDriver([]), + 'driver' => new Test([]), ]); } @@ -43,7 +43,7 @@ public function tearDown(): void public function testConstructor() { - $testDriver = new TestDriver([]); + $testDriver = new Test([]); $webservice = new TestWebservice([ 'driver' => $testDriver, diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 430d3d3..1e2a3fa 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\TestDriver; +use TestApp\Webservice\Driver\Test; require_once 'vendor/autoload.php'; @@ -88,7 +88,7 @@ ConnectionManager::setConfig('test', [ 'className' => Connection::class, - 'driver' => TestDriver::class, + 'driver' => Test::class, ] + ConnectionManager::parseDsn(getenv('DB_DSN'))); Log::setConfig([ diff --git a/tests/test_app/src/Webservice/Driver/TestDriver.php b/tests/test_app/src/Webservice/Driver/Test.php similarity index 93% rename from tests/test_app/src/Webservice/Driver/TestDriver.php rename to tests/test_app/src/Webservice/Driver/Test.php index 5699610..f451e6f 100644 --- a/tests/test_app/src/Webservice/Driver/TestDriver.php +++ b/tests/test_app/src/Webservice/Driver/Test.php @@ -7,7 +7,7 @@ use Muffin\Webservice\Webservice\WebserviceInterface; use TestApp\Webservice\EndpointTestWebservice; -class TestDriver extends AbstractDriver +class Test extends AbstractDriver { /** * Initialize is used to easily extend the constructor. From c957a7c367b2bd5ae0bad78597b1ab7d045ee569 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Thu, 4 Apr 2024 16:08:42 +0200 Subject: [PATCH 19/21] Pipeline fixed for PHP 8.1 target --- tests/test_app/src/Model/Endpoint/TestEndpoint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app/src/Model/Endpoint/TestEndpoint.php b/tests/test_app/src/Model/Endpoint/TestEndpoint.php index eb800f2..2822160 100644 --- a/tests/test_app/src/Model/Endpoint/TestEndpoint.php +++ b/tests/test_app/src/Model/Endpoint/TestEndpoint.php @@ -32,7 +32,7 @@ public function validationDefault(Validator $validator): Validator * * @return true */ - public function findExamples(): true + public function findExamples(): bool { return true; } From 3c72226f8ecadc82b626d96ac361d6719d440802 Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Wed, 17 Apr 2024 12:57:13 +0200 Subject: [PATCH 20/21] Updated Connection and Endpoint to not have final constructors, added psalm constraint. Added cache files and diff file for phpcs to gitignore. --- .gitignore | 2 ++ src/Model/Endpoint.php | 3 ++- src/Model/Resource.php | 5 ++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a072370..6f0931b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /vendor .phpunit.result.cache /.idea +*.diff +*.cache diff --git a/src/Model/Endpoint.php b/src/Model/Endpoint.php index 94841c5..ca7fc2c 100644 --- a/src/Model/Endpoint.php +++ b/src/Model/Endpoint.php @@ -39,6 +39,7 @@ * @package Muffin\Webservice\Model * @template TSubject of object * @implements \Cake\Event\EventDispatcherInterface + * @psalm-consistent-constructor */ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDispatcherInterface { @@ -151,7 +152,7 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp * @param array $config List of options for this endpoint * @throws \Exception */ - final public function __construct(array $config = []) + public function __construct(array $config = []) { if (!empty($config['alias'])) { $this->setAlias($config['alias']); diff --git a/src/Model/Resource.php b/src/Model/Resource.php index 1c11b9c..cc2fec5 100644 --- a/src/Model/Resource.php +++ b/src/Model/Resource.php @@ -7,6 +7,9 @@ use Cake\Datasource\EntityTrait; use Cake\Datasource\InvalidPropertyInterface; +/** + * @psalm-consistent-constructor + */ class Resource implements EntityInterface, InvalidPropertyInterface { use EntityTrait; @@ -30,7 +33,7 @@ class Resource implements EntityInterface, InvalidPropertyInterface * @param array $properties hash of properties to set in this resource * @param array $options list of options to use when creating this resource */ - final public function __construct(array $properties = [], array $options = []) + public function __construct(array $properties = [], array $options = []) { $options += [ 'useSetters' => true, From 8a7b692a1a622b689dc1d3ca0603e71d42da5d8a Mon Sep 17 00:00:00 2001 From: Justin Ruiter Date: Wed, 1 May 2024 07:38:38 +0200 Subject: [PATCH 21/21] Small fixes and removes unneccessary methods --- src/Datasource/Query.php | 118 +-------------------------------------- 1 file changed, 3 insertions(+), 115 deletions(-) diff --git a/src/Datasource/Query.php b/src/Datasource/Query.php index 88918a5..6260663 100644 --- a/src/Datasource/Query.php +++ b/src/Datasource/Query.php @@ -216,7 +216,8 @@ public function all(): ResultSetInterface $res = $this->_execute(); if (!is_iterable($res)) { - return new ResultSet([], 0); + $this->_results = new ResultSet([], 0); + return $this->_results; } $results = $this->decorateResults($res); @@ -236,121 +237,11 @@ public function all(): ResultSetInterface */ public function orderBy(Closure|array|string $fields, bool $overwrite = false): Query { - if ($overwrite) { - $this->_parts['order'] = null; - } - - if (is_array($fields) && empty($fields)) { - return $this; - } - - $this->_parts['order'] ??= new OrderByExpression(); - $this->_conjugate('order', $fields, '', []); + $this->order($fields, $overwrite); return $this; } - /** - * Helper function used to build conditions by composing QueryExpression objects. - * - * @param string $part Name of the query part to append the new part to - * @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $append Expression or builder function to append. - * to append. - * @param string $conjunction type of conjunction to be used to operate part - * @param array $types Associative array of type names used to bind values to query - * @return void - */ - protected function _conjugate( - string $part, - ExpressionInterface|Closure|array|string|null $append, - string $conjunction, - array $types - ): void { - /** @var \Cake\Database\Expression\QueryExpression $expression */ - $expression = $this->_parts[$part] ?: $this->newExpr(); - if ((is_array($append) && empty($append)) || $append === null) { - $this->_parts[$part] = $expression; - - return; - } - - if ($append instanceof Closure) { - $append = $append($this->newExpr(), $this); - } - - if ($expression->getConjunction() === $conjunction) { - $expression->add($append, $types); - } else { - $expression = $this->newExpr() - ->setConjunction($conjunction) - ->add([$expression, $append], $types); - } - - $this->_parts[$part] = $expression; - $this->_dirty(); - } - - /** - * Marks a query as dirty, removing any preprocessed information - * from in memory caching. - * - * @return void - */ - protected function _dirty(): void - { - $this->_dirty = true; - } - - /** - * Returns a new QueryExpression object. This is a handy function when - * building complex queries using a fluent interface. You can also override - * this function in subclasses to use a more specialized QueryExpression class - * if required. - * - * You can optionally pass a single raw SQL string or an array or expressions in - * any format accepted by \Cake\Database\Expression\QueryExpression: - * - * ``` - * $expression = $query->expr(); // Returns an empty expression object - * $expression = $query->expr('Table.column = Table2.column'); // Return a raw SQL expression - * ``` - * - * @param \Cake\Database\ExpressionInterface|array|string|null $rawExpression A string, array or anything you want wrapped in an expression object - * @return \Cake\Database\Expression\QueryExpression - */ - public function newExpr(ExpressionInterface|array|string|null $rawExpression = null): QueryExpression - { - return $this->expr($rawExpression); - } - - /** - * Returns a new QueryExpression object. This is a handy function when - * building complex queries using a fluent interface. You can also override - * this function in subclasses to use a more specialized QueryExpression class - * if required. - * - * You can optionally pass a single raw SQL string or an array or expressions in - * any format accepted by \Cake\Database\Expression\QueryExpression: - * - * ``` - * $expression = $query->expr(); // Returns an empty expression object - * $expression = $query->expr('Table.column = Table2.column'); // Return a raw SQL expression - * ``` - * - * @param \Cake\Database\ExpressionInterface|array|string|null $rawExpression A string, array or anything you want wrapped in an expression object - * @return \Cake\Database\Expression\QueryExpression - */ - public function expr(ExpressionInterface|array|string|null $rawExpression = null): QueryExpression - { - $expression = new QueryExpression([], $this->getTypeMap()); - - if ($rawExpression !== null) { - $expression->add($rawExpression); - } - - return $expression; - } - /** * Returns the existing type map. * @@ -581,9 +472,6 @@ public function where( array $types = [], bool $overwrite = false ): Query { - if ($overwrite) { - $this->_parts['where'] = $conditions; - } $this->_parts['where'] = !$overwrite ? Hash::merge($this->clause('where'), $conditions) : $conditions; return $this;