diff --git a/.editorconfig b/.editorconfig index a84f86c..41e9eef 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,4 +15,7 @@ end_of_line = crlf [*.yml] indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 + +[*.neon] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index 17493ec..b909522 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,7 +2,8 @@ .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore -.semver export-ignore -phpunit.xml.dist export-ignore .travis.yml export-ignore tests export-ignore +phpstan.neon export-ignore +psalm.xml export-ignore +.github export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6153d71 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - '*' + +jobs: + testsuite: + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: + php-version: ['7.2', '7.4', '8.0'] + db-type: ['mysql', 'pgsql'] + prefer-lowest: [''] + include: + - php-version: '7.2' + db-type: 'sqlite' + prefer-lowest: 'prefer-lowest' + + services: + postgres: + image: postgres + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: postgres + + steps: + - uses: actions/checkout@v2 + + - name: Setup Service + if: matrix.db-type == 'mysql' + run: | + sudo service mysql start + mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE cakephp;' + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, intl, pdo_${{ matrix.db-type }} + coverage: pcov + + - name: Composer install + run: | + composer --version + if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then + composer update --prefer-lowest --prefer-stable + else + composer install + fi + + - name: Run PHPUnit + run: | + if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi + 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 + 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' + uses: codecov/codecov-action@v1 + + cs-stan: + name: Coding Standard & Static Analysis + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + extensions: mbstring, intl + coverage: none + tools: psalm:4.7, phpstan:0.12 + + - name: Composer Install + run: composer require cakephp/cakephp-codesniffer:^4.2 + + - name: Run phpcs + run: vendor/bin/phpcs --standard=CakePHP src/ tests/ + + - name: Run psalm + if: success() || failure() + run: psalm --output-format=github + + - name: Run phpstan + if: success() || failure() + run: phpstan analyse diff --git a/.gitignore b/.gitignore index 364f9bd..654564b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /composer.lock /plugins /vendor +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6788a9c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,58 +0,0 @@ -language: php - -php: - - 5.6 - - 7.4 - -services: - - postgresql - - mysql - -env: - global: - - DEFAULT=1 - matrix: - - DB=mysql DB_DSN='mysql://root@127.0.0.1/cakephp_test?init[]=SET sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"' - - DB=pgsql DB_DSN='postgres://postgres@127.0.0.1/cakephp_test' - - DB=sqlite DB_DSN='sqlite:///:memory:' - -matrix: - fast_finish: true - - include: - - php: 7.4 - env: PHPCS=1 DEFAULT=0 - - - php: 7.1 - env: PHPSTAN=1 DEFAULT=0 - - - php: 5.6 - env: PREFER_LOWEST=1 - -before_install: - - if [[ $DB == 'mysql' ]]; then mysql -u root -e 'CREATE DATABASE cakephp_test;'; fi - - if [[ $DB == 'pgsql' ]]; then psql -c 'CREATE DATABASE cakephp_test;' -U postgres; fi - -install: - - if [[ $TRAVIS_PHP_VERSION != 7.4 ]]; then phpenv config-rm xdebug.ini; fi - - - if [[ $PHPSTAN != 1 ]]; then composer install --no-interaction; fi - - | - if [[ $PHPSTAN == 1 ]]; then - composer require phpstan/phpstan:^0.12 - composer install --no-dev - fi - -script: - - if [[ $TRAVIS_PHP_VERSION == 7.4 ]]; then ./vendor/bin/phpunit --coverage-clover=clover.xml; fi - - if [[ $DEFAULT == 1 ]]; then ./vendor/bin/phpunit; fi - - - if [[ $PHPCS == 1 ]]; then ./vendor/bin/phpcs -p --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests; fi - - - if [[ $PHPSTAN == 1 ]]; then vendor/bin/phpstan analyse src; fi - -after_success: -- if [[ $TRAVIS_PHP_VERSION == 7.4 ]]; then bash <(curl -s https://codecov.io/bash); fi - -notifications: - email: false diff --git a/README.md b/README.md index 508182a..43cb8c3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Webservice -[![Build Status](https://img.shields.io/travis/UseMuffin/Webservice/master.svg?style=flat-square)](https://travis-ci.org/UseMuffin/Webservice) +[![Build Status](https://img.shields.io/travis/UseMuffin/Webservice/master.svg?style=flat-square)](https://github.com/UseMuffin/Webservice/actions?query=workflow%3ACI+branch%3Amaster) [![Coverage](https://img.shields.io/codecov/c/github/UseMuffin/Webservice/master.svg?style=flat-square)](https://codecov.io/github/UseMuffin/Webservice) [![Total Downloads](https://img.shields.io/packagist/dt/muffin/webservice.svg?style=flat-square)](https://packagist.org/packages/muffin/webservice) [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) @@ -23,6 +23,24 @@ bin/cake plugin load Muffin/Webservice ## Usage +### Datasource Configuration + +In your `app.php`, add a new `webservice` config under `Datasources`: + +```php + 'Datasources' => [ + // Other db config here + 'webservice' => [ + 'className' => \Muffin\Webservice\Connection::class, + 'service' => 'Articles', + // Any additional keys will be set as Driver's config. + ], + ], +``` + +If you are making a plugin then conventionally the datasource config key name +should be underscored version of plugin name. + ### As an ORM #### Create driver @@ -31,15 +49,15 @@ bin/cake plugin load Muffin/Webservice setClient(new Client([ 'host' => 'example.com' @@ -54,8 +72,8 @@ class Articles extends AbstractDriver + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Connection.php b/src/Connection.php deleted file mode 100644 index 601597b..0000000 --- a/src/Connection.php +++ /dev/null @@ -1,78 +0,0 @@ -_normalizeConfig($config); - $driver = $config['driver']; - unset($config['driver'], $config['service']); - - $this->_driver = new $driver($config); - - if (!($this->_driver instanceof AbstractDriver)) { - throw new UnexpectedDriverException(['driver' => $driver]); - } - } - - /** - * Validates certain custom configuration values. - * - * @param array $config Raw custom configuration. - * @return array - * @throws \Muffin\Webservice\Exception\MissingConnectionException If the connection does not exist. - * @throws \Muffin\Webservice\Exception\MissingDriverException If the driver does not exist. - */ - protected function _normalizeConfig($config) - { - if (empty($config['driver'])) { - if (empty($config['service'])) { - throw new MissingConnectionException(['name' => $config['name']]); - } - - if (!$config['driver'] = App::className($config['service'], 'Webservice/Driver')) { - throw new MissingDriverException(['driver' => $config['driver']]); - } - } - - return $config; - } - - /** - * Proxies the driver's methods. - * - * @param string $method Method name. - * @param array $args Arguments to pass-through - * @return mixed - */ - public function __call($method, $args) - { - return call_user_func_array([$this->_driver, $method], $args); - } -} diff --git a/src/Datasource/Connection.php b/src/Datasource/Connection.php new file mode 100644 index 0000000..f31bc3c --- /dev/null +++ b/src/Datasource/Connection.php @@ -0,0 +1,84 @@ +_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]); + } + } + + /** + * Validates certain custom configuration values. + * + * @param array $config Raw custom configuration. + * @return array + * @throws \Muffin\Webservice\Datasource\Exception\MissingConnectionException If the connection does not exist. + * @throws \Muffin\Webservice\Webservice\Exception\MissingDriverException If the driver does not exist. + */ + protected function _normalizeConfig(array $config): array + { + if (empty($config['driver'])) { + if (empty($config['service'])) { + throw new MissingConnectionException(['name' => $config['name']]); + } + + $config['driver'] = App::className($config['service'], 'Webservice/Driver'); + if (!$config['driver']) { + throw new MissingDriverException(['driver' => $config['driver']]); + } + } + + return $config; + } + + /** + * Proxies the driver's methods. + * + * @param string $method Method name. + * @param array $args Arguments to pass-through + * @return mixed + */ + public function __call($method, $args) + { + return call_user_func_array([$this->_driver, $method], $args); + } +} diff --git a/src/Datasource/Exception/MissingConnectionException.php b/src/Datasource/Exception/MissingConnectionException.php new file mode 100644 index 0000000..ace3e6e --- /dev/null +++ b/src/Datasource/Exception/MissingConnectionException.php @@ -0,0 +1,16 @@ +_prepareDataAndOptions($data, $options); + [$data, $options] = $this->_prepareDataAndOptions($data, $options); $primaryKey = (array)$this->_endpoint->getPrimaryKey(); + /** @psalm-var class-string<\Muffin\Webservice\Model\Resource> */ $resourceClass = $this->_endpoint->getResourceClass(); - /* @var \Muffin\Webservice\Model\Resource $entity */ $entity = new $resourceClass(); $entity->setSource($this->_endpoint->getRegistryAlias()); @@ -104,7 +105,7 @@ public function one(array $data, array $options = []) * @return array The list of validation errors. * @throws \RuntimeException If no validator can be created. */ - protected function _validate($data, $options, $isNew) + protected function _validate(array $data, array $options, bool $isNew): array { if (!$options['validate']) { return []; @@ -116,6 +117,7 @@ protected function _validate($data, $options, $isNew) } elseif (is_string($options['validate'])) { $validator = $this->_endpoint->getValidator($options['validate']); } else { + /** @var \Cake\Validation\Validator $validator */ $validator = $options['validator']; } @@ -126,7 +128,7 @@ protected function _validate($data, $options, $isNew) )); } - return $validator->errors($data, $isNew); + return $validator->validate($data, $isNew); } /** @@ -136,7 +138,7 @@ protected function _validate($data, $options, $isNew) * @param array $options The options passed to this marshaller. * @return array An array containing prepared data and options. */ - protected function _prepareDataAndOptions($data, $options) + protected function _prepareDataAndOptions(array $data, array $options): array { $options += ['validate' => true]; @@ -163,10 +165,10 @@ protected function _prepareDataAndOptions($data, $options) * * @param array $data The data to hydrate. * @param array $options List of options - * @return array An array of hydrated records. + * @return \Cake\Datasource\EntityInterface[] An array of hydrated records. * @see \Muffin\Webservice\Model\Endpoint::newEntities() */ - public function many(array $data, array $options = []) + public function many(array $data, array $options = []): array { $output = []; foreach ($data as $record) { @@ -196,9 +198,9 @@ public function many(array $data, array $options = []) * @param array $options List of options. * @return \Cake\Datasource\EntityInterface */ - public function merge(EntityInterface $entity, array $data, array $options = []) + public function merge(EntityInterface $entity, array $data, array $options = []): EntityInterface { - list($data, $options) = $this->_prepareDataAndOptions($data, $options); + [$data, $options] = $this->_prepareDataAndOptions($data, $options); $isNew = $entity->isNew(); $keys = []; @@ -262,9 +264,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 array + * @return \Cake\Datasource\EntityInterface[] */ - public function mergeMany($entities, array $data, array $options = []) + public function mergeMany($entities, array $data, array $options = []): array { $primary = (array)$this->_endpoint->getPrimaryKey(); @@ -272,7 +274,7 @@ public function mergeMany($entities, array $data, array $options = []) ->groupBy(function ($el) use ($primary) { $keys = []; foreach ($primary as $key) { - $keys[] = isset($el[$key]) ? $el[$key] : ''; + $keys[] = $el[$key] ?? ''; } return implode(';', $keys); @@ -282,7 +284,9 @@ public function mergeMany($entities, array $data, array $options = []) }) ->toArray(); - $new = isset($indexed[null]) ? $indexed[null] : []; + /** @psalm-suppress NullArrayOffset, InvalidArrayOffset */ + $new = $indexed[null] ?? []; + /** @psalm-suppress PossiblyNullArrayOffset, InvalidArrayOffset */ unset($indexed[null]); $output = []; @@ -292,7 +296,7 @@ public function mergeMany($entities, array $data, array $options = []) } $key = implode(';', $entity->extract($primary)); - if ($key === null || !isset($indexed[$key])) { + if ($key === '' || !isset($indexed[$key])) { continue; } diff --git a/src/Query.php b/src/Datasource/Query.php similarity index 71% rename from src/Query.php rename to src/Datasource/Query.php index 3d00505..d8a8814 100644 --- a/src/Query.php +++ b/src/Datasource/Query.php @@ -1,12 +1,15 @@ webservice($webservice); - $this->endpoint($endpoint); + $this->setWebservice($webservice); + $this->setEndpoint($endpoint); } /** @@ -155,7 +158,7 @@ public function delete() * @param string $name name of the clause to be returned * @return mixed */ - public function clause($name) + public function clause(string $name) { if (isset($this->_parts[$name])) { return $this->_parts[$name]; @@ -167,37 +170,52 @@ public function clause($name) /** * Set the endpoint to be used * - * @param \Muffin\Webservice\Model\Endpoint|null $endpoint The endpoint to use - * @return \Muffin\Webservice\Model\Endpoint|$this + * @param \Muffin\Webservice\Model\Endpoint $endpoint The endpoint to use + * @return $this + * @psalm-suppress LessSpecificReturnStatement */ - public function endpoint(Endpoint $endpoint = null) + public function setEndpoint(Endpoint $endpoint) { - if ($endpoint === null) { - return $this->getRepository(); - } - $this->repository($endpoint); return $this; } /** - * Set the webservice to be used + * Set the endpoint to be used * - * @param null|\Muffin\Webservice\Webservice\WebserviceInterface $webservice The webservice to use - * @return \Muffin\Webservice\Webservice\WebserviceInterface|self + * @return \Muffin\Webservice\Model\Endpoint + * @psalm-suppress MoreSpecificReturnType */ - public function webservice(WebserviceInterface $webservice = null) + public function getEndpoint(): Endpoint { - if ($webservice === null) { - return $this->_webservice; - } + /** @var \Muffin\Webservice\Model\Endpoint */ + return $this->getRepository(); + } + /** + * Set the webservice to be used + * + * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice The webservice to use + * @return $this + */ + public function setWebservice(WebserviceInterface $webservice) + { $this->_webservice = $webservice; return $this; } + /** + * Get the webservice used + * + * @return \Muffin\Webservice\Webservice\WebserviceInterface + */ + public function getWebservice() + { + return $this->_webservice; + } + /** * Apply custom finds to against an existing query object. * @@ -216,6 +234,7 @@ public function webservice(WebserviceInterface $webservice = null) */ public function find($finder, array $options = []) { + /** @psalm-suppress UndefinedInterfaceMethod */ return $this->getRepository()->callFinder($finder, $this, $options); } @@ -231,6 +250,7 @@ public function firstOrFail() if ($entity) { return $entity; } + /** @psalm-suppress UndefinedInterfaceMethod */ throw new RecordNotFoundException(sprintf( 'Record not found in endpoint "%s"', $this->getRepository()->getName() @@ -241,10 +261,10 @@ public function firstOrFail() * Alias a field with the endpoint's current alias. * * @param string $field The field to alias. - * @param null $alias Not being used + * @param string|null $alias Not being used * @return array The field prefixed with the endpoint alias. */ - public function aliasField($field, $alias = null) + public function aliasField(string $field, ?string $alias = null): array { return [$field => $field]; } @@ -253,17 +273,18 @@ public function aliasField($field, $alias = null) * Apply conditions to the query * * @param array|null $conditions The conditions to apply - * @param array|null $types Not used + * @param array $types Not used * @param bool $overwrite Whether to overwrite the current conditions - * @return $this|array + * @return $this + * @psalm-suppress MoreSpecificImplementedParamType */ - public function where($conditions = null, $types = [], $overwrite = false) + public function where($conditions = null, array $types = [], bool $overwrite = false) { if ($conditions === null) { return $this->clause('where'); } - $this->_parts['where'] = (!$overwrite) ? Hash::merge($this->clause('where'), $conditions) : $conditions; + $this->_parts['where'] = !$overwrite ? Hash::merge($this->clause('where'), $conditions) : $conditions; return $this; } @@ -271,13 +292,14 @@ public function where($conditions = null, $types = [], $overwrite = false) /** * Add AND conditions to the query * - * @param string|array|\Cake\Database\ExpressionInterface|callable $conditions The conditions to add with AND. + * @param string|array $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 - * @return $this + * @psalm-suppress PossiblyInvalidArgument */ - public function andWhere($conditions, $types = []) + public function andWhere($conditions, array $types = []) { $this->where($conditions, $types); @@ -287,15 +309,11 @@ public function andWhere($conditions, $types = []) /** * Charge this query's action * - * @param int|null $action Action to use - * @return $this|int + * @param int $action Action to use + * @return $this */ - public function action($action = null) + public function action(int $action) { - if ($action === null) { - return $this->clause('action'); - } - $this->_parts['action'] = $action; return $this; @@ -310,21 +328,22 @@ public function action($action = null) * * Pages should start at 1. * - * @param int $page The page number you want. + * @param int $num The page number you want. * @param int $limit The number of rows you want in the page. If null * the current limit clause will be used. * @return $this */ - public function page($page = null, $limit = null) + public function page(int $num, ?int $limit = null) { - if ($page === null) { - return $this->clause('page'); + if ($num < 1) { + throw new InvalidArgumentException('Pages must start at 1.'); } + if ($limit !== null) { $this->limit($limit); } - $this->_parts['page'] = $page; + $this->_parts['page'] = $num; return $this; } @@ -343,13 +362,11 @@ public function page($page = null, $limit = null) * * @param int $limit number of records to be returned * @return $this + * @psalm-suppress MoreSpecificImplementedParamType + * @psalm-suppress ParamNameMismatch */ - public function limit($limit = null) + public function limit($limit) { - if ($limit === null) { - return $this->clause('limit'); - } - $this->_parts['limit'] = $limit; return $this; @@ -367,7 +384,7 @@ public function set($fields = null) return $this->clause('set'); } - if (!in_array($this->action(), [self::ACTION_CREATE, self::ACTION_UPDATE])) { + 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')); } @@ -377,7 +394,7 @@ public function set($fields = null) } /** - * {@inheritDoc} + * @inheritDoc */ public function offset($num) { @@ -399,13 +416,13 @@ public function offset($num) * By default this function will append any passed argument to the list of fields * to be selected, unless the second argument is set to true. * - * @param array|string $fields fields to be added to the list + * @param array|\Cake\Database\ExpressionInterface|\Closure|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) { - $this->_parts['order'] = (!$overwrite) ? Hash::merge($this->clause('order'), $fields) : $fields; + $this->_parts['order'] = !$overwrite ? Hash::merge($this->clause('order'), $fields) : $fields; return $this; } @@ -450,18 +467,19 @@ public function applyOptions(array $options) * * @return int */ - public function count() + public function count(): int { - if ($this->action() !== self::ACTION_READ) { + if ($this->clause('action') !== self::ACTION_READ) { return 0; } - if (!$this->__resultSet) { + if (!$this->_result) { $this->_execute(); } - if ($this->__resultSet) { - return $this->__resultSet->total(); + if ($this->_result) { + /** @psalm-suppress PossiblyInvalidMethodCall, PossiblyUndefinedMethod */ + return (int)$this->_result->total(); } return 0; @@ -477,11 +495,11 @@ public function count() * $singleUser = $query->first(); * ``` * - * @return mixed the first result from the ResultSet + * @return \Cake\Datasource\EntityInterface|array|null the first result from the ResultSet */ public function first() { - if (!$this->__resultSet) { + if (!$this->_result) { $this->limit(1); } @@ -497,7 +515,8 @@ public function first() */ public function triggerBeforeFind() { - if (!$this->_beforeFindFired && $this->action() === self::ACTION_READ) { + if (!$this->_beforeFindFired && $this->clause('action') === self::ACTION_READ) { + /** @var \Muffin\Webservice\Model\Endpoint $endpoint */ $endpoint = $this->getRepository(); $this->_beforeFindFired = true; $endpoint->dispatchEvent('Model.beforeFind', [ @@ -511,28 +530,36 @@ public function triggerBeforeFind() /** * Execute the query * - * @return \Traversable + * @return bool|int|\Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet + * @psalm-suppress MoreSpecificReturnType */ public function execute() { - return $this->_execute(); + if ($this->clause('action') === self::ACTION_READ) { + /** @psalm-suppress LessSpecificReturnStatement */ + return $this->_execute(); + } + + return $this->_result = $this->_webservice->execute($this); } /** * Executes this query and returns a traversable object containing the results * - * @return \Traversable + * @return \Cake\Datasource\ResultSetInterface */ - protected function _execute() + protected function _execute(): ResultSetInterface { $this->triggerBeforeFind(); - if ($this->__resultSet) { + if ($this->_result) { + /** @psalm-var class-string<\Cake\Datasource\ResultSetInterface> $decorator */ $decorator = $this->_decoratorClass(); - return new $decorator($this->__resultSet); + return new $decorator($this->_result); } - return $this->__resultSet = $this->_webservice->execute($this); + /** @var \Cake\Datasource\ResultSetInterface */ + return $this->_result = $this->_webservice->execute($this); } /** @@ -544,17 +571,17 @@ public function __debugInfo() { return [ '(help)' => 'This is a Query object, to get the results execute or iterate it.', - 'action' => $this->action(), + 'action' => $this->clause('action'), 'formatters' => $this->_formatters, 'offset' => $this->clause('offset'), - 'page' => $this->page(), - 'limit' => $this->limit(), + 'page' => $this->clause('page'), + 'limit' => $this->clause('limit'), 'set' => $this->set(), 'sort' => $this->clause('order'), 'extraOptions' => $this->getOptions(), 'conditions' => $this->where(), - 'repository' => $this->endpoint(), - 'webservice' => $this->webservice(), + 'repository' => $this->getEndpoint(), + 'webservice' => $this->getWebservice(), ]; } @@ -577,8 +604,9 @@ public function jsonSerialize() * @param bool $overwrite whether to reset fields with passed list or not * @return $this * @see \Cake\Database\Query::select + * @psalm-suppress MoreSpecificImplementedParamType */ - public function select($fields = [], $overwrite = false) + public function select($fields = [], bool $overwrite = false) { if (!is_string($fields) && is_callable($fields)) { $fields = $fields($this); diff --git a/src/ResultSet.php b/src/Datasource/ResultSet.php similarity index 93% rename from src/ResultSet.php rename to src/Datasource/ResultSet.php index 021bc5d..124cf1b 100644 --- a/src/ResultSet.php +++ b/src/Datasource/ResultSet.php @@ -1,6 +1,7 @@ _results = array_values($resources); $this->_total = $total; @@ -152,7 +153,7 @@ public function unserialize($serialized) * * @return int */ - public function count() + public function count(): int { return count($this->_results); } @@ -162,7 +163,7 @@ public function count() * * @return int|null */ - public function total() + public function total(): ?int { return $this->_total; } diff --git a/src/Schema.php b/src/Datasource/Schema.php similarity index 80% rename from src/Schema.php rename to src/Datasource/Schema.php index 8499d47..062f928 100644 --- a/src/Schema.php +++ b/src/Datasource/Schema.php @@ -1,8 +1,10 @@ _repository = $endpoint; foreach ($columns as $field => $definition) { @@ -124,7 +125,7 @@ public function __construct($endpoint, array $columns = []) * * @return string */ - public function name() + public function name(): string { return $this->_repository; } @@ -158,7 +159,7 @@ public function name() * @param array|string $attrs The attributes for the column. * @return $this */ - public function addColumn($name, $attrs) + public function addColumn(string $name, $attrs) { if (is_string($attrs)) { $attrs = ['type' => $attrs]; @@ -177,9 +178,9 @@ public function addColumn($name, $attrs) /** * Get the column names in the endpoint. * - * @return array + * @return string[] */ - public function columns() + public function columns(): array { return array_keys($this->_columns); } @@ -190,7 +191,7 @@ public function columns() * @param string $name The column name. * @return array|null Column data or null. */ - public function column($name) + public function getColumn(string $name): ?array { if (!isset($this->_columns[$name])) { return null; @@ -202,24 +203,29 @@ public function column($name) } /** - * Sets the type of a column, or returns its current type - * if none is passed. + * Check if schema has column * - * @param string $name The column to get the type of. - * @param string $type The type to set the column to. - * @return string|null Either the column type or null. - * @deprecated 2.0.0 Please use setColumnType or getColumnType instead. + * @param string $name The column name. + * @return bool */ - public function columnType($name, $type = null) + public function hasColumn(string $name): bool { - if (!isset($this->_columns[$name])) { - return null; - } - if ($type !== null) { - $this->setColumnType($name, $type); - } + return isset($this->_columns[$name]); + } - return $this->getColumnType($name); + /** + * Remove a column from the table schema. + * + * If the column is not defined in the table, no error will be raised. + * + * @param string $name The name of the column + * @return $this + */ + public function removeColumn(string $name) + { + unset($this->_columns[$name], $this->_typeMap[$name]); + + return $this; } /** @@ -229,7 +235,7 @@ public function columnType($name, $type = null) * @param string $type Type to set for the column * @return $this */ - public function setColumnType($name, $type) + public function setColumnType(string $name, string $type) { $this->_columns[$name]['type'] = $type; $this->_typeMap[$name] = $type; @@ -243,7 +249,7 @@ public function setColumnType($name, $type) * @param string $name Column name * @return null|string */ - public function getColumnType($name) + public function getColumnType(string $name): ?string { if (!isset($this->_columns[$name])) { return null; @@ -260,7 +266,7 @@ public function getColumnType($name) * @param string $column The column name to get the base type from * @return string|null The base type name */ - public function baseColumnType($column) + public function baseColumnType(string $column): ?string { if (isset($this->_columns[$column]['baseType'])) { return $this->_columns[$column]['baseType']; @@ -272,8 +278,8 @@ public function baseColumnType($column) return null; } - if (Type::getMap($type)) { - $type = Type::build($type)->getBaseType(); + if (TypeFactory::getMap($type)) { + $type = TypeFactory::build($type)->getBaseType(); } return $this->_columns[$column]['baseType'] = $type; @@ -285,7 +291,7 @@ public function baseColumnType($column) * * @return array */ - public function typeMap() + public function typeMap(): array { return $this->_typeMap; } @@ -298,13 +304,13 @@ public function typeMap() * @param string $name The column to get the type of. * @return bool Whether or not the field is nullable. */ - public function isNullable($name) + public function isNullable(string $name): bool { if (!isset($this->_columns[$name])) { return true; } - return ($this->_columns[$name]['null'] === true); + return $this->_columns[$name]['null'] === true; } /** @@ -312,7 +318,7 @@ public function isNullable($name) * * @return array */ - public function defaultValues() + public function defaultValues(): array { $defaults = []; foreach ($this->_columns as $name => $data) { @@ -334,7 +340,7 @@ public function defaultValues() * @return array Column name(s) for the primary key. An * empty list will be returned when the endpoint has no primary key. */ - public function primaryKey() + public function getPrimaryKey(): array { $primaryKeys = []; foreach ($this->_columns as $name => $data) { @@ -348,26 +354,6 @@ public function primaryKey() return $primaryKeys; } - /** - * Get/set the options for a endpoint. - * - * Endpoint options allow you to set platform specific endpoint level options. - * - * @param array|null $options The options to set, or null to read options. - * @return $this|array Either the endpoint instance, or an array of options when reading. - * @deprecated 2.0.0 Please use setOptions and getOptions instead. - */ - public function options($options = null) - { - if ($options === null) { - return $this->getOptions(); - } - - $this->setOptions($options); - - return $this; - } - /** * Set the schema options for an endpoint * @@ -386,7 +372,7 @@ public function setOptions(array $options) * * @return array */ - public function getOptions() + public function getOptions(): array { return $this->_options; } diff --git a/src/Driver/empty b/src/Driver/empty deleted file mode 100644 index e69de29..0000000 diff --git a/src/Exception/MissingConnectionException.php b/src/Exception/MissingConnectionException.php deleted file mode 100644 index 2a3bd32..0000000 --- a/src/Exception/MissingConnectionException.php +++ /dev/null @@ -1,15 +0,0 @@ - */ protected $_resourceClass; @@ -84,14 +88,14 @@ class Endpoint implements RepositoryInterface, EventListenerInterface, EventDisp /** * The name of the field that represents the primary key in the endpoint * - * @var string|array + * @var string|array|null */ protected $_primaryKey; /** * The name of the field that represents a human readable representation of a row * - * @var string + * @var string|string[] */ protected $_displayField; @@ -181,15 +185,18 @@ public function __construct(array $config = []) * instance is created through the EndpointRegistry without a connection. * * @return string - * * @see \Muffin\Webservice\Model\EndpointRegistry::get() */ - public static function defaultConnectionName() + public static function defaultConnectionName(): string { - $namespaceParts = explode('\\', get_called_class()); - $plugin = array_slice(array_reverse($namespaceParts), 3, 2); + $namespaceParts = explode('\\', static::class); + $plugin = current(array_slice(array_reverse($namespaceParts), 3, 2)); - return Inflector::underscore(current($plugin)); + if ($plugin === 'App') { + return 'webservice'; + } + + return Inflector::underscore($plugin); } /** @@ -207,36 +214,20 @@ public static function defaultConnectionName() * @param array $config Configuration options passed to the constructor * @return void */ - public function initialize(array $config) + public function initialize(array $config): void { } - /** - * Returns the endpoint name or sets a new one - * - * @param string|null $endpoint the new endpoint name - * @return string - * @deprecated 2.0.0 use setName() and getName() instead. - */ - public function endpoint($endpoint = null) - { - if ($endpoint !== null) { - $this->setName($endpoint); - } - - return $this->getName(); - } - /** * Set the name of this endpoint * * @param string $name The name for this endpoint instance * @return $this */ - public function setName($name) + public function setName(string $name) { $inflectMethod = $this->getInflectionMethod(); - $this->_name = Inflector::$inflectMethod($name); + $this->_name = Inflector::{$inflectMethod}($name); return $this; } @@ -246,69 +237,37 @@ public function setName($name) * * @return string */ - public function getName() + public function getName(): string { if ($this->_name === null) { - $endpoint = namespaceSplit(get_class($this)); + $endpoint = namespaceSplit(static::class); $endpoint = substr(end($endpoint), 0, -8); $inflectMethod = $this->getInflectionMethod(); - $this->_name = Inflector::$inflectMethod($endpoint); + $this->_name = Inflector::{$inflectMethod}($endpoint); } return $this->_name; } - /** - * Returns the endpoint alias or sets a new one - * - * @param string|null $alias the new endpoint alias - * @return string - * @deprecated 2.0.0 Use getAlias and setAlias instead - */ - public function alias($alias = null) - { - if ($alias !== null) { - $this->setAlias($alias); - } - - return $this->getAlias(); - } - /** * Alias a field with the endpoint's current alias. * * @param string $field The field to alias. * @return string The field prefixed with the endpoint alias. */ - public function aliasField($field) + public function aliasField(string $field): string { return $this->getAlias() . '.' . $field; } - /** - * Returns the endpoint registry key used to create this endpoint instance - * - * @param string|null $registryAlias the key used to access this object - * @return string - * @deprecated 2.0.0 Use setRegistryAlias()/getRegistryAlias() instead. - */ - public function registryAlias($registryAlias = null) - { - if ($registryAlias !== null) { - $this->setRegistryAlias($registryAlias); - } - - return $this->getRegistryAlias(); - } - /** * Sets the table registry key used to create this table instance. * * @param string $registryAlias The key used to access this object. * @return $this */ - public function setRegistryAlias($registryAlias) + public function setRegistryAlias(string $registryAlias) { $this->_registryAlias = $registryAlias; @@ -320,7 +279,7 @@ public function setRegistryAlias($registryAlias) * * @return string */ - public function getRegistryAlias() + public function getRegistryAlias(): string { if ($this->_registryAlias === null) { $this->_registryAlias = $this->getAlias(); @@ -329,29 +288,13 @@ public function getRegistryAlias() return $this->_registryAlias; } - /** - * Set the driver to use - * - * @param \Muffin\Webservice\AbstractDriver|null $connection The driver to use - * @return \Muffin\Webservice\AbstractDriver|\Muffin\Webservice\Connection - * @deprecated 2.0.0 Use setConnection() and getConnection() instead. - */ - public function connection($connection = null) - { - if ($connection !== null) { - $this->setConnection($connection); - } - - return $this->getConnection(); - } - /** * Sets the connection driver. * - * @param \Muffin\Webservice\Connection $connection Connection instance + * @param \Muffin\Webservice\Datasource\Connection $connection Connection instance * @return $this */ - public function setConnection($connection) + public function setConnection(Connection $connection) { $this->_connection = $connection; @@ -361,37 +304,13 @@ public function setConnection($connection) /** * Returns the connection driver. * - * @return \Muffin\Webservice\Connection|null + * @return \Muffin\Webservice\Datasource\Connection */ - public function getConnection() + public function getConnection(): Connection { return $this->_connection; } - /** - * Returns the schema endpoint object describing this endpoint's properties. - * - * If an \Muffin\Webservice\Schema is passed, it will be used for this endpoint - * instead of the default one. - * - * If an array is passed, a new \Muffin\Webservice\Schema will be constructed - * out of it and used as the schema for this endpoint. - * - * @param array|\Muffin\Webservice\Schema|null $schema New schema to be used for this endpoint - * @return \Muffin\Webservice\Schema - * @deprecated 2.0.0 Use setSchema() and getSchema() instead - */ - public function schema($schema = null) - { - if ($schema === null) { - return $this->getSchema(); - } - - $this->setSchema($schema); - - return $this->getSchema(); - } - /** * Set the endpoints schema * @@ -401,7 +320,7 @@ public function schema($schema = null) * If an array is passed, a new \Muffin\Webservice\Schema will be constructed * out of it and used as the schema for this endpoint. * - * @param \Muffin\Webservice\Schema|array $schema Either an array of fields and config, or a schema object + * @param \Muffin\Webservice\Datasource\Schema|array $schema Either an array of fields and config, or a schema object * @return $this */ public function setSchema($schema) @@ -418,9 +337,9 @@ public function setSchema($schema) /** * Returns the schema endpoint object describing this endpoint's properties. * - * @return \Muffin\Webservice\Schema + * @return \Muffin\Webservice\Datasource\Schema */ - public function getSchema() + public function getSchema(): Schema { if ($this->_schema === null) { $this->_schema = $this->_initializeSchema($this->getWebservice()->describe($this->getName())); @@ -447,11 +366,11 @@ public function getSchema() * } * ``` * - * @param \Muffin\Webservice\Schema $schema The schema definition fetched from webservice. - * @return \Muffin\Webservice\Schema the altered 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) + protected function _initializeSchema(Schema $schema): Schema { return $schema; } @@ -465,27 +384,11 @@ protected function _initializeSchema(Schema $schema) * @param string $field The field to check for. * @return bool True if the field exists, false if it does not. */ - public function hasField($field) + public function hasField(string $field): bool { $schema = $this->getSchema(); - return $schema->column($field) !== null; - } - - /** - * Returns the primary key field name or sets a new one - * - * @param string|array|null $key sets a new name to be used as primary key - * @return string|array - * @deprecated 2.0.0 Use getPrimaryKey() and setPrimaryKey() instead - */ - public function primaryKey($key = null) - { - if ($key !== null) { - $this->setPrimaryKey($key); - } - - return $this->getPrimaryKey(); + return $schema->getColumn($field) !== null; } /** @@ -505,16 +408,13 @@ public function setPrimaryKey($key) * Get the endpoints primary key, if one is not set, fetch it from the schema * * @return array|string - * @throws \Muffin\Webservice\Exception\UnexpectedDriverException When no schema exists to fetch the key from + * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no schema exists to fetch the key from */ public function getPrimaryKey() { if ($this->_primaryKey === null) { $schema = $this->getSchema(); - if (!$schema) { - throw new UnexpectedDriverException(__('No schema has been defined for this endpoint')); - } - $key = (array)$schema->primaryKey(); + $key = $schema->getPrimaryKey(); if (count($key) === 1) { $key = $key[0]; } @@ -524,26 +424,10 @@ public function getPrimaryKey() return $this->_primaryKey; } - /** - * Returns the display field or sets a new one - * - * @param string|null $key sets a new name to be used as display field - * @return string - * @deprecated 2.0.0 Use setDisplayField and getDisplayField instead. - */ - public function displayField($key = null) - { - if ($key !== null) { - $this->setDisplayField($key); - } - - return $this->getDisplayField(); - } - /** * Sets the endpoint display field * - * @param string $field The new field to use as the display field + * @param string|string[] $field The new field to use as the display field * @return $this */ public function setDisplayField($field) @@ -556,8 +440,8 @@ public function setDisplayField($field) /** * Get the endpoints current display field * - * @return string - * @throws \Muffin\Webservice\Exception\UnexpectedDriverException When no schema exists to fetch the key from + * @return string|string[] + * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no schema exists to fetch the key from */ public function getDisplayField() { @@ -566,13 +450,10 @@ public function getDisplayField() $this->_displayField = array_shift($primary); $schema = $this->getSchema(); - if (!$schema) { - throw new UnexpectedDriverException(__('No schema has been defined for this endpoint')); - } - if ($schema->column('title')) { + if ($schema->getColumn('title')) { $this->_displayField = 'title'; } - if ($schema->column('name')) { + if ($schema->getColumn('name')) { $this->_displayField = 'name'; } } @@ -580,38 +461,23 @@ public function getDisplayField() return $this->_displayField; } - /** - * Returns the class used to hydrate resources for this endpoint or sets a new one - * - * @param string|null $name the name of the class to use - * @throws \Muffin\Webservice\Exception\MissingResourceClassException when the entity class cannot be found - * @return string - * @deprecated 2.0.0 Use setResourceClassName() and getResourceClassName() instead. - */ - public function resourceClass($name = null) - { - if ($name !== null) { - $this->setResourceClass($name); - } - - return $this->getResourceClass(); - } - /** * Set the resource class name used to hydrate resources for this endpoint * * @param string $name Name of the class to use * @return $this - * @throws \Muffin\Webservice\Exception\MissingResourceClassException If the resource class specified does not exist + * @throws \Muffin\Webservice\Model\Exception\MissingResourceClassException If the resource class specified does not exist */ - public function setResourceClass($name) + public function setResourceClass(string $name) { - $this->_resourceClass = App::className($name, 'Model/Resource'); - - if (!$this->_resourceClass) { + /** @psalm-var class-string<\Muffin\Webservice\Model\Resource>|null */ + $className = App::className($name, 'Model/Resource'); + if (!$className) { throw new MissingResourceClassException([$name]); } + $this->_resourceClass = $className; + return $this; } @@ -619,19 +485,21 @@ public function setResourceClass($name) * Get the resource class name used to hydrate resources for this endpoint * * @return string + * @psalm-return class-string<\Muffin\Webservice\Model\Resource> */ - public function getResourceClass() + public function getResourceClass(): string { if (!$this->_resourceClass) { - $default = \Muffin\Webservice\Model\Resource::class; - $self = get_called_class(); + $default = Resource::class; + $self = static::class; $parts = explode('\\', $self); - if ($self === __CLASS__ || count($parts) < 3) { + if ($self === self::class || count($parts) < 3) { return $this->_resourceClass = $default; } $alias = Inflector::singularize(substr(array_pop($parts), 0, -8)); + /** @psalm-var class-string<\Muffin\Webservice\Model\Resource> */ $name = implode('\\', array_slice($parts, 0, -1)) . '\Resource\\' . $alias; if (!class_exists($name)) { return $this->_resourceClass = $default; @@ -643,29 +511,13 @@ public function getResourceClass() return $this->_resourceClass; } - /** - * Returns the inflect method or sets a new one - * - * @param null|string $method The inflection method to use - * @return null|string - * @deprecated 2.0.0 Use setInflectionMethod() and getInflectionMethod() instead. - */ - public function inflectionMethod($method = null) - { - if ($method !== null) { - $this->setInflectionMethod($method); - } - - return $this->getInflectionMethod(); - } - /** * Set a new inflection method * * @param string $method The name of the inflection method * @return $this */ - public function setInflectionMethod($method) + public function setInflectionMethod(string $method) { $this->_inflectionMethod = $method; @@ -675,44 +527,24 @@ public function setInflectionMethod($method) /** * Get the inflection method * - * @return string|null + * @return string */ - public function getInflectionMethod() + public function getInflectionMethod(): string { return $this->_inflectionMethod; } - /** - * Returns an instance of the Webservice used - * - * @param \Muffin\Webservice\Webservice\WebserviceInterface|string|null $webservice The webservice to use - * @return $this|\Muffin\Webservice\Webservice\WebserviceInterface - * @deprecated 2.0.0 Use setWebservice() and getWebservice() instead. - */ - public function webservice($webservice = null) - { - if ($webservice !== null) { - return $this->setWebservice($this->getName(), $webservice); - } - - return $this->getWebservice(); - } - /** * Set the webservice instance to be used for this endpoint * * @param string $alias Alias for the webservice * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice The webservice instance * @return $this - * @throws \Muffin\Webservice\Exception\UnexpectedDriverException When no driver exists for the endpoint + * @throws \Muffin\Webservice\Webservice\Exception\UnexpectedDriverException When no driver exists for the endpoint */ - public function setWebservice($alias, $webservice) + public function setWebservice(string $alias, WebserviceInterface $webservice) { $connection = $this->getConnection(); - if (!$connection) { - throw new UnexpectedDriverException(__('No driver has been defined for this endpoint')); - } - $connection->setWebservice($alias, $webservice); $this->_webservice = $connection->getWebservice($alias); @@ -724,7 +556,7 @@ public function setWebservice($alias, $webservice) * * @return \Muffin\Webservice\Webservice\WebserviceInterface */ - public function getWebservice() + public function getWebservice(): WebserviceInterface { if ($this->_webservice === null) { $this->_webservice = $this->getConnection()->getWebservice($this->getName()); @@ -743,10 +575,10 @@ public function getWebservice() * listeners. Any listener can set a valid result set using $query * * @param string $type the type of query to perform - * @param array|\ArrayAccess $options An array that will be passed to Query::applyOptions() - * @return \Muffin\Webservice\Query + * @param array $options An array that will be passed to Query::applyOptions() + * @return \Muffin\Webservice\Datasource\Query */ - public function find($type = 'all', $options = []) + public function find(string $type = 'all', array $options = []): Query { $query = $this->query()->read(); @@ -759,11 +591,11 @@ public function find($type = 'all', $options = []) * By default findAll() applies no conditions, you * can override this method in subclasses to modify how `find('all')` works. * - * @param \Muffin\Webservice\Query $query The query to find with + * @param \Muffin\Webservice\Datasource\Query $query The query to find with * @param array $options The options to use for the find - * @return \Muffin\Webservice\Query The query builder + * @return \Muffin\Webservice\Datasource\Query The query builder */ - public function findAll(Query $query, array $options) + public function findAll(Query $query, array $options): Query { return $query; } @@ -821,11 +653,11 @@ public function findAll(Query $query, array $options) * ] * ``` * - * @param \Muffin\Webservice\Query $query The query to find with + * @param \Muffin\Webservice\Datasource\Query $query The query to find with * @param array $options The options for the find - * @return \Muffin\Webservice\Query The query builder + * @return \Muffin\Webservice\Datasource\Query The query builder */ - public function findList(Query $query, array $options) + public function findList(Query $query, array $options): Query { $options += [ 'keyField' => $this->getPrimaryKey(), @@ -860,7 +692,7 @@ public function findList(Query $query, array $options) * the associated value * @return array */ - protected function _setFieldMatchers($options, $keys) + protected function _setFieldMatchers(array $options, array $keys): array { foreach ($keys as $field) { if (!is_array($options[$field])) { @@ -900,12 +732,12 @@ protected function _setFieldMatchers($options, $keys) * ``` * * @param mixed $primaryKey primary key value to find - * @param array|\ArrayAccess $options options accepted by `Endpoint::find()` + * @param array $options Options. * @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, $options = []) + public function get($primaryKey, array $options = []): EntityInterface { $key = (array)$this->getPrimaryKey(); $alias = $this->getAlias(); @@ -922,14 +754,14 @@ public function get($primaryKey, $options = []) throw new InvalidPrimaryKeyException(sprintf( 'Record not found in endpoint "%s" with primary key [%s]', $this->getName(), - implode($primaryKey, ', ') + implode(', ', $primaryKey) )); } $conditions = array_combine($key, $primaryKey); - $cacheConfig = isset($options['cache']) ? $options['cache'] : false; - $cacheKey = isset($options['key']) ? $options['key'] : false; - $finder = isset($options['finder']) ? $options['finder'] : 'all'; + $cacheConfig = $options['cache'] ?? false; + $cacheKey = $options['key'] ?? false; + $finder = $options['finder'] ?? 'all'; unset($options['key'], $options['cache'], $options['finder']); $query = $this->find($finder, $options)->where($conditions); @@ -937,7 +769,7 @@ public function get($primaryKey, $options = []) if ($cacheConfig) { if (!$cacheKey) { $cacheKey = sprintf( - "get:%s.%s%s", + 'get:%s.%s%s', $this->getConnection()->configName(), $this->getName(), json_encode($primaryKey) @@ -961,34 +793,41 @@ public function get($primaryKey, $options = []) * called allowing you to define additional default values. The new * entity will be saved and returned. * - * @param array $search The criteria to find existing records by. + * @param mixed $search The criteria to find existing records by. * @param callable|null $callback A callback that will be invoked for newly * created entities. This callback will be called *before* the entity * is persisted. - * @return \Cake\Datasource\EntityInterface An entity. + * @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($search, ?callable $callback = null) { $query = $this->find()->where($search); $row = $query->first(); if ($row) { return $row; } + $entity = $this->newEntity(); $entity->set($search, ['guard' => false]); if ($callback) { $callback($entity); } - return $this->save($entity) ?: $entity; + $result = $this->save($entity); + if ($result === false) { + throw new PersistenceFailedException($entity, ['findOrCreate']); + } + + return $entity; } /** * Creates a new Query instance for this repository * - * @return \Muffin\Webservice\Query + * @return \Muffin\Webservice\Datasource\Query */ - public function query() + public function query(): Query { return new Query($this->getWebservice(), $this); } @@ -1003,9 +842,11 @@ public function query() * @param array $fields A hash of field => new value. * @param mixed $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) + public function updateAll($fields, $conditions): int { + /** @psalm-suppress PossiblyInvalidMethodCall, PossiblyUndefinedMethod */ return $this->query()->update()->where($conditions)->set($fields)->execute()->count(); } @@ -1018,11 +859,12 @@ public function updateAll($fields, $conditions) * need those first load a collection of records and delete them. * * @param mixed $conditions Conditions to be used, accepts anything Query::where() can take. - * @return \Traversable|int Count Returns the affected rows. - * + * @return int Count of affected rows. * @see \Muffin\Webservice\Endpoint::delete() + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress InvalidReturnType */ - public function deleteAll($conditions) + public function deleteAll($conditions): int { return $this->query()->delete()->where($conditions)->execute(); } @@ -1031,12 +873,12 @@ public function deleteAll($conditions) * Returns true if there is any record in this repository matching the specified * conditions. * - * @param array|\ArrayAccess $conditions list of conditions to pass to the query + * @param mixed $conditions list of conditions to pass to the query * @return bool */ - public function exists($conditions) + public function exists($conditions): bool { - return ($this->find()->where($conditions)->count() > 0); + return $this->find()->where($conditions)->count() > 0; } /** @@ -1044,53 +886,53 @@ public function exists($conditions) * returns the same resource after a successful save or false in case * of any error. * - * @param \Cake\Datasource\EntityInterface $resource the resource to be saved + * @param \Cake\Datasource\EntityInterface $entity the resource to be saved * @param array|\ArrayAccess $options The options to use when saving. - * @return \Cake\Datasource\EntityInterface|bool + * @return \Cake\Datasource\EntityInterface|false */ - public function save(EntityInterface $resource, $options = []) + public function save(EntityInterface $entity, $options = []) { $options = new ArrayObject((array)$options + [ 'checkRules' => true, 'checkExisting' => false, ]); - if ($resource->getErrors()) { + if ($entity->getErrors()) { return false; } - if ($resource->isNew() === false && !$resource->isDirty()) { - return $resource; + if ($entity->isNew() === false && !$entity->isDirty()) { + return $entity; } $primaryColumns = (array)$this->getPrimaryKey(); - if ($options['checkExisting'] && $primaryColumns && $resource->isNew() && $resource->has($primaryColumns)) { + if ($options['checkExisting'] && $primaryColumns && $entity->isNew() && $entity->has($primaryColumns)) { $alias = $this->getAlias(); $conditions = []; - foreach ($resource->extract($primaryColumns) as $k => $v) { + foreach ($entity->extract($primaryColumns) as $k => $v) { $conditions["$alias.$k"] = $v; } - $resource->isNew(!$this->exists($conditions)); + $entity->setNew(!$this->exists($conditions)); } - $mode = $resource->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; - if ($options['checkRules'] && !$this->checkRules($resource, $mode, $options)) { + $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; + if ($options['checkRules'] && !$this->checkRules($entity, $mode, $options)) { return false; } - $event = $this->dispatchEvent('Model.beforeSave', compact('resource', 'options')); + $event = $this->dispatchEvent('Model.beforeSave', compact('entity', 'options')); if ($event->isStopped()) { - return $event->result; + return $event->getResult(); } - $data = $resource->extract($this->getSchema()->columns(), true); + $data = $entity->extract($this->getSchema()->columns(), true); - if ($resource->isNew()) { + if ($entity->isNew()) { $query = $this->query()->create(); } else { - $query = $this->query()->update()->where($resource->extract($primaryColumns)); + $query = $this->query()->update()->where($entity->extract($primaryColumns)); } $query->set($data); @@ -1099,13 +941,14 @@ public function save(EntityInterface $resource, $options = []) return false; } - if (($resource->isNew()) && ($result instanceof EntityInterface)) { + if ($entity->isNew() && ($result instanceof EntityInterface)) { return $result; } - $className = get_class($resource); + /** @psalm-var class-string<\Cake\Datasource\EntityInterface> $className */ + $className = get_class($entity); - return new $className($resource->toArray(), [ + return new $className($entity->toArray(), [ 'markNew' => false, 'markClean' => true, ]); @@ -1114,15 +957,16 @@ public function save(EntityInterface $resource, $options = []) /** * Delete a single resource. * - * @param \Cake\Datasource\EntityInterface $resource The resource to remove. + * @param \Cake\Datasource\EntityInterface $entity The resource to remove. * @param array|\ArrayAccess $options The options for the delete. * @return bool */ - public function delete(EntityInterface $resource, $options = []) + public function delete(EntityInterface $entity, $options = []): bool { - return (bool)$this->query()->delete()->where([ - $this->getPrimaryKey() => $resource->get($this->getPrimaryKey()), - ])->execute(); + $primaryKeys = (array)$this->getPrimaryKey(); + $values = $entity->extract($primaryKeys); + + return (bool)$this->query()->delete()->where(array_combine($primaryKeys, $values))->execute(); } /** @@ -1131,7 +975,7 @@ public function delete(EntityInterface $resource, $options = []) * @param string $type name of finder to check * @return bool */ - public function hasFinder($type) + public function hasFinder(string $type): bool { $finder = 'find' . $type; @@ -1143,12 +987,12 @@ public function hasFinder($type) * if no query is passed a new one will be created and returned * * @param string $type name of the finder to be called - * @param \Muffin\Webservice\Query $query The query object to apply the finder options to + * @param \Muffin\Webservice\Datasource\Query $query The query object to apply the finder options to * @param array $options List of options to pass to the finder - * @return \Muffin\Webservice\Query + * @return \Muffin\Webservice\Datasource\Query * @throws \BadMethodCallException If the requested finder cannot be found */ - public function callFinder($type, Query $query, array $options = []) + public function callFinder(string $type, Query $query, array $options = []): Query { $query->applyOptions($options); $options = $query->getOptions(); @@ -1170,7 +1014,7 @@ public function callFinder($type, Query $query, array $options = []) * @return mixed * @throws \BadMethodCallException when there are missing arguments, or when and & or are combined. */ - protected function _dynamicFinder($method, $args) + protected function _dynamicFinder(string $method, array $args) { $method = Inflector::underscore($method); preg_match('/^find_([\w]+)_by_/', $method, $matches); @@ -1215,7 +1059,7 @@ protected function _dynamicFinder($method, $args) $conditions = [ 'OR' => $makeConditions($fields, $args), ]; - } elseif ($hasAnd !== false) { + } else { $fields = explode('_and_', $fields); $conditions = $makeConditions($fields, $args); } @@ -1250,11 +1094,9 @@ public function __call($method, $args) * Override this method if you want a endpoint object to use custom * marshalling logic. * - * @return \Muffin\Webservice\Marshaller - * - * @see \Muffin\Webservice\Marshaller + * @return \Muffin\Webservice\Datasource\Marshaller */ - public function marshaller() + public function marshaller(): Marshaller { return new Marshaller($this); } @@ -1268,18 +1110,12 @@ public function marshaller() * on the primary key data existing in the database when the entity * is saved. Until the entity is saved, it will be a detached record. * - * @param array|null $data The data to build an entity with. + * @param array $data The data to build an entity with. * @param array $options A list of options for the object hydration. - * @return \Muffin\Webservice\Model\Resource + * @return \Cake\Datasource\EntityInterface */ - public function newEntity($data = null, array $options = []) + public function newEntity(array $data = [], array $options = []): EntityInterface { - if ($data === null) { - $class = $this->getResourceClass(); - $entity = new $class([], ['source' => $this->getRegistryAlias()]); - - return $entity; - } $marshaller = $this->marshaller(); return $marshaller->one($data, $options); @@ -1287,8 +1123,23 @@ public function newEntity($data = null, array $options = []) /** * {@inheritDoc} + * + * @return \Cake\Datasource\EntityInterface + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress InvalidReturnType + */ + public function newEmptyEntity(): EntityInterface + { + $class = $this->getResourceClass(); + $entity = new $class([], ['source' => $this->getRegistryAlias()]); + + return $entity; + } + + /** + * @inheritDoc */ - public function newEntities(array $data, array $options = []) + public function newEntities(array $data, array $options = []): array { $marshaller = $this->marshaller(); @@ -1310,10 +1161,9 @@ public function newEntities(array $data, array $options = []) * data merged in * @param array $data key value list of fields to be merged into the resource * @param array $options A list of options for the object hydration. - * * @return \Cake\Datasource\EntityInterface */ - public function patchEntity(EntityInterface $entity, array $data, array $options = []) + public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface { $marshaller = $this->marshaller(); @@ -1336,10 +1186,10 @@ public function patchEntity(EntityInterface $entity, array $data, array $options * data merged in * @param array $data list of arrays to be merged into the entities * @param array $options A list of options for the objects hydration. - * * @return array + * @psalm-return array */ - public function patchEntities($entities, array $data, array $options = []) + public function patchEntities(iterable $entities, array $data, array $options = []): array { $marshaller = $this->marshaller(); @@ -1370,7 +1220,7 @@ public function patchEntities($entities, array $data, array $options = []) * * @return array */ - public function implementedEvents() + public function implementedEvents(): array { $eventMap = [ 'Model.beforeMarshal' => 'beforeMarshal', @@ -1405,7 +1255,7 @@ public function implementedEvents() * @param \Cake\Datasource\RulesChecker $rules The rules object to be modified. * @return \Cake\Datasource\RulesChecker */ - public function buildRules(RulesChecker $rules) + public function buildRules(RulesChecker $rules): RulesChecker { return $rules; } @@ -1417,15 +1267,13 @@ public function buildRules(RulesChecker $rules) */ public function __debugInfo() { - $conn = $this->getConnection(); - return [ 'registryAlias' => $this->getRegistryAlias(), 'alias' => $this->getAlias(), 'endpoint' => $this->getName(), 'resourceClass' => $this->getResourceClass(), 'defaultConnection' => $this->defaultConnectionName(), - 'connectionName' => $conn ? $conn->configName() : null, + 'connectionName' => $this->getConnection()->configName(), 'inflector' => $this->getInflectionMethod(), ]; } @@ -1448,8 +1296,12 @@ public function setAlias($alias) * * @return string */ - public function getAlias() + public function getAlias(): string { + if ($this->_alias === null) { + $this->_alias = $this->getName(); + } + return $this->_alias; } } diff --git a/src/Model/EndpointLocator.php b/src/Model/EndpointLocator.php index d2831b4..7e8081f 100644 --- a/src/Model/EndpointLocator.php +++ b/src/Model/EndpointLocator.php @@ -1,52 +1,33 @@ _instances[$alias] = $object; + return $this->instances[$alias] = $repository; } /** @@ -57,20 +38,22 @@ public function set($alias, Endpoint $object) * @return \Muffin\Webservice\Model\Endpoint * @throws \RuntimeException If the registry alias is already in use. */ - public function get($alias, array $options = []) + public function get(string $alias, array $options = []): Endpoint { - if (isset($this->_instances[$alias])) { - if (!empty($options) && $this->_options[$alias] !== $options) { - throw new RuntimeException(sprintf( - 'You cannot configure "%s", it already exists in the locator.', - $alias - )); - } - - return $this->_instances[$alias]; - } + /** @var \Muffin\Webservice\Model\Endpoint */ + return parent::get($alias, $options); + } - list(, $classAlias) = pluginSplit($alias); + /** + * Wrapper for creating endpoint instances + * + * @param string $alias Endpoint alias. + * @param array $options The alias to check for. + * @return \Muffin\Webservice\Model\Endpoint + */ + protected function createInstance(string $alias, array $options) + { + [, $classAlias] = pluginSplit($alias); $options = ['alias' => $classAlias] + $options; if (empty($options['className'])) { @@ -81,129 +64,55 @@ public function get($alias, array $options = []) $options['className'] = $className; } else { if (!isset($options['endpoint']) && strpos($options['className'], '\\') === false) { - list(, $endpoint) = pluginSplit($options['className']); + [, $endpoint] = pluginSplit($options['className']); $options['endpoint'] = Inflector::underscore($endpoint); } - $options['className'] = 'Muffin\Webservice\Model\Endpoint'; + $options['className'] = Endpoint::class; } if (empty($options['connection'])) { - if ($options['className'] !== 'Muffin\Webservice\Model\Endpoint') { + if ($options['className'] !== Endpoint::class) { $connectionName = $options['className']::defaultConnectionName(); } else { - $pluginParts = explode('/', pluginSplit($alias)[0]); - - $connectionName = Inflector::underscore(end($pluginParts)); + if (strpos($alias, '.') === false) { + $connectionName = 'webservice'; + } else { + /** @psalm-suppress PossiblyNullArgument */ + $pluginParts = explode('/', pluginSplit($alias)[0]); + $connectionName = Inflector::underscore(end($pluginParts)); + } } - $options['connection'] = ConnectionManager::get($connectionName); + $options['connection'] = $this->getConnection($connectionName); + } elseif (is_string($options['connection'])) { + $options['connection'] = $this->getConnection($options['connection']); } $options['registryAlias'] = $alias; - $this->_instances[$alias] = $this->_create($options); - $this->_options[$alias] = $options; - return $this->_instances[$alias]; - } + /** @psalm-var class-string<\Muffin\Webservice\Model\Endpoint> $className */ + $className = $options['className']; - /** - * Check to see if an instance exists in the locator. - * - * @param string $alias The alias to check for. - * @return bool - */ - public function exists($alias) - { - return isset($this->_instances[$alias]); + return new $className($options); } /** - * Returns configuration for an alias or the full configuration array for all aliases. + * Get connection instance. * - * @param string|null $alias Endpoint alias - * @return array + * @param string $connectionName Connection name. + * @return \Muffin\Webservice\Datasource\Connection */ - public function getConfig($alias = null) + protected function getConnection(string $connectionName): Connection { - if ($alias === null) { - return $this->_config; + try { + /** @var \Muffin\Webservice\Datasource\Connection */ + return ConnectionManager::get($connectionName); + } catch (MissingDatasourceConfigException $e) { + $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()); } - - return isset($this->_config[$alias]) ? $this->_config[$alias] : []; - } - - /** - * Stores a list of options to be used when instantiating an object with a matching alias. - * - * If configuring many aliases, use an array keyed by the alias. - * - * ``` - * $locator->setConfig([ - * 'Example' => ['registryAlias' => 'example'], - * 'Posts' => ['registryAlias' => 'posts'], - * ]); - * ``` - * - * @param string|array $alias The alias to set configuration for, or an array of configuration keyed by alias - * @param null|array $config Array of configuration options - * @return $this - * @throws \RuntimeException - */ - public function setConfig($alias, $config = null) - { - if (!is_string($alias)) { - $this->_config = $alias; - - return $this; - } - - if (isset($this->_instances[$alias])) { - throw new RuntimeException(sprintf( - 'You cannot configure "%s", it has already been constructed.', - $alias - )); - } - - $this->_config[$alias] = $config; - - return $this; - } - - /** - * Clear the endpoint locator of all instances - * - * @return void - */ - public function clear() - { - $this->_instances = []; - $this->_options = []; - $this->_config = []; - } - - /** - * Remove a specific endpoint instance from the locator by alias - * - * @param string $alias String alias of the endpoint - * @return void - */ - public function remove($alias) - { - unset( - $this->_instances[$alias], - $this->_options[$alias], - $this->_config[$alias] - ); - } - - /** - * Wrapper for creating endpoint instances - * - * @param array $options The alias to check for. - * @return \Muffin\Webservice\Model\Endpoint - */ - protected function _create(array $options) - { - return new $options['className']($options); } } diff --git a/src/Model/EndpointRegistry.php b/src/Model/EndpointRegistry.php deleted file mode 100644 index ff70b99..0000000 --- a/src/Model/EndpointRegistry.php +++ /dev/null @@ -1,123 +0,0 @@ - $classAlias] + $options; - - if (empty($options['className'])) { - $options['className'] = Inflector::camelize($alias); - } - $className = App::className($options['className'], 'Model/Endpoint', 'Endpoint'); - if ($className) { - $options['className'] = $className; - } else { - if (!isset($options['endpoint']) && strpos($options['className'], '\\') === false) { - list(, $endpoint) = pluginSplit($options['className']); - $options['endpoint'] = Inflector::underscore($endpoint); - } - $options['className'] = 'Muffin\Webservice\Model\Endpoint'; - } - - if (empty($options['connection'])) { - if ($options['className'] !== 'Muffin\Webservice\Model\Endpoint') { - $connectionName = $options['className']::defaultConnectionName(); - } else { - $pluginParts = explode('/', pluginSplit($alias)[0]); - - $connectionName = Inflector::underscore(end($pluginParts)); - } - - $options['connection'] = ConnectionManager::get($connectionName); - } - - $options['registryAlias'] = $alias; - self::$_instances[$alias] = self::_create($options); - self::$_options[$alias] = $options; - - return self::$_instances[$alias]; - } - - /** - * Wrapper for creating endpoint instances - * - * @param array $options The alias to check for. - * - * @return \Muffin\Webservice\Model\Endpoint - */ - protected static function _create(array $options) - { - return new $options['className']($options); - } -} diff --git a/src/Exception/MissingEndpointSchemaException.php b/src/Model/Exception/MissingEndpointSchemaException.php similarity index 56% rename from src/Exception/MissingEndpointSchemaException.php rename to src/Model/Exception/MissingEndpointSchemaException.php index 9341997..a1a4891 100644 --- a/src/Exception/MissingEndpointSchemaException.php +++ b/src/Model/Exception/MissingEndpointSchemaException.php @@ -1,12 +1,12 @@ isNew($options['markNew']); + $this->setNew($options['markNew']); } if (!empty($properties) && $options['markClean'] && !$options['useSetters']) { - $this->_properties = $properties; + $this->_fields = $properties; return; } diff --git a/src/Model/ResourceBasedEntityInterface.php b/src/Model/ResourceBasedEntityInterface.php index 06ef4fe..66b1d37 100644 --- a/src/Model/ResourceBasedEntityInterface.php +++ b/src/Model/ResourceBasedEntityInterface.php @@ -1,17 +1,17 @@ set($resource->toArray()); } diff --git a/src/Model/Schema.php b/src/Model/Schema.php index 74a2d0d..89639b7 100644 --- a/src/Model/Schema.php +++ b/src/Model/Schema.php @@ -1,17 +1,19 @@ stopPropagation()` to achieve the same result. - * - * @param \Cake\Event\Event $event container object having the `request`, `response` and `additionalParams` - * keys in the data property. - * @return void - */ - public function beforeDispatch(Event $event) - { - $controller = false; - if (isset($event->data['controller'])) { - $controller = $event->data['controller']; - } - if ($controller) { - $callback = ['Muffin\Webservice\Model\EndpointRegistry', 'get']; - $controller->modelFactory('Endpoint', $callback); - } - } -} diff --git a/src/AbstractDriver.php b/src/Webservice/Driver/AbstractDriver.php similarity index 62% rename from src/AbstractDriver.php rename to src/Webservice/Driver/AbstractDriver.php index d375ea1..1662168 100644 --- a/src/AbstractDriver.php +++ b/src/Webservice/Driver/AbstractDriver.php @@ -1,15 +1,17 @@ setConfig($config); @@ -63,23 +65,7 @@ public function __construct($config = []) * * @return void */ - abstract public function initialize(); - - /** - * Set or return an instance of the client used for communication - * - * @param object $client The client to use - * @return object - * @deprecated 2.0.0 Use setClient() and getClient() instead. - */ - public function client($client = null) - { - if ($client === null) { - return $this->getClient(); - } - - return $this->setClient($client); - } + abstract public function initialize(): void; /** * Set the client instance this driver will use to make requests @@ -87,7 +73,7 @@ public function client($client = null) * @param object $client Client instance * @return $this */ - public function setClient($client) + public function setClient(object $client) { $this->_client = $client; @@ -99,28 +85,11 @@ public function setClient($client) * * @return object */ - public function getClient() + public function getClient(): object { return $this->_client; } - /** - * Set or get a instance of a webservice - * - * @param string $name The name of the webservice - * @param \Muffin\Webservice\Webservice\WebserviceInterface|null $webservice The instance of the webservice you'd like to set - * @return $this|\Muffin\Webservice\Webservice\WebserviceInterface - * @deprecated 2.0.0 Use setWebservice() or getWebservice() instead. - */ - public function webservice($name, WebserviceInterface $webservice = null) - { - if ($webservice !== null) { - $this->setWebservice($name, $webservice); - } - - return $this->getWebservice($name); - } - /** * Set the webservice instance used by the driver * @@ -128,7 +97,7 @@ public function webservice($name, WebserviceInterface $webservice = null) * @param \Muffin\Webservice\Webservice\WebserviceInterface $webservice Instance of the webservice * @return $this */ - public function setWebservice($name, WebserviceInterface $webservice) + public function setWebservice(string $name, WebserviceInterface $webservice) { $this->_webservices[$name] = $webservice; @@ -139,12 +108,12 @@ public function setWebservice($name, WebserviceInterface $webservice) * Fetch a webservice instance from the driver registry * * @param string $name Registry alias to fetch - * @return \Muffin\Webservice\Webservice\WebserviceInterface|null + * @return \Muffin\Webservice\Webservice\WebserviceInterface */ - public function getWebservice($name) + public function getWebservice(string $name): WebserviceInterface { if (!isset($this->_webservices[$name])) { - list($pluginName) = pluginSplit(App::shortName(get_class($this), 'Webservice/Driver')); + [$pluginName] = pluginSplit(App::shortName(static::class, 'Webservice/Driver')); $webserviceClass = implode('.', array_filter([$pluginName, Inflector::camelize($name)])); @@ -160,15 +129,17 @@ public function getWebservice($name) } /** - * Returns a logger instance - * - * @return \Psr\Log\LoggerInterface + * Sets a logger * - * @deprecated 1.4.0 Use getLogger() instead. + * @param \Psr\Log\LoggerInterface $logger Logger object + * @return $this + * @psalm-suppress ImplementedReturnTypeMismatch */ - public function logger() + public function setLogger(LoggerInterface $logger) { - return $this->logger; + $this->logger = $logger; + + return $this; } /** @@ -176,7 +147,7 @@ public function logger() * * @return \Psr\Log\LoggerInterface|null */ - public function getLogger() + public function getLogger(): ?LoggerInterface { return $this->logger; } @@ -186,28 +157,11 @@ public function getLogger() * * @return string */ - public function configName() + public function configName(): string { return (string)$this->_config['name']; } - /** - * Enables or disables query logging for this driver - * - * @param bool|null $enable whether to turn logging on or disable it. Use null to read current value. - * @return bool - * - * @deprecated 1.4.0 Use enableQueryLogging()/disableQueryLogging()/isQueryLoggingEnabled() instead. - */ - public function logQueries($enable = null) - { - if ($enable === null) { - return $this->_logQueries; - } - - return $this->_logQueries = $enable; - } - /** * Enable query logging for the driver * @@ -237,7 +191,7 @@ public function disableQueryLogging() * * @return bool */ - public function isQueryLoggingEnabled() + public function isQueryLoggingEnabled(): bool { return $this->_logQueries; } @@ -249,17 +203,10 @@ public function isQueryLoggingEnabled() * @param array $args Arguments to pass-through. * @return mixed * @throws \RuntimeException If the client object has not been initialized. - * @throws \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException If the method does not exist in the client. + * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException If the method does not exist in the client. */ public function __call($method, $args) { - if (!is_object($this->getClient())) { - throw new RuntimeException(sprintf( - 'The `%s` client has not been initialized', - $this->getConfig('name') - )); - } - if (!method_exists($this->getClient(), $method)) { throw new UnimplementedWebserviceMethodException([ 'name' => $this->getConfig('name'), @@ -279,7 +226,7 @@ public function __debugInfo() { return [ 'client' => $this->getClient(), - 'logger' => $this->logger(), + 'logger' => $this->getLogger(), 'query_logging' => $this->isQueryLoggingEnabled(), 'webservices' => array_keys($this->_webservices), ]; @@ -292,26 +239,28 @@ public function __debugInfo() * * @param string $className Class name of the webservice to initialize * @param array $options Set of options to pass to the constructor - * @return WebserviceInterface - * @throws \Muffin\Webservice\Exception\MissingWebserviceClassException If no webservice class can be found + * @return \Muffin\Webservice\Webservice\WebserviceInterface + * @throws \Muffin\Webservice\Webservice\Exception\MissingWebserviceClassException If no webservice class can be found */ - protected function _createWebservice($className, array $options = []) + protected function _createWebservice(string $className, array $options = []): WebserviceInterface { $webservice = App::className($className, 'Webservice', 'Webservice'); if ($webservice) { + /** @psalm-var \Muffin\Webservice\Webservice\WebserviceInterface */ return new $webservice($options); } - $namespaceParts = explode('\\', get_class($this)); + $namespaceParts = explode('\\', static::class); $fallbackWebserviceClass = end($namespaceParts); - list($pluginName) = pluginSplit($className); + [$pluginName] = pluginSplit($className); if ($pluginName) { $fallbackWebserviceClass = $pluginName . '.' . $fallbackWebserviceClass; } $fallbackWebservice = App::className($fallbackWebserviceClass, 'Webservice', 'Webservice'); if ($fallbackWebservice) { + /** @psalm-var \Muffin\Webservice\Webservice\WebserviceInterface */ return new $fallbackWebservice($options); } diff --git a/src/Exception/MissingDriverException.php b/src/Webservice/Exception/MissingDriverException.php similarity index 54% rename from src/Exception/MissingDriverException.php rename to src/Webservice/Exception/MissingDriverException.php index 07de5dc..be71b8d 100644 --- a/src/Exception/MissingDriverException.php +++ b/src/Webservice/Exception/MissingDriverException.php @@ -1,11 +1,12 @@ getDriver(); - } - - return $this->setDriver($driver); - } - /** * Set the webservice driver and return the instance for chaining * - * @param \Muffin\Webservice\AbstractDriver $driver Instance of the driver + * @param \Muffin\Webservice\Webservice\Driver\AbstractDriver $driver Instance of the driver * @return $this */ public function setDriver(AbstractDriver $driver) @@ -99,27 +86,15 @@ public function setDriver(AbstractDriver $driver) /** * Get this webservices driver * - * @return \Muffin\Webservice\AbstractDriver|null + * @return \Muffin\Webservice\Webservice\Driver\AbstractDriver */ - public function getDriver() + public function getDriver(): AbstractDriver { - return $this->_driver; - } - - /** - * Set the endpoint path to use - * - * @param string|null $endpoint The endpoint - * @return string|$this - * @deprecated 2.0.0 Use setEndpoint() and getEndpoint() instead. - */ - public function endpoint($endpoint = null) - { - if ($endpoint === null) { - return $this->getEndpoint(); + if ($this->_driver === null) { + throw new RuntimeException('No driver has been defined'); } - return $this->setEndpoint($endpoint); + return $this->_driver; } /** @@ -128,7 +103,7 @@ public function endpoint($endpoint = null) * @param string $endpoint Endpoint path * @return $this */ - public function setEndpoint($endpoint) + public function setEndpoint(string $endpoint) { $this->_endpoint = $endpoint; @@ -140,7 +115,7 @@ public function setEndpoint($endpoint) * * @return string */ - public function getEndpoint() + public function getEndpoint(): string { return $this->_endpoint; } @@ -152,7 +127,7 @@ public function getEndpoint() * @param array $requiredFields The required fields * @return void */ - public function addNestedResource($url, array $requiredFields) + public function addNestedResource(string $url, array $requiredFields): void { $this->_nestedResources[$url] = [ 'requiredFields' => $requiredFields, @@ -163,40 +138,39 @@ public function addNestedResource($url, array $requiredFields) * Checks if a set of conditions match a nested resource * * @param array $conditions The conditions in a query - * @return bool|string Either a URL or false in case no nested resource matched + * @return string|null Either a URL or false in case no nested resource matched */ - public function nestedResource(array $conditions) + public function nestedResource(array $conditions): ?string { foreach ($this->_nestedResources as $url => $options) { - if (count(array_intersect_key(array_flip($options['requiredFields']), $conditions)) !== count($options['requiredFields'])) { + $fieldsInConditionsCount = count(array_intersect_key(array_flip($options['requiredFields']), $conditions)); + $requiredFieldsCount = count($options['requiredFields']); + + if ($fieldsInConditionsCount !== $requiredFieldsCount) { continue; } return Text::insert($url, $conditions); } - return false; + return null; } /** * Executes a query * - * @param Query $query The query to execute + * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * - * @return \Muffin\Webservice\ResultSet|int|bool + * @return bool|int|\Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet */ public function execute(Query $query, array $options = []) { $result = $this->_executeQuery($query, $options); - if ($this->getDriver() === null) { - throw new \UnexpectedValueException(__('No driver has been defined')); - } + $logger = $this->getDriver()->getLogger(); - // Write to the logger when one has been defined - if ($this->getDriver()->getLogger()) { - $this->_logQuery($query, $this->getDriver()->getLogger()); + if ($logger !== null) { + $this->_logQuery($query, $logger); } return $result; @@ -206,17 +180,18 @@ public function execute(Query $query, array $options = []) * Returns a schema for the provided endpoint * * @param string $endpoint The endpoint to get the schema for - * @return \Muffin\Webservice\Schema The schema to use + * @return \Muffin\Webservice\Datasource\Schema The schema to use */ - public function describe($endpoint) + public function describe(string $endpoint): Schema { - $shortName = App::shortName(get_class($this), 'Webservice', 'Webservice'); - list($plugin, $name) = pluginSplit($shortName); + $shortName = App::shortName(static::class, 'Webservice', 'Webservice'); + [$plugin, $name] = pluginSplit($shortName); $endpoint = Inflector::classify(str_replace('-', '_', $endpoint)); $schemaShortName = implode('.', array_filter([$plugin, $endpoint])); $schemaClassName = App::className($schemaShortName, 'Model/Endpoint/Schema', 'Schema'); if ($schemaClassName) { + /** @var \Muffin\Webservice\Datasource\Schema */ return new $schemaClassName($endpoint); } @@ -229,13 +204,15 @@ public function describe($endpoint) /** * Execute the appropriate method for a query * - * @param \Muffin\Webservice\Query $query The query to execute + * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return bool|int|\Muffin\Webservice\ResultSet + * @return bool|int|\Muffin\Webservice\Model\Resource|\Muffin\Webservice\Datasource\ResultSet + * @psalm-suppress NullableReturnStatement + * @psalm-suppress InvalidNullableReturnType */ protected function _executeQuery(Query $query, array $options = []) { - switch ($query->action()) { + switch ($query->clause('action')) { case Query::ACTION_CREATE: return $this->_executeCreateQuery($query, $options); case Query::ACTION_READ: @@ -252,16 +229,16 @@ protected function _executeQuery(Query $query, array $options = []) /** * Executes a query with the create action * - * @param \Muffin\Webservice\Query $query The query to execute + * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return bool|void - * @throws \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been + * @return bool|\Muffin\Webservice\Model\Resource + * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been * implemented into userland classes */ protected function _executeCreateQuery(Query $query, array $options = []) { throw new UnimplementedWebserviceMethodException([ - 'name' => get_class($this), + 'name' => static::class, 'method' => '_executeCreateQuery', ]); } @@ -269,16 +246,16 @@ protected function _executeCreateQuery(Query $query, array $options = []) /** * Executes a query with the read action * - * @param \Muffin\Webservice\Query $query The query to execute + * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return \Muffin\Webservice\ResultSet|bool|void - * @throws \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been + * @return bool|\Muffin\Webservice\Datasource\ResultSet + * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been * implemented into userland classes */ protected function _executeReadQuery(Query $query, array $options = []) { throw new UnimplementedWebserviceMethodException([ - 'name' => get_class($this), + 'name' => static::class, 'method' => '_executeReadQuery', ]); } @@ -286,16 +263,16 @@ protected function _executeReadQuery(Query $query, array $options = []) /** * Executes a query with the update action * - * @param \Muffin\Webservice\Query $query The query to execute + * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return int|bool|void - * @throws \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been + * @return int|bool|\Muffin\Webservice\Model\Resource + * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been * implemented into userland classes */ protected function _executeUpdateQuery(Query $query, array $options = []) { throw new UnimplementedWebserviceMethodException([ - 'name' => get_class($this), + 'name' => static::class, 'method' => '_executeUpdateQuery', ]); } @@ -303,16 +280,16 @@ protected function _executeUpdateQuery(Query $query, array $options = []) /** * Executes a query with the delete action * - * @param \Muffin\Webservice\Query $query The query to execute + * @param \Muffin\Webservice\Datasource\Query $query The query to execute * @param array $options The options to use - * @return int|bool|void - * @throws \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been + * @return int|bool + * @throws \Muffin\Webservice\Webservice\Exception\UnimplementedWebserviceMethodException When this method has not been * implemented into userland classes */ protected function _executeDeleteQuery(Query $query, array $options = []) { throw new UnimplementedWebserviceMethodException([ - 'name' => get_class($this), + 'name' => static::class, 'method' => '_executeDeleteQuery', ]); } @@ -323,8 +300,11 @@ protected function _executeDeleteQuery(Query $query, array $options = []) * @param string $resourceClass The class to use to create the resource * @param array $properties The properties to apply * @return \Muffin\Webservice\Model\Resource + * @psalm-suppress LessSpecificReturnStatement + * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress InvalidStringClass */ - protected function _createResource($resourceClass, array $properties = []) + protected function _createResource(string $resourceClass, array $properties = []): Resource { return new $resourceClass($properties, [ 'markClean' => true, @@ -335,17 +315,17 @@ protected function _createResource($resourceClass, array $properties = []) /** * Logs a query to the specified logger * - * @param \Muffin\Webservice\Query $query The query to log + * @param \Muffin\Webservice\Datasource\Query $query The query to log * @param \Psr\Log\LoggerInterface $logger The logger instance to use * @return void */ - protected function _logQuery(Query $query, LoggerInterface $logger) + protected function _logQuery(Query $query, LoggerInterface $logger): void { if (!$this->getDriver()->isQueryLoggingEnabled()) { return; } - $logger->debug($query->endpoint()->getName(), [ + $logger->debug($query->getEndpoint()->getName(), [ 'params' => $query->where(), ]); } @@ -357,7 +337,7 @@ protected function _logQuery(Query $query, LoggerInterface $logger) * @param array $results Array of results from the API * @return \Muffin\Webservice\Model\Resource[] Array of resource objects */ - protected function _transformResults(Endpoint $endpoint, array $results) + protected function _transformResults(Endpoint $endpoint, array $results): array { $resources = []; foreach ($results as $result) { @@ -374,7 +354,7 @@ protected function _transformResults(Endpoint $endpoint, array $results) * @param array $result The API result * @return \Muffin\Webservice\Model\Resource */ - protected function _transformResource(Endpoint $endpoint, array $result) + protected function _transformResource(Endpoint $endpoint, array $result): Resource { $properties = []; @@ -393,8 +373,8 @@ protected function _transformResource(Endpoint $endpoint, array $result) public function __debugInfo() { return [ - 'driver' => $this->getDriver(), - 'endpoint' => $this->getEndpoint(), + 'driver' => $this->_driver, + 'endpoint' => $this->_endpoint, ]; } } diff --git a/src/Webservice/WebserviceInterface.php b/src/Webservice/WebserviceInterface.php index 496d45f..6c41616 100644 --- a/src/Webservice/WebserviceInterface.php +++ b/src/Webservice/WebserviceInterface.php @@ -1,8 +1,10 @@ loadPlugins(['Muffin/Webservice']); + } + /** * Test that the plugins bootstrap is correctly registering the Endpoint * repository type with the factory locator @@ -38,7 +43,6 @@ public function testFactoryLocatorAddition() { $result = FactoryLocator::get('Endpoint'); - $this->assertInstanceOf(EndpointLocator::class, $result[0]); - $this->assertSame('get', $result[1]); + $this->assertInstanceOf(EndpointLocator::class, $result); } } diff --git a/tests/TestCase/ConnectionTest.php b/tests/TestCase/ConnectionTest.php index 740e819..7347738 100644 --- a/tests/TestCase/ConnectionTest.php +++ b/tests/TestCase/ConnectionTest.php @@ -1,18 +1,21 @@ expectException(MissingDriverException::class); + new Connection([ 'name' => 'test', 'service' => 'MissingDriver', ]); } - /** - * @expectedException \Muffin\Webservice\Exception\MissingConnectionException - */ public function testConstructorNoDriver() { + $this->expectException(MissingConnectionException::class); + new Connection([ 'name' => 'test', ]); diff --git a/tests/TestCase/MarshallerTest.php b/tests/TestCase/MarshallerTest.php index 5337308..fbad5d6 100644 --- a/tests/TestCase/MarshallerTest.php +++ b/tests/TestCase/MarshallerTest.php @@ -1,17 +1,18 @@ 'test', @@ -32,6 +33,7 @@ public function setUp() 'connection' => $connection, 'primaryKey' => 'id', 'displayField' => 'title', + 'alias' => 'TestEndpoint', ]); $this->marshaller = new Marshaller($endpoint); @@ -221,7 +223,7 @@ public function testMergeWithValidationErrors() 'title' => 'Testing', 'body' => 'Longer body', ]); - $entity->isNew(false); + $entity->setNew(false); $data = [ 'title' => 'Changed the title', diff --git a/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php b/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php index dc8454d..b040ce4 100644 --- a/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php +++ b/tests/TestCase/Model/Endpoint/Schema/SchemaTest.php @@ -1,8 +1,10 @@ schema = new TestSchema('test'); } @@ -38,7 +40,7 @@ public function testColumns() $this->assertEquals(['id', 'title', 'body'], $this->schema->columns()); } - public function testColumn() + public function testGetColumn() { $this->assertEquals( [ @@ -50,7 +52,7 @@ public function testColumn() 'comment' => null, 'primaryKey' => null, ], - $this->schema->column('id') + $this->schema->getColumn('id') ); } @@ -124,7 +126,7 @@ public function testDefaultValues() public function testPrimaryKey() { $this->schema->addColumn('id', ['type' => 'integer', 'primaryKey' => true]); - $this->assertEquals(['id'], $this->schema->primaryKey()); + $this->assertEquals(['id'], $this->schema->getPrimaryKey()); } public function testOptions() diff --git a/tests/TestCase/Model/EndpointLocatorTest.php b/tests/TestCase/Model/EndpointLocatorTest.php index 7621528..e64e524 100644 --- a/tests/TestCase/Model/EndpointLocatorTest.php +++ b/tests/TestCase/Model/EndpointLocatorTest.php @@ -1,7 +1,12 @@ Locator = new EndpointLocator(); } - public function tearDown() + public function tearDown(): void { parent::tearDown(); unset($this->Locator); } - public function testConfig() - { - $this->assertSame([], $this->Locator->getConfig()); - - $configTest = ['foo' => 'bar']; - $this->Locator->setConfig('test', $configTest); - $this->assertSame($configTest, $this->Locator->getConfig('test')); - - $configExample = ['example' => true]; - $this->Locator->setConfig('example', $configExample); - $this->assertSame($configExample, $this->Locator->getConfig('example')); - } - - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage You cannot configure "Test", it has already been constructed. - */ - public function testSetConfigForExistingObject() - { - $this->Locator->get('Test', [ - 'registryAlias' => 'Test', - 'connection' => 'test', - ]); - - $this->Locator->setConfig('Test', ['foo' => 'bar']); - } - - public function testSetConfigUsingAliasArray() - { - $multiAliasConfig = [ - 'Test' => ['foo' => 'bar'], - 'Example' => ['foo' => 'bar'], - ]; - - $result = $this->Locator->setConfig($multiAliasConfig); - $this->assertInstanceOf(EndpointLocator::class, $result); - - $this->assertSame($multiAliasConfig, $this->Locator->getConfig()); - } - public function testRemoveUsingExists() { /** @var \PHPUnit\Framework\MockObject\MockObject|\Muffin\Webservice\Model\Endpoint $first */ $first = $this->getMockBuilder(Endpoint::class) ->setConstructorArgs([['alias' => 'First']]) - ->setMethods(['getAlias']) + ->onlyMethods(['getAlias']) ->getMock(); $first->expects($this->any()) ->method('getAlias') ->willReturn('First'); - /** @var \PHPUnit\Framework\MockObject\MockObject|\Muffin\Webservice\Model\Endpoint $first */ + /** @var \PHPUnit\Framework\MockObject\MockObject|\Muffin\Webservice\Model\Endpoint $second */ $second = $this->getMockBuilder(Endpoint::class) ->setConstructorArgs([['alias' => 'Second']]) - ->setMethods(['getAlias']) + ->onlyMethods(['getAlias']) ->getMock(); $second->expects($this->any()) ->method('getAlias') @@ -101,7 +66,7 @@ public function testGet() /** @var \Muffin\Webservice\Model\Endpoint $first */ $first = $this->getMockBuilder(Endpoint::class) ->setConstructorArgs([['alias' => 'First']]) - ->setMethods(['getAlias']) + ->onlyMethods(['getAlias']) ->getMock(); $first->expects($this->any()) ->method('getAlias') @@ -113,12 +78,23 @@ public function testGet() $this->assertSame($first, $result); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage You cannot configure "First", it already exists in the locator. - */ + public function testGetException() + { + $this->expectException(MissingDatasourceConfigException::class); + $this->expectExceptionMessage( + 'The datasource configuration "non-existent" was not found.' + . ' You can override Endpoint::defaultConnectionName() to return the connection name you want.' + ); + + $locator = new EndpointLocator(); + $locator->get('Foo', ['className' => TestEndpoint::class, 'connection' => 'non-existent']); + } + public function testGetWithExistingObject() { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('You cannot configure "First", it already exists in the registry.'); + $result = $this->Locator->get('First', [ 'className' => Endpoint::class, 'registryAlias' => 'First', @@ -155,7 +131,7 @@ public function testClear() /** @var \Muffin\Webservice\Model\Endpoint $first */ $first = $this->getMockBuilder(Endpoint::class) ->setConstructorArgs([['alias' => 'First']]) - ->setMethods(['getAlias']) + ->onlyMethods(['getAlias']) ->getMock(); $first->expects($this->any()) ->method('getAlias') diff --git a/tests/TestCase/Model/EndpointRegistryTest.php b/tests/TestCase/Model/EndpointRegistryTest.php deleted file mode 100644 index 24ceb02..0000000 --- a/tests/TestCase/Model/EndpointRegistryTest.php +++ /dev/null @@ -1,127 +0,0 @@ - new Connection([ - 'name' => 'test', - 'service' => 'test', - ]), - ]); - - $this->assertInstanceOf(Endpoint::class, $result); - $this->assertEquals('test', $result->endpoint()); - } - - /** - * Ensure that if you try and set the options for an already configured Endpoint instance an - * exception is thrown - * - * @expectedException \RuntimeException - * @expectedExceptionMessage You cannot configure "Test", it already exists in the registry. - */ - public function testReconfiguringExistingInstance() - { - $result = EndpointRegistry::get('Test', [ - 'connection' => new Connection([ - 'name' => 'test', - 'service' => 'test', - ]), - 'displayField' => 'foo', - ]); - - $this->assertInstanceOf(Endpoint::class, $result); - $this->assertEquals('test', $result->endpoint()); - - $result = EndpointRegistry::get('Test', [ - 'displayField' => 'foo', - ]); - } - - public function testGettingSameInstance() - { - $result = EndpointRegistry::get('Test', [ - 'connection' => new Connection([ - 'name' => 'test', - 'service' => 'test', - ]), - ]); - - $this->assertInstanceOf(Endpoint::class, $result); - $this->assertEquals('test', $result->endpoint()); - - $result = EndpointRegistry::get('Test'); - - $this->assertInstanceOf(Endpoint::class, $result); - $this->assertEquals('test', $result->endpoint()); - } - - public function testGetInstanceWithNoEndpointName() - { - $result = EndpointRegistry::get('Test', [ - 'connection' => new Connection([ - 'name' => 'test', - 'service' => 'test', - ]), - 'className' => 'UnfindableClass', - ]); - - $this->assertInstanceOf(Endpoint::class, $result); - $this->assertEquals('unfindable_class', $result->endpoint()); - } - - public function testRemovingInstance() - { - $result = EndpointRegistry::get('Test', [ - 'connection' => new Connection([ - 'name' => 'test', - 'service' => 'test', - ]), - ]); - - $this->assertInstanceOf(Endpoint::class, $result); - - EndpointRegistry::remove('Test'); - - $ref = new \ReflectionClass(EndpointRegistry::class); - $this->assertEmpty($ref->getStaticProperties()['_instances']); - $this->assertEmpty($ref->getStaticProperties()['_options']); - } - - public function testClearing() - { - $result = EndpointRegistry::get('Test', [ - 'connection' => new Connection([ - 'name' => 'test', - 'service' => 'test', - ]), - ]); - - $this->assertInstanceOf(Endpoint::class, $result); - - EndpointRegistry::clear(); - - $ref = new \ReflectionClass(EndpointRegistry::class); - $this->assertEmpty($ref->getStaticProperties()['_instances']); - $this->assertEmpty($ref->getStaticProperties()['_options']); - } -} diff --git a/tests/TestCase/Model/EndpointTest.php b/tests/TestCase/Model/EndpointTest.php index b19fe8c..8d26ce9 100644 --- a/tests/TestCase/Model/EndpointTest.php +++ b/tests/TestCase/Model/EndpointTest.php @@ -1,36 +1,41 @@ endpoint->find(); - $this->assertInstanceOf('\Muffin\Webservice\Query', $query); + $this->assertInstanceOf(Query::class, $query); } public function testFindByTitle() @@ -123,11 +128,10 @@ public function testExists() $this->assertFalse($this->endpoint->exists(['id' => 10])); } - /** - * @expectedException \Cake\Datasource\Exception\RecordNotFoundException - */ public function testGetNonExisting() { + $this->expectException(RecordNotFoundException::class); + $this->endpoint->get(10); } @@ -175,11 +179,10 @@ public function testUpdatingSave() $this->assertEquals($newResource->title, 'New ORM for webservices'); } - /** - * @expectedException \Cake\Datasource\Exception\RecordNotFoundException - */ public function testDelete() { + $this->expectException(RecordNotFoundException::class); + $resource = $this->endpoint->get(2); $this->assertTrue($this->endpoint->delete($resource)); @@ -200,10 +203,13 @@ public function testDeleteAll() public function testNewEntity() { - $this->assertEquals(new Resource([ + $resource = new Resource([ 'title' => 'New entity', 'body' => 'New entity body', - ]), $this->endpoint->newEntity([ + ]); + $resource->setSource('test'); + + $this->assertEquals($resource, $this->endpoint->newEntity([ 'title' => 'New entity', 'body' => 'New entity body', ])); @@ -211,15 +217,21 @@ public function testNewEntity() public function testNewEntities() { + $resource1 = new Resource([ + 'title' => 'New entity', + 'body' => 'New entity body', + ]); + $resource1->setSource('test'); + + $resource2 = new Resource([ + 'title' => 'Second new entity', + 'body' => 'Second new entity body', + ]); + $resource2->setSource('test'); + $this->assertEquals([ - new Resource([ - 'title' => 'New entity', - 'body' => 'New entity body', - ]), - new Resource([ - 'title' => 'Second new entity', - 'body' => 'Second new entity body', - ]), + $resource1, + $resource2, ], $this->endpoint->newEntities([ [ 'title' => 'New entity', @@ -257,7 +269,6 @@ public function testAliasField() public function testConnection() { $endpoint = new Endpoint(['endpoint' => 'users']); - $this->assertNull($endpoint->getConnection()); $endpoint->setConnection($this->connection); $this->assertSame($this->connection, $endpoint->getConnection()); } @@ -377,7 +388,7 @@ public function testSchema() $schema = ['id' => ['type' => 'integer']]; $endpoint->setSchema($schema); $this->assertEquals( - new \Muffin\Webservice\Schema('another', $schema), + new Schema('another', $schema), $endpoint->getSchema() ); } @@ -391,7 +402,7 @@ public function testFindWithSelectAndWhere() ->select($fields) ->where($conditions); - $this->assertInstanceOf('\Muffin\Webservice\Query', $query); + $this->assertInstanceOf(Query::class, $query); $this->assertSame($fields, $query->clause('select')); $this->assertSame($conditions, $query->clause('where')); } @@ -414,14 +425,13 @@ public function testConstructorResourceClass() 'resourceClass' => 'Example', ]); - $this->assertSame('Muffin\Webservice\Test\test_app\Model\Resource\Example', $endpoint->getResourceClass()); + $this->assertSame('TestApp\Model\Resource\Example', $endpoint->getResourceClass()); } - /** - * @expectedException \Muffin\Webservice\Exception\MissingResourceClassException - */ public function testSetResourceMissingClass() { + $this->expectException(MissingResourceClassException::class); + new Endpoint([ 'name' => 'example', 'resourceClass' => 'Missing', @@ -433,24 +443,6 @@ public function testHasField() $this->assertTrue($this->endpoint->hasField('title')); } - /** - * Fake an incorrect return of the schema to check the exception - * - * @expectedException \Muffin\Webservice\Exception\UnexpectedDriverException - */ - public function testGetPrimaryKeyException() - { - $endpoint = $this->getMockBuilder(Endpoint::class) - ->setMethods(['getSchema']) - ->getMock(); - - $endpoint->expects($this->once()) - ->method('getSchema') - ->willReturn(false); - - $endpoint->getPrimaryKey(); - } - public function testSetWebservice() { $testWebservice = new TestWebservice(); @@ -460,34 +452,16 @@ public function testSetWebservice() $this->assertInstanceOf(WebserviceInterface::class, $this->endpoint->getWebservice()); } - /** - * @expectedException \Muffin\Webservice\Exception\UnexpectedDriverException - */ - public function testSetWebserviceException() - { - $endpoint = $this->getMockBuilder(Endpoint::class) - ->setMethods(['getConnection']) - ->getMock(); - - $endpoint->expects($this->once()) - ->method('getConnection') - ->willReturn(false); - - $testWebservice = new TestWebservice(); - $endpoint->setWebservice('test', $testWebservice); - } - public function testHasFinder() { $this->assertTrue($this->endpoint->hasFinder('Examples')); $this->assertFalse($this->endpoint->hasFinder('Missing')); } - /** - * @expectedException \BadMethodCallException - */ public function testCallMissingFinder() { + $this->expectException(BadMethodCallException::class); + $query = $this->getMockBuilder(Query::class) ->setConstructorArgs([new TestWebservice(), $this->endpoint]) ->getMock(); @@ -498,8 +472,8 @@ public function testCallMissingFinder() public function testDebugInfo() { $expected = [ - 'registryAlias' => null, - 'alias' => null, + 'registryAlias' => 'test', + 'alias' => 'test', 'endpoint' => 'test', 'resourceClass' => 'Muffin\\Webservice\\Model\\Resource', 'defaultConnection' => 'test_app', @@ -515,6 +489,6 @@ public function testGetResourceWithCustomResource() { $endpoint = new ExampleEndpoint(); - $this->assertEquals('Muffin\Webservice\Test\test_app\Model\Resource\Example', $endpoint->getResourceClass()); + $this->assertEquals('TestApp\Model\Resource\Example', $endpoint->getResourceClass()); } } diff --git a/tests/TestCase/Model/ResourceTest.php b/tests/TestCase/Model/ResourceTest.php index e3e4264..7a9e888 100644 --- a/tests/TestCase/Model/ResourceTest.php +++ b/tests/TestCase/Model/ResourceTest.php @@ -1,14 +1,13 @@ $endpoint, + 'source' => 'TestEndPoint', ]); - $this->assertEquals($endpoint, $resource->getSource()); + $this->assertEquals('TestEndPoint', $resource->getSource()); } public function testConstructUseSettersOff() diff --git a/tests/TestCase/QueryTest.php b/tests/TestCase/QueryTest.php index 0372239..6fa31cc 100644 --- a/tests/TestCase/QueryTest.php +++ b/tests/TestCase/QueryTest.php @@ -1,18 +1,19 @@ assertNull($this->query->action()); + $this->assertNull($this->query->clause('action')); $this->assertEquals($this->query, $this->query->action(Query::ACTION_READ)); - $this->assertEquals(Query::ACTION_READ, $this->query->action()); + $this->assertEquals(Query::ACTION_READ, $this->query->clause('action')); } public function testActionMethods() { $this->assertEquals($this->query, $this->query->create()); - $this->assertEquals(Query::ACTION_CREATE, $this->query->action()); + $this->assertEquals(Query::ACTION_CREATE, $this->query->clause('action')); $this->assertEquals($this->query, $this->query->read()); - $this->assertEquals(Query::ACTION_READ, $this->query->action()); + $this->assertEquals(Query::ACTION_READ, $this->query->clause('action')); $this->assertEquals($this->query, $this->query->update()); - $this->assertEquals(Query::ACTION_UPDATE, $this->query->action()); + $this->assertEquals(Query::ACTION_UPDATE, $this->query->clause('action')); $this->assertEquals($this->query, $this->query->delete()); - $this->assertEquals(Query::ACTION_DELETE, $this->query->action()); + $this->assertEquals(Query::ACTION_DELETE, $this->query->clause('action')); } public function testAliasField() @@ -86,8 +87,8 @@ public function testApplyOptions() ], 'customOption' => 'value', ])); - $this->assertEquals(1, $this->query->page()); - $this->assertEquals(2, $this->query->limit()); + $this->assertEquals(1, $this->query->clause('page')); + $this->assertEquals(2, $this->query->clause('limit')); $this->assertEquals([ 'field' => 'ASC', ], $this->query->clause('order')); @@ -98,21 +99,20 @@ public function testApplyOptions() public function testFind() { - $this->query->endpoint()->setPrimaryKey('id'); - $this->query->endpoint()->setDisplayField('title'); + $this->query->getEndpoint()->setPrimaryKey('id'); + $this->query->getEndpoint()->setDisplayField('title'); $this->assertEquals($this->query, $this->query->find('list')); $debugInfo = $this->query->__debugInfo(); - $this->assertInternalType('callable', $debugInfo['formatters'][0]); + $this->assertIsCallable($debugInfo['formatters'][0]); } - /** - * @expectedException \UnexpectedValueException - */ public function testSetInvalidAction() { + $this->expectException(UnexpectedValueException::class); + $this->query->read(); $this->query->set([]); @@ -166,11 +166,12 @@ public function testOrder() public function testExecuteTwice() { $mockWebservice = $this - ->getMockBuilder('\Muffin\Webservice\Test\test_app\Webservice\StaticWebservice') - ->setMethods([ + ->getMockBuilder('\TestApp\Webservice\StaticWebservice') + ->onlyMethods([ 'execute', ]) ->getMock(); + $mockWebservice->expects($this->once()) ->method('execute') ->will($this->returnValue(new ResultSet([ @@ -187,7 +188,10 @@ public function testExecuteTwice() 'title' => 'Webservices', ]), ], 3))); - $this->query->webservice($mockWebservice); + + $this->query + ->setWebservice($mockWebservice) + ->action(Query::ACTION_READ); $this->query->execute(); @@ -265,13 +269,13 @@ public function testSelectWithString() public function testSelectWithExpression() { - $exp = new Comparison('upvotes', 50, 'integer', '>='); + $exp = new ComparisonExpression('upvotes', 50, 'integer', '>='); $this->query->select($exp); - /** @var Comparison $comparisonClause */ + /** @var ComparisonExpression $comparisonClause */ $comparisonClause = $this->query->clause('select')[0]; - $this->assertInstanceOf(Comparison::class, $comparisonClause); + $this->assertInstanceOf(ComparisonExpression::class, $comparisonClause); $this->assertEquals(50, $comparisonClause->getValue()); $this->assertEquals('>=', $comparisonClause->getOperator()); } @@ -291,7 +295,7 @@ public function testSelectWithCallable() /** * @inheritDoc */ - public function tearDown() + public function tearDown(): void { parent::tearDown(); diff --git a/tests/TestCase/ResultSetTest.php b/tests/TestCase/ResultSetTest.php index aaa46ee..eb72e53 100644 --- a/tests/TestCase/ResultSetTest.php +++ b/tests/TestCase/ResultSetTest.php @@ -1,14 +1,14 @@ assertInternalType('string', serialize($this->resultSet)); + $this->assertIsString(serialize($this->resultSet)); } public function testUnserialize() { $unserialized = unserialize(serialize($this->resultSet)); - $this->assertInstanceOf('\Muffin\Webservice\ResultSet', $unserialized); + $this->assertInstanceOf(ResultSet::class, $unserialized); } /** * @inheritDoc */ - public function tearDown() + public function tearDown(): void { parent::tearDown(); diff --git a/tests/TestCase/Webservice/WebserviceTest.php b/tests/TestCase/Webservice/WebserviceTest.php index d81ee1b..8dca7d7 100644 --- a/tests/TestCase/Webservice/WebserviceTest.php +++ b/tests/TestCase/Webservice/WebserviceTest.php @@ -1,16 +1,18 @@ assertEquals('/articles/16-10-2015', $this->webservice->nestedResource([ 'date' => '16-10-2015', ])); - $this->assertFalse($this->webservice->nestedResource([ + $this->assertNull($this->webservice->nestedResource([ 'title' => 'hello', ])); } - /** - * @expectedException \UnexpectedValueException - * @expectedExceptionMessage No driver has been defined - */ - public function testExecuteWithoutDriver() - { - $webservice = new TestWebservice(); - - $query = new Query($webservice, new Endpoint()); - - $webservice->execute($query); - } - public function testExecuteLoggingWithLogger() { $logger = $this->getMockBuilder('Cake\Log\Engine\ConsoleLog') - ->setMethods([ + ->onlyMethods([ 'debug', ]) ->getMock(); @@ -105,7 +94,7 @@ public function testExecuteLoggingWithLogger() public function testExecuteLoggingWithLoggerEnabled() { $logger = $this->getMockBuilder('Cake\Log\Engine\ConsoleLog') - ->setMethods([ + ->onlyMethods([ 'debug', ]) ->getMock(); @@ -121,48 +110,44 @@ public function testExecuteLoggingWithLoggerEnabled() $this->webservice->execute($query); } - /** - * @expectedException \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException - * @expectedExceptionMessage Webservice Muffin\Webservice\Test\test_app\Webservice\TestWebservice does not implement _executeCreateQuery - */ public function testExecuteWithoutCreate() { + $this->expectException(UnimplementedWebserviceMethodException::class); + $this->expectExceptionMessage('Webservice TestApp\Webservice\TestWebservice does not implement _executeCreateQuery'); + $query = new Query($this->webservice, new Endpoint()); $query->create(); $this->webservice->execute($query); } - /** - * @expectedException \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException - * @expectedExceptionMessage Webservice Muffin\Webservice\Test\test_app\Webservice\TestWebservice does not implement _executeReadQuery - */ public function testExecuteWithoutRead() { + $this->expectException(UnimplementedWebserviceMethodException::class); + $this->expectExceptionMessage('Webservice TestApp\Webservice\TestWebservice does not implement _executeReadQuery'); + $query = new Query($this->webservice, new Endpoint()); $query->read(); $this->webservice->execute($query); } - /** - * @expectedException \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException - * @expectedExceptionMessage Webservice Muffin\Webservice\Test\test_app\Webservice\TestWebservice does not implement _executeUpdateQuery - */ public function testExecuteWithoutUpdate() { + $this->expectException(UnimplementedWebserviceMethodException::class); + $this->expectExceptionMessage('Webservice TestApp\Webservice\TestWebservice does not implement _executeUpdateQuery'); + $query = new Query($this->webservice, new Endpoint()); $query->update(); $this->webservice->execute($query); } - /** - * @expectedException \Muffin\Webservice\Exception\UnimplementedWebserviceMethodException - * @expectedExceptionMessage Webservice Muffin\Webservice\Test\test_app\Webservice\TestWebservice does not implement _executeDeleteQuery - */ public function testExecuteWithoutDelete() { + $this->expectException(UnimplementedWebserviceMethodException::class); + $this->expectExceptionMessage('Webservice TestApp\Webservice\TestWebservice does not implement _executeDeleteQuery'); + $query = new Query($this->webservice, new Endpoint()); $query->delete(); @@ -171,7 +156,7 @@ public function testExecuteWithoutDelete() public function testCreateResource() { - /* @var \Muffin\Webservice\Model\Resource $resource */ + /** @var \Muffin\Webservice\Model\Resource $resource */ $resource = $this->webservice->createResource('\Muffin\Webservice\Model\Resource', []); $this->assertInstanceOf('\Muffin\Webservice\Model\Resource', $resource); @@ -199,7 +184,7 @@ public function testTransformResults() ], ]); - $this->assertInternalType('array', $resources); + $this->assertIsArray($resources); $this->assertInstanceOf('\Muffin\Webservice\Model\Resource', $resources[0]); } @@ -214,17 +199,18 @@ public function testDescribe() public function testDebugInfo() { - $this->assertEquals([ + $expected = [ 'driver' => $this->webservice->getDriver(), - 'endpoint' => $this->webservice->getEndpoint(), - ], $this->webservice->__debugInfo()); + 'endpoint' => null, + ]; + + $this->assertEquals($expected, $this->webservice->__debugInfo()); } - /** - * @expectedException \Muffin\Webservice\Exception\MissingEndpointSchemaException - */ public function testDescribeException() { + $this->expectException(MissingEndpointSchemaException::class); + $this->webservice->describe('example'); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7affa7d..76891ea 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,6 @@ register(); - -$loader->addNamespace('TestApp', APP); -$loader->addNamespace('SomeVendor\SomePlugin', APP . 'plugins' . DS . 'SomeVendor' . DS . 'SomePlugin' . DS . 'src'); -$loader->addNamespace('TestPlugin', APP . 'plugins' . DS . 'TestPlugin' . DS . 'src'); - require_once CORE_PATH . 'config/bootstrap.php'; date_default_timezone_set('UTC'); @@ -47,7 +44,7 @@ Configure::write('debug', true); Configure::write('App', [ - 'namespace' => 'Muffin\\Webservice\\Test\\test_app', + 'namespace' => 'TestApp', 'encoding' => 'UTF-8', 'base' => false, 'baseUrl' => false, @@ -89,8 +86,10 @@ putenv('DB_DSN=sqlite:///:memory:'); } -ConnectionManager::setConfig('test', ['url' => getenv('DB_DSN')]); -ConnectionManager::setConfig('test_webservice', ['url' => getenv('DB_DSN')]); +ConnectionManager::setConfig('test', [ + 'className' => Connection::class, + 'driver' => TestDriver::class, +] + ConnectionManager::parseDsn(env('DB_DSN'))); Log::setConfig([ 'debug' => [ @@ -104,8 +103,3 @@ 'file' => 'error', ], ]); - -Plugin::getCollection()->add(new \Muffin\Webservice\Plugin()); -require Plugin::getCollection()->get('Muffin/Webservice')->getConfigPath() . 'bootstrap.php'; - -loadPHPUnitAliases(); diff --git a/tests/test_app/Webservice/Driver/Test.php b/tests/test_app/Webservice/Driver/Test.php deleted file mode 100644 index ce35fa7..0000000 --- a/tests/test_app/Webservice/Driver/Test.php +++ /dev/null @@ -1,27 +0,0 @@ -_createResource($resourceClass, $properties); diff --git a/tests/test_app/plugins/TestPlugin/src/Webservice/Driver/TestPlugin.php b/tests/test_app/plugins/TestPlugin/src/Webservice/Driver/TestPlugin.php index d5350ca..6d3fb1b 100644 --- a/tests/test_app/plugins/TestPlugin/src/Webservice/Driver/TestPlugin.php +++ b/tests/test_app/plugins/TestPlugin/src/Webservice/Driver/TestPlugin.php @@ -1,18 +1,18 @@ _createResource($resourceClass, $properties); diff --git a/tests/test_app/Model/Endpoint/AppEndpoint.php b/tests/test_app/src/Model/Endpoint/AppEndpoint.php similarity index 59% rename from tests/test_app/Model/Endpoint/AppEndpoint.php rename to tests/test_app/src/Model/Endpoint/AppEndpoint.php index e7e0581..601d1d5 100644 --- a/tests/test_app/Model/Endpoint/AppEndpoint.php +++ b/tests/test_app/src/Model/Endpoint/AppEndpoint.php @@ -1,10 +1,10 @@ addColumn('id', [ 'type' => 'int', diff --git a/tests/test_app/Model/Endpoint/TestEndpoint.php b/tests/test_app/src/Model/Endpoint/TestEndpoint.php similarity index 79% rename from tests/test_app/Model/Endpoint/TestEndpoint.php rename to tests/test_app/src/Model/Endpoint/TestEndpoint.php index 86f03fd..f48fbcd 100644 --- a/tests/test_app/Model/Endpoint/TestEndpoint.php +++ b/tests/test_app/src/Model/Endpoint/TestEndpoint.php @@ -1,28 +1,27 @@ requirePresence('title') - ->notEmpty('title') + ->notEmptyString('title') ->requirePresence('body') - ->notEmpty('body') + ->notEmptyString('body') ->minLength('body', 5, 'Must be 5 characters or longer'); return $validator; diff --git a/tests/test_app/Model/Resource/Example.php b/tests/test_app/src/Model/Resource/Example.php similarity index 58% rename from tests/test_app/Model/Resource/Example.php rename to tests/test_app/src/Model/Resource/Example.php index 3bd4b82..1dd5b6e 100644 --- a/tests/test_app/Model/Resource/Example.php +++ b/tests/test_app/src/Model/Resource/Example.php @@ -1,10 +1,10 @@ resources[$index], ], 1); } - if (isset($query->where()[$query->endpoint()->aliasField('title')])) { + if (isset($query->where()[$query->getEndpoint()->aliasField('title')])) { $resources = []; foreach ($this->resources as $resource) { - if ($resource->title !== $query->where()[$query->endpoint()->aliasField('title')]) { + if ($resource->title !== $query->where()[$query->getEndpoint()->aliasField('title')]) { continue; } @@ -109,7 +108,7 @@ protected function _executeDeleteQuery(Query $query, array $options = []) unset($this->resources[$this->conditionsToIndex($conditions)]); - return ($exists) ? 1 : 0; + return $exists ? 1 : 0; } elseif (is_array($conditions['id'])) { $deleted = 0; diff --git a/tests/test_app/Webservice/Logger.php b/tests/test_app/src/Webservice/Logger.php similarity index 95% rename from tests/test_app/Webservice/Logger.php rename to tests/test_app/src/Webservice/Logger.php index 5b4dedd..36e0c55 100644 --- a/tests/test_app/Webservice/Logger.php +++ b/tests/test_app/src/Webservice/Logger.php @@ -1,6 +1,7 @@ [ diff --git a/tests/test_app/Webservice/TestWebservice.php b/tests/test_app/src/Webservice/TestWebservice.php similarity index 88% rename from tests/test_app/Webservice/TestWebservice.php rename to tests/test_app/src/Webservice/TestWebservice.php index 331ad64..c4e8e59 100644 --- a/tests/test_app/Webservice/TestWebservice.php +++ b/tests/test_app/src/Webservice/TestWebservice.php @@ -1,13 +1,13 @@ _createResource($resourceClass, $properties);