diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index c47da26..00bbcc3 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -14,24 +14,29 @@ jobs: strategy: matrix: - php-versions: ['7.4'] + php: ['8.1', '8.2', '8.3'] + symfony: ['6.4', '7.1'] + exclude: + - php: '8.1' + symfony: '7.1' runs-on: ubuntu-latest + name: On PHP ${{ matrix.php }} and Symfony ${{ matrix.symfony }} + steps: + # https://github.com/marketplace/actions/checkout - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 # https://github.com/marketplace/actions/setup-php-action - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-versions }} + php-version: ${{ matrix.php }} extensions: mbstring, intl ini-values: post_max_size=256M, max_execution_time=180 - - uses: actions/checkout@v2 - - name: Check PHP version run: php -v @@ -41,8 +46,18 @@ jobs: - name: Install dependencies run: composer install --prefer-dist --no-progress - - name: Code lint PHP files - run: ./vendor/bin/phplint + - name: Install Symfony ${{ matrix.symfony }} packages + run: | + composer update symfony/http-client:${{ matrix.symfony }} + composer update symfony/cache:${{ matrix.symfony }} + composer update symfony/stopwatch:${{ matrix.symfony }} + composer update symfony/property-access:${{ matrix.symfony }} + + - name: Lint PHP files + run: | + curl -Ls https://github.com/overtrue/phplint/releases/latest/download/phplint.phar -o /usr/local/bin/phplint + chmod +x /usr/local/bin/phplint + /usr/local/bin/phplint --no-cache --no-progress -v - name: Coding standards run: ./vendor/bin/phpcs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..be03104 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,20 @@ +name: release-please + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + # Uses https://github.com/marketplace/actions/release-please-action + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + release-type: php + package-name: strata/data diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd9240..439d10b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.8.0] - 2021-TBC +## [0.9.0] - TBC + +### Changed +- Minimum requirements PHP 8.1 + +## [0.8.0] - 2023-02-10 ### Added - Refactored [Frontend](https://github.com/strata/frontend) data layer into its own repo - Flexible data access layer for accessing data, transforming, and mapping from HTTP APIs - Added support for GraphQL, Rest and HTTP data providers - Added query manager and query objects to help sending data requests and structure returned data -- \ No newline at end of file diff --git a/README.md b/README.md index 0b84e82..e26b125 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,28 @@ # Data -The purpose of this package is to help read data from APIs and other external sources. +A simple way to manage data retrieval from APIs and other sources. This package is built using Symfony components and can +be used with any PHP application, Symfony, Laravel or plain PHP. -Features: +You can: -* Download data via HTTP APIs -* Throw exceptions on failed requests -* Supports REST and GraphQL -* Decode data from a variety of formats (e.g. JSON, Markdown) -* Automated debug information (logging and time profiling) +* Read data from REST and GraphQL APIs +* Authenticate with APIs +* Handle errors consistently * Cache requests to increase performance -* Tools to help detect whether data has changed since last request -* Transform data (e.g. convert source category to match local category name) +* Decode data from a variety of formats (e.g. JSON, Markdown) +* Transform data (e.g. map a category name) +* Work out if data has changed since the last request -Planned for the future: +See the [documentation](docs/README.md) for more. -* Validate data items to see whether they contain required data properties -* Efficient bulk API queries via concurrent requests -* Download data via local, FTP, S3 filesystem (via Flysystem) +You can use this with the [frontend](https://github.com/strata/frontend) package to help you build a frontend website. ## Status Please note this software is in development, usage may change before the 1.0 release. ## Requirements -* PHP 7.4+ +* PHP 8.1+ * [Composer](https://getcomposer.org/) ## Installation @@ -32,13 +30,10 @@ Please note this software is in development, usage may change before the 1.0 rel Install via Composer: ``` -composer require strata/data:^0.8 +composer require strata/data:^0.9 ``` -## Documentation - -See [docs](docs/README.md) - these are currently being cleaned up and we plan to publish better docs for v0.10. - ## Thanks to -https://developer.happyr.com/http-client-and-caching +* [Symfony](https://symfony.com/) +* https://developer.happyr.com/http-client-and-caching diff --git a/composer.json b/composer.json index 357d7ee..951ce97 100755 --- a/composer.json +++ b/composer.json @@ -6,23 +6,20 @@ "authors": [ { "name": "Simon Jones", - "email": "simon@studio24.net", - "homepage": "https://studio24.net/", - "role": "Developer" + "email": "simon@studio24.net" } ], "require": { - "php": "^7.4|^8.0", - "ext-json": "*", - "symfony/http-client": "^5.4|^6.0", + "php": "^8.1", + "erusev/parsedown-extra": "^0.8", + "laminas/laminas-feed": "^2.22", + "league/commonmark": "^2.4", "spatie/yaml-front-matter": "^2.0", - "erusev/parsedown-extra": "^0.8.1", - "symfony/cache": "^5.4|^6.0", - "symfony/stopwatch": "^5.4|^6.0", - "symfony/monolog-bundle": "^3.7", - "symfony/property-access": "^5.4|^6.0", - "laminas/laminas-feed": "^2.16", - "league/commonmark": "^2.2" + "symfony/http-client": "^6.4|^7.1", + "symfony/cache": "^6.4|^7.1", + "symfony/stopwatch": "^6.4|^7.1", + "symfony/property-access": "^6.4|^7.1", + "symfony/monolog-bundle": "^3.7" }, "autoload": { "psr-4": { @@ -31,39 +28,37 @@ "exclude-from-classmap": ["/tests/"] }, "require-dev": { - "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5", - "phpstan/phpstan": "^1.0", - "overtrue/phplint": "^3.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.10", + "phpstan/phpstan": "^1.11", "roave/security-advisories": "dev-latest" }, "scripts": { - "lint": [ - "./vendor/bin/phplint" - ], - "cs": [ + "phpcs": [ "./vendor/bin/phpcs" ], - "fix": [ + "phpcbf": [ "./vendor/bin/phpcbf" ], - "unit": [ + "phpunit": [ "./vendor/bin/phpunit" ], + "phpstan": [ + "./vendor/bin/phpstan analyse --memory-limit 512M" + ], "test": [ - "composer lint", - "composer cs", - "composer unit" + "composer phpcs", + "composer phpunit" ] }, - "scripts-descriptions": { - "lint": "Lint PHP files", - "cs": "Test coding standards are met in PHP code", - "fix": "Fix PHP code so it meets coding standards", - "unit": "Run PHPUnit tests", - "test": "Run all tests (phplint, phpcs, phpunit)" - }, "config": { "allow-plugins": false + }, + + "extra": { + "symfony": { + "allow-contrib": false, + "require": "7.2.*" + } } } diff --git a/docs/README.md b/docs/README.md index c694dd8..746a2d1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,47 @@ # Introduction -_Please note: documentation is in progress._ +A simple way to manage data retrieval from APIs and other sources. This package is built using Symfony components and can +be used with any PHP application, Symfony, Laravel or plain PHP. + +You can use this with the [frontend](https://github.com/strata/frontend) package to help you build a frontend website. + +You can: + +* Read data from REST and GraphQL APIs +* Authenticate with APIs +* Handle errors consistently +* Cache requests to increase performance +* Decode data from a variety of formats (e.g. JSON, Markdown) +* Transform data (e.g. map a category name) +* Work out if data has changed since the last request + +## Changelog + +All notable changes to strata/data are documented on [GitHub](https://github.com/strata/data/blob/main/CHANGELOG.md). + +## Retrieving data + +Strata Data has a lightweight architecture. + +Data is retrieved via a **[Data provider](retrieving-data/data-providers.md)** . This could be a REST API, GraphQL API, or other source. +Data providers wrap up data reading functionality along with support for **[caching](advanced-usage/caching.md)**, decoding raw data, error handling and helpers to make development easier. + +You use **[queries](retrieving-data/query.md)** to make running a data request easier. +A **[query manager](retrieving-data/query-manager.md)** can be used to manage multiple queries. + +Single data is returned as either an object or array. + +A collection of data is returned as a collection object, containing either objects or arrays. + +## Changing data + +Returned data can be modified via **[transformers](changing-data/transformers.md)** or **[mappers](changing-data/mapping.md)**. Transformers change data, while mappers map data to an object or array. + +**[Pagination](changing-data/mapping#setting-pagination)** can be automated when you return a collection of results. + +## Advanced usage + +You can [validate data](advanced-usage/validating.md) to check it is valid. This is useful if you need to check data before you use it (e.g. a data import). + +[Data history](data-history.md) can be used to help determine if retrieved data has changed since last access. -Read and write data from external data providers in a standardised format. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4c3164d..73e1272 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,18 +1,14 @@ # Table of contents * [Introduction](README.md) -* [Principles](principles.md) - -## Usage - -* [Installation](usage/installation.md) -* [Making a request](usage/making-requests.md) -* [Caching](usage/caching.md) -* [Validation](usage/validating.md) +* [Getting started](getting-started.md) +* [About](about.md) ## Retrieving data -* [Intro](retrieving-data/README.md) +* [Introduction](retrieving-data/README.md) +* [Making a request](retrieving-data/making-requests.md) +* [Property paths](retrieving-data/property-paths.md) * [Data providers](retrieving-data/data-providers.md) * [HTTP data provider](retrieving-data/http.md) * [GraphQL data provider](retrieving-data/graphql.md) @@ -20,9 +16,11 @@ * [GraphQL queries](retrieving-data/graphql.md) * [Query Manager](retrieving-data/query-manager.md) * [Custom query classes](retrieving-data/custom-query-classes.md) +* [Bulk queries](retrieving-data/bulk-queries.md) ## Changing data +* [Introduction](changing-data/README.md) * [Transforming and mapping data](changing-data/changing-data.md) * [Accessing properties](changing-data/property-paths.md) * [Transforming data](changing-data/transformers.md) @@ -31,6 +29,8 @@ ## Advanced usage +* [Validation](advanced-usage/validating.md) +* [Caching](advanced-usage/caching.md) * [Data History](advanced-usage/data-history.md) * [Events](advanced-usage/events.md) * [Testing API requests](advanced-usage/testing-api-requests.md) diff --git a/docs/about.md b/docs/about.md new file mode 100644 index 0000000..6b7e26a --- /dev/null +++ b/docs/about.md @@ -0,0 +1,6 @@ +# About + +Strata is developed by [Studio 24](https://www.studio24.net/), a digital design and development agency +based in Cambridge, UK, focussed on building accessible, sustainable websites and web apps that work for everyone. + +You can [get in touch](https://www.studio24.net/contact/) about how we can help on your PHP web application project. diff --git a/docs/caching.md b/docs/advanced-usage/caching.md similarity index 98% rename from docs/caching.md rename to docs/advanced-usage/caching.md index 56758ee..00d1a33 100644 --- a/docs/caching.md +++ b/docs/advanced-usage/caching.md @@ -42,7 +42,7 @@ On data providers: ### Query Manager caching -The [query manager](retrieving-data/query-manager.md) uses similar techniques to control caching: +The [query manager](../retrieving-data/query-manager.md) uses similar techniques to control caching: On the query manager: diff --git a/docs/advanced-usage/testing-api-requests.md b/docs/advanced-usage/testing-api-requests.md index 04fc8a8..14e3976 100644 --- a/docs/advanced-usage/testing-api-requests.md +++ b/docs/advanced-usage/testing-api-requests.md @@ -1,8 +1,8 @@ # Testing API requests -When testing HTTP requests you need to create mock responses based on what would actually be returned from a real HTTP request. Symfony's HTTPClient has support for [testing HTTP requests](https://symfony.com/doc/current/components/http_client.html#testing-http-clients-and-responses). +When testing HTTP requests you need to create mock responses based on what would actually be returned from a real HTTP request. Symfony's HTTPClient has support for [testing HTTP requests](https://symfony.com/doc/current/http_client.html#http-client-and-responses). -You can also use `MockResponseFromFile` to generate a mock response easily from a file. +You can also use the`MockResponseFromFile` class to generate a mock response easily from a file. ## MockResponseFromFile @@ -16,7 +16,7 @@ Allows you to load a mock request from file. Body file is loaded from `{$filename}` -The optional info file is loaded from `{$filename}.info.php` and must contain the `$info` variable \(array\). By default mock responses return a 200 status code which you can change by setting the `$info` array. +The optional info file is loaded from `{$filename}.info.php` and must contain the `$info` variable (array). By default mock responses return a 200 status code which you can change by setting the `$info` array. ### Usage diff --git a/docs/validating.md b/docs/advanced-usage/validating.md similarity index 97% rename from docs/validating.md rename to docs/advanced-usage/validating.md index 370e2a5..a392692 100644 --- a/docs/validating.md +++ b/docs/advanced-usage/validating.md @@ -27,7 +27,7 @@ A simple validation rules system exists, inspired by [Laravel's Validation](http Simply create a new instance of `Strata\Data\Validate\ValidationRules` and pass in the validation requirements in the constructor. This is an array made up of the property path to the field you want to validate and the validation rule. -See [how to write property paths](property-paths.md). +See [how to write property paths](../retrieving-data/property-paths.md). The rules format is: diff --git a/docs/changing-data/README.md b/docs/changing-data/README.md index 45b28d3..3f95b74 100644 --- a/docs/changing-data/README.md +++ b/docs/changing-data/README.md @@ -19,5 +19,5 @@ Example use cases are: * Map a single item to an object (and optionally type set item fields, e.g. to an DateTime object) * Map a collection of items to a set of objects -We use Symfony's PropertyAccess component to help read and write data. See [how to write property paths](../property-paths.md) +We use Symfony's PropertyAccess component to help read and write data. See [how to write property paths](../retrieving-data/property-paths.md) for more details. \ No newline at end of file diff --git a/docs/data-history.md b/docs/data-history.md deleted file mode 100644 index f65af54..0000000 --- a/docs/data-history.md +++ /dev/null @@ -1,212 +0,0 @@ -# Data history - -You can make use of Data History to determine if data you are fetching from an external data provider has changed since the -last time you fetched it. This is helpful to avoid unnecessary processing for data import jobs. - -## How it works -As you read in data from a data provider, add this to the history log with a key representing a unique identifier along -with the data representing the raw data/content. - -You can then run tests to check whether: -* Is the data new (there are no previous history logs) -* Is the data changed (the current fetched data is different to the last item in the history log) - -You can also store additional metadata in the history log which can be retrieved. - -The Data History is stored in a PSR-6 compatible cache. Rather than store raw data, the system stores a unique hash of -the raw data/content. This is used to efficiently check whether content has changed. - -## Setup - -```php -use Strata\Data\Cache\DataHistory; - -$history = new DataHistory($cache, $cacheLifetime); -``` - -Pass a PSR-6 compatible cache adapater, see [supported adapters](https://symfony.com/doc/current/components/cache/cache_pools.html). -The example below uses the filesystem adapter. Please note, setting a cache lifetime on the adapter has -no effect since this is overwritten in the DataHistory class. - -```php -$history = new DataHistory(new FilesystemAdapter('history', 0, __DIR__ . '/path/to/cache/folder')); -``` - -### Cache lifetime -You can pass a second argument to set the cache lifetime of history log items (one log item is stored per unique identifier). -E.g. - -```php -$history = new DataHistory(new FilesystemAdapter('history', 0, __DIR__ . '/path/to/cache/folder'), CacheLifetime::YEAR); -``` - -The system stores one cache entry per unique identifier. The cache system itself stores files for up to the cache lifetime, -which by default is set to two months. It's important the cache lifetime is higher than the max history days. - -You can also set the cache lifetime via `setCacheLifetime($lifetime)`: - -```php -$history->setCacheLifetime(CacheLifetime::YEAR); -``` - -The `CacheLifetime` class has a set of convenience constants to set cache lifetime in seconds: `CacheLifetime::MINUTE`, -`CacheLifetime::HOUR`, `CacheLifetime::DAY`, `CacheLifetime::WEEK`, `CacheLifetime::MONTH`, `CacheLifetime::YEAR`. - -### Max history days -For each unique piece of data multiple history logs are kept, representing the last time a piece of data was retrieved from -a external data source. These logs are pruned every so often, to keep storage efficient. By default up to 30 days are kept, -which is OK for most purposes. - -If your schedule to import data is longer than every 30 days you'll need to set a longer max history via `setMaxHistoryDays()`. For -example, to set the max history days to 60: - -```php -$history->setMaxHistoryDays(60); -``` - -## Testing your data is new or changed - -First fetch your data from your external data provider. Then you can test whether this is new via: - -```php -if ($history->isNew('unique_key', 'raw data')) { - // do something -} -``` - -This returns true if no history log items exist and this is considered new data. - -You can test whether your data is changed via: - -```php -if ($history->isChanged('unique_key', 'raw data')) { - // do something -} -``` - -This returns true if the data is different to the last entry in the data history log, or if no history items exist. - -You can also use this reverse test, to check whether data is identical to the last entry in the history log: - -```php -if ($history->isIdentical('unique_key', 'raw data')) { - // do something -} -``` - -## Adding a new history log - -To add items to the history log you first need to add it: - -```php -$history->add('unique_key', 'raw data'); -``` - -For performance, this does not immediately save to the cache since it's likely you'll be processing lots of data. At the -end of your data processing make sure you save the Data History to the cache via: - -```php -$history->commit(); -``` - -If you forget to run this command, then the Data History items are not saved. - -### Only save a new log item when the data has changed -It's important to only save new history log items if data changes, otherwise the `isChanged()` test will fail on future -requests. - -### Complete example - -A complete example of checking and storing history log data: - -```php -if ($history->isChanged('unique_key', 'raw data')) { - // Process new data - // ... - - $history->add('unique_key', 'raw data'); -} -``` - -Or an alternative pattern, where you skip data that has not changed: - -```php -foreach ($lotsOfData as $item) { - if ($history->isIdentical($item['id'], $item['data'])) { - // skip data, the example here is a continue to move to the next item in a loop - continue; - } - - // Process new data - // ... - $history->add('unique_key', 'raw data'); -} -``` - -And remember to save the history cache at the end of data processing! - -```php -$history->commit(); -``` - -### Metadata - -You can add metadata to the history log, which is an array of key, value pairs. For example: - -```php -$history->add('unique_key', 'raw data', ['my_field' => 'my value']); -``` - -## Getting the last saved item - -You can retrieve the last saved item in the history log via: - -```php -$item = $history->getLastItem('unique_key'); -``` - -This is an array containing three keys: `'updated'`, `'content_hash'`, `'metadata'`. If there are no results, `null` is returned. - -You can return a specific field by passing this as the third argument. For example, to return metadata: - -```php -$metadata = $history->getLastItem('unique_key', 'metadata'); -``` - -### Get all items - -If you wish, you can return an array of all history logs for an item via: - -```php -$items = $history->getAll('unique_key'); -``` - -## Efficient data processing - -By using the two above tests you can improve performance of data importing. However, given we store Data History in a cache -you should not depend on these tests always working. If you have a data import process where data only changes once every -six months then with a max history days of 30 days, the data import process will think data is new or changed at least every -30 days. - -It is recommended your data import process is robust and works whether data is changed or not. The Data History system -should be used improve performance - rather than be a substitute for knowing absolutely if data is new or changed. -If your data import system has a critical dependency on knowing if data is changed or new, you will need to test -this by looking up data in your data storage system (e.g. database). - -In this instance you can make use of the same hash system to store content hash representations of data, which are more -efficient to compare than full raw data. - -### Content hasher -To create a content hash, simply pass raw data (array or string): - -```php -use Strata\Data\Helper\ContentHasher; - -$hash = ContentHasher::hash($data); -``` - -To check if data has changed, pass the last content hash (string) and the new raw data (array or string): - -```php -$changed = ContentHasher::hasContentChanged($lastContentHash, $newRawData); -``` diff --git a/docs/data-manager.md b/docs/data-manager.md deleted file mode 100644 index fb218a1..0000000 --- a/docs/data-manager.md +++ /dev/null @@ -1,2 +0,0 @@ -# Data manager - diff --git a/docs/events.md b/docs/events.md deleted file mode 100644 index 1b3cb35..0000000 --- a/docs/events.md +++ /dev/null @@ -1,69 +0,0 @@ -# Events - -We use the [Symfony Event system](https://symfony.com/doc/current/components/event_dispatcher.html) to manage events at -certain points when fetching data. This is useful to add adhoc functionality, such as logging. - -The following events are available when accessing data. - -| Event name | Class constant | Description | Available event methods | -| ------------- | ------------- | ------------- | ------------- | -| data.request.start | StartEvent::NAME | Runs at the start of a request | getRequestId(), getUri(), getContext() | -| data.request.success | SuccessEvent::NAME | If a request is successful | getRequestId(), getUri(), getContext(), getException() | -| data.request.failure | FailureEvent::NAME | If a request is considered failed | getRequestId(), getUri(), getContext() | -| data.request.decode | DecodeEvent::NAME | After response has been decoded | getDecodedData(), getRequestId(), getUri(), getContext() | - -## How to add a listener for a single event - -Event listeners are simple callbacks that run when a specific event is dispatched. - -Use the `addListener(string $eventName, callable $listener, int $priority = 0)` method to add a [listener](https://symfony.com/doc/current/components/event_dispatcher.html#connecting-listeners) -to an event. A simple example appears below: - -```php -$api->addListener(StartRequest::NAME, function(StartRequest $event) { - // Get properties from event - $uri = $event->getUri(); - $context = $event->getContext(); - - // Do something at start of data request -}); -``` - -## How to add an event subscriber - -An event subscriber is a class that can listen to multiple events. Since it's in a class it's easier to re-use code. - -You can add an [event subscriber](https://symfony.com/doc/current/components/event_dispatcher.html#using-event-subscribers) -which can listen to multiple events via the `addSubscriber(EventSubscriberInterface $subscriber)` method. - -## Available event subscribers - -### LoggerSubscriber - -Add logging for the data request process. - -```php -use Strata\Data\Http\Rest; -use Strata\Data\Event\Subscriber\LoggerSubscriber; -use Monolog\Logger; - -$api = new Rest(); -$api->addSubscriber(new LoggerSubscriber(new Logger('/path/to/log'))); -``` - -### StopwatchSubscriber - -Add timing profiling for the Symfony Stopwatch profiler (only recommended in development). - -```php -use Strata\Data\Http\Rest; -use Strata\Data\Event\Subscriber\StopwatchSubscriber; -use Symfony\Component\Stopwatch\Stopwatch; - -$api = new Rest(); -$api->addSubscriber(new StopwatchSubscriber(new Stopwatch()); -``` - - - - diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..921a0c0 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,161 @@ +# Getting started + +## Requirements + +* PHP 8.1+ +* [Composer](https://getcomposer.org/) + +## Installation + +Install via Composer: + +``` +composer require strata/data:^0.9 +``` + +## Access data from a REST API + +Set up your API connection, caching all responses for 1 hour (the default lifetime): + +```php +use Strata\Data\Http\Rest; + +$api = new Rest('https://httpbin.org/'); +$api->enableCache(); +``` + +Run a GET query on the `/anything` endpoint and return the JSON decoded response as an array. +The HTTP query is only run when data is accessed. If there's an error an exception is thrown. + +```php +$response = $api->get('anything', ['my-name' => 'Thomas Anderson']); +$data = $api->decode($response); +``` + +Get the raw response content as a string: + +```php +$content = $response->getContent(); +``` + +Find out if the API request was successful: + +```php +if ($response->isSuccessful()) { + echo 'Request was successful'; +} +``` + +Find out if the API response was cached: + +```php +if ($response->isHit()) { + echo sprintf('Data has been cached for %d seconds', $response->getAge()); +} +``` + +## Access data from a GraphQL API + +Set up your API connection, caching all responses for 1 hour: + +```php +use Strata\Data\Http\GraphQL; + +$api = new GraphQL('https://www.example.com/api/'); +$api->enableCache(); +``` + +Run a GraphQL query and return data as an array. If there's an error an exception is thrown. + +```php +$query = <<<'EOD' +query Page($uri: [String], $site: [String]) { + entry(uri: $uri, site: $site) { + id + typeHandle + status + uri + title + language + postDate + content +} +EOD; +$variables = [ + 'site' => 1, + 'uri' => '/about-us', +]; +$response = $api->query($query, $variables); +$data = $api->decode($response); +``` + +## Build API requests with queries + +It can be easier to build a REST API query via a query object. + +```php + +It is easier to use a query object to run API requests. + +```php +use Strata\Data\Query\Query; +use Strata\Data\Http\Rest; + +$api = new Rest('https://www.example.com/api/'); +$api->enableCache(); + +$page = 2; +$query = Query::uri('posts') + ->addParam('page', $page) + ->setCurrentPage($page) + ->setTotalResults('[meta][total_results]') + ->setRootPropertyPath('[data]') + ->setDataProvider($api); +``` + +Return a collection of results, along with pagination. Responses are automatically decoded (each item is returned as an array). + +```php +$posts = $query->getCollection(); + +$pagination = $collection->getPagination(); +$totalResults = $pagination->getTotalResults(); +``` + +## Run multiple API requests via the Data Manager + +You can use a Data Manager to run multiple queries. + +```php +use Strata\Data\Query\QueryManager; +use Strata\Data\Http\Rest; + +$manager = new QueryManager(); + +// The first argument is the data provider name +$manager->addDataProvider('internal_api', new Rest("https://example1.com/api/")); + +// Add a second data provider +$manager->addDataProvider('cms', new Rest("https://example2.com/api/")); + +// Add your first query to run against internal API +$manager->add('content', Query::uri('content')->setParam('id', 24)); + +// Add a second query to run against CMS API +$query = Query::uri('news') + ->setParam('limit', 25); +$manager->add('news', $query, 'cms'); + +// This runs both queries concurrently and returns data for the content query +$data = $manager->get('content'); + +// Return data for the news query +$data = $manager->getCollection('news'); +$pagination = $data->getPagination(); +``` + +## More information + +Find out more: +* TODO +* \ No newline at end of file diff --git a/docs/helpers.md b/docs/helpers.md index 69e3710..043833c 100644 --- a/docs/helpers.md +++ b/docs/helpers.md @@ -4,12 +4,6 @@ A number of helper classes exist in Strata Data. ## ContentHasher - - -## UnionTypes - -Allow type checking of union types for function arguments for PHP 7.4. - ### is The static method `is()` tests whether a value is one of the passed types. @@ -23,16 +17,3 @@ if (UnionTypes::is($value, 'string', 'array')) { } ``` -### assert - -The static method `assert()` tests whether a value is one of the passed types and if not throws an `InvalidArgumentException` exception. -Valid types are: array, callable, bool, float, int, string, iterable, object, or classname. You can check for any number -of types. - -``` -// Throw an exception is $value is not either a string or an array -UnionTypes::assert('propertyName', $value, 'string', 'array'); - -// Throw an exception is $value is not either a string or a DateTime object -UnionTypes::assert($value, 'string', 'DateTime'); -``` diff --git a/docs/img/data-architecture.png b/docs/img/data-architecture.png deleted file mode 100644 index 5ce0429..0000000 Binary files a/docs/img/data-architecture.png and /dev/null differ diff --git a/docs/principles.md b/docs/principles.md deleted file mode 100644 index 756867c..0000000 --- a/docs/principles.md +++ /dev/null @@ -1,39 +0,0 @@ -# Principles - -_Please note: This section needs updating_ - -Strata Data offers a standardised way to read and manipulate data from external sources. Its aim is to make working with data easier. - -## Architecture - -Strata Data has a lightweight architecture. - -![Architecture of Strata Data](.gitbook/assets/data-architecture.png) - -### Data providers - -Data comes from a **Data source**, for example a REST API. - -You read data using a **Data provider**, this wraps up data reading functionality along with support for **caching**, decoding raw data, error handling, events and helpers to make development easier. - -The data provider layer is custom and has methods that make sense for data retrieval. - -**Data history** can be used to help determine if retrieved data has changed since last access. - -Once data is returned from the data provider, it's ready to use. If returned data contains multiple values \(e.g. a JSON array\) then it is expected that data is accessible from the data provider as an array. - -See [data providers](retrieving-data/data-providers.md), [caching](usage/caching.md) and [data history](advanced-usage/data-history.md). - -### Validating data - -You can **validate** data to check it is valid. It is recommended to validate source data before it has been modified, this can help when reporting any errors back to the data provider. - -See [validating data](usage/validating.md). - -### Changing data - -Data can be modified via **transformers** or **mappers**. Transformers change data, while mappers map data to a new array or object. - -Mappers can also build collections along with automated pagination. - -See [changing data](changing-data/changing-data.md). diff --git a/docs/making-requests.md b/docs/retrieving-data/making-requests.md similarity index 90% rename from docs/making-requests.md rename to docs/retrieving-data/making-requests.md index 893a095..7b9e06c 100644 --- a/docs/making-requests.md +++ b/docs/retrieving-data/making-requests.md @@ -2,7 +2,7 @@ ## Setting up your data connection -Instantiate a [data provider](retrieving-data/README.md), for example using the generic Http data provider: +Instantiate a [data provider](README.md), for example using the generic Http data provider: ```php use Strata\Data\Http\Http; @@ -16,13 +16,13 @@ See []()authentication ## Requests To make a request use a concrete method from the data provider, these are different for different types of providers. -See [data providers](retrieving-data/README.md) for documentation. +See [data providers](README.md) for documentation. -The generic [Http data provider](retrieving-data/http.md) supports request methods such as `get()`, `post()` and `exists()`. +The generic [Http data provider](http.md) supports request methods such as `get()`, `post()` and `exists()`. -The [Rest data provider](retrieving-data/rest.md) automatically decodes data as JSON. +The [Rest data provider](rest.md) automatically decodes data as JSON. -The [GraphQL data provider](retrieving-data/graphql.md) supports request methods such as `ping()` and `query()`. +The [GraphQL data provider](graphql.md) supports request methods such as `ping()` and `query()`. an available Data class. RestApi supports things like get and post, GraphQL has a query method. Full details on available methods appear below. diff --git a/docs/property-paths.md b/docs/retrieving-data/property-paths.md similarity index 100% rename from docs/property-paths.md rename to docs/retrieving-data/property-paths.md diff --git a/docs/retrieving-data/query.md b/docs/retrieving-data/query.md index de96f68..eb8b753 100644 --- a/docs/retrieving-data/query.md +++ b/docs/retrieving-data/query.md @@ -48,7 +48,7 @@ This: In the example above the total results data is set as `[meta][total_results]` which is a property path pointing to `$data['meta']['total_results']` -See [property paths](../property-paths.md) for more information on how to use these. +See [property paths](property-paths.md) for more information on how to use these. ## Query classes diff --git a/docs/testing-api-requests.md b/docs/testing-api-requests.md deleted file mode 100644 index a05c9ea..0000000 --- a/docs/testing-api-requests.md +++ /dev/null @@ -1,76 +0,0 @@ -# Testing API requests - -When testing HTTP requests you need to create mock responses based on what would actually be returned from a real HTTP -request. Symfony's HTTPClient has support for [testing HTTP requests](https://symfony.com/doc/current/components/http_client.html#testing-http-clients-and-responses). - -You can also use `MockResponseFromFile` to generate a mock response easily from a file. - -## MockResponseFromFile - -Allows you to load a mock request from file. - -### Parameters - -* `$filename (string)` File to load mock response from - -### Description - -Body file is loaded from `{$filename}` - -The optional info file is loaded from `{$filename}.info.php` and must contain the `$info` variable (array). By default -mock responses return a 200 status code which you can change by setting the `$info` array. - -### Usage - -The following code loads `./responses/api-test.json` and if it exists `./responses/api-test.json.info.php` to create -a mock response. - -```php -use Symfony\Component\HttpClient\MockHttpClient; -use Strata\Data\Api_DELETE\RestApi; -use Strata\Data\Response\MockResponseFromFile; - -$responses = [ - new MockResponseFromFile(__DIR__ . '/responses/api-test.json'), -]; - -$api = new RestApi('https://example.com/'); -$api->setClient(new MockHttpClient($responses, 'https://example.com/')); - -$response = $api->get('test'); - -// Outputs:404 -echo $response->getStatusCode(); - -// Outputs: JSON response content -echo $response->getContent(); - -// Outputs: 0 -echo $api->getHeader($response, 'X-Total-Results'); -``` - -#### ./responses/api-test.json - -```json -{ - "message": "PAGE NOT FOUND" -} -``` - -#### ./responses/api-test.json.info.php - -```php - 404, - 'response_headers' => [ - 'X-Total-Results' => '0' - ] -]; -``` - -See [ResponseInterface::getInfo()](https://github.com/symfony/symfony/blob/master/src/Symfony/Contracts/HttpClient/ResponseInterface.php) -for possible info, the most common are: - -* `http_code (int)` - the last HTTP response code -* `response_headers (array)` - an array of response headers diff --git a/docs/usage/caching.md b/docs/usage/caching.md deleted file mode 100644 index ca6cfba..0000000 --- a/docs/usage/caching.md +++ /dev/null @@ -1,226 +0,0 @@ -# Caching - -When making requests to external data providers you may want to cache data responses so you can reduce the number of outgoing HTTP requests. - -Features include: - -* Automatic caching for data requests -* Hydrates cached HTTP responses back into a response object -* `isHit()` method on HTTP responses to help detect when cache is used -* Enable and disable cache for different types of data requests -* Set custom cache lifetime and tags for different data requests -* For concurrent requests saves cache via the [persist queue](https://symfony.com/doc/current/components/cache/cache_pools.html#saving-cache-items) to increase performance -* Probability-based pruning of expired cache entries to increase performance - -Also see [Data history](../advanced-usage/data-history.md), which also uses the cache as a storage engine, to detect whether new content has changed or is new. - -## Setup - -Pass a [PSR-6 compatible cache adapter](https://symfony.com/doc/current/components/cache/cache_pools.html#creating-cache-pools) to the `setCache()` method to enable caching. It's recommended to use a cache that supports tagging. - -```php -use Symfony\Component\Cache\Adapter\FilesystemTagAwareAdapter; - -$api->setCache(new FilesystemTagAwareAdapter()); -``` - -This sets and enables the cache for all data requests. - -Please note, setting a cache lifetime on the cache adapter has no effect since this is overwritten in the `DataCache` class. See how to [alter the cache lifetime](caching.md#cache-lifetime). - -You can also pass further arguments to customise the cache, for example setting the cache namespace and cache path: - -```php -$cache = new FilesystemTagAwareAdapter('cache', 0, __DIR__ . '/path/to/cache/folder'); -$api->setCache($cache); -``` - -## Using the cache - -The data cache automatically caches data requests if the request is cacheable. For HTTP data requests this is determined as: - -* Cache is enabled -* GET or HEAD requests - -For GraphQL queries this is determiend by: - -* Cache is enabled -* GET, HEAD or POST requests - -To cache data simply set the cache and then make your data request: - -```php -$result = $api->get('my-data'); -``` - -This automatically saves items to the cache via the data provider with a default cache lifetime of one hour. - -For example, these requests all return exactly the same data when caching is enabled. Only the first request is actually sent to httpbin.org - -```php -$api = new RestApi('http://httpbin.org/'); -$api->setCache(new FilesystemTagAwareAdapter()); - -// This returns a random UUID from httpbin.org -echo PHP_EOL . $api->get('uuid')->toArray()['uuid']; -echo PHP_EOL . $api->get('uuid')->toArray()['uuid']; -``` - -You can also disable the cache for future data requests: - -```php -$api->disableCache(); -``` - -And re-enable it again when you want to use it: - -```php -$api->enableCache(); -``` - -This allows more fine-grained caching rules, where you may want to cache some data requests and not others. - -If you wish you can also reset the cache to it's last set value. This is useful if you want to enable the cache for one -query only but don't want to keep track of whether the cache is currently enabled or disabled. - -```php -$api->enableCache(); - -// Run query... - -$api->resetEnableCache(); -``` - -### Working out if an HTTP response has been cached - -All HTTP data requests return `CacheableResponse` response objects. This decorates the standard HTTP response and adds the method `isHit()` to a response so you can work out if response data was returned from the cache or was requested fresh from the origin. - -```php -$response = $api->get('uuid'); - -if ($response->isHit()) { - echo "HIT"; -} else { - echo "MISS"; -} -``` - -### Caching HTTP responses - -When an HTTP response is returned from the cache this is hydrated via the Symfony `MockResponse` class and a valid HTTP response object is returned. This restores the following data from a response: - -* Status code -* Response headers -* Body content - -### Cache lifetime - -By default the Data Cache caches data for up to one hour. You can set a custom cache lifetime when enabling the cache, by passing the number of seconds to store data in the cache: - -```php -$api->enableCache(300); -``` - -You can also use the `CacheLifetime` class, which has a set of convenience constants to set cache lifetime in seconds: `CacheLifetime::MINUTE`, `CacheLifetime::HOUR`, `CacheLifetime::DAY`, `CacheLifetime::WEEK`, `CacheLifetime::MONTH`, `CacheLifetime::YEAR`. - -```php -use Strata\Data\Cache\CacheLifetime; -$api->enableCache(CacheLifetime::MINUTE * 5); -``` - -You can also set the cache lifetime via the cache directly: - -```php -$api->getCache()->setLifetime(CacheLifetime::MINUTE * 5); -``` - -### Adding tags - -If your cache adapter supports tags, you can set tags to be saved against all future data requests. If your cache adapter does not support tags this will throw a `CacheException`. - -Pass an array of tags to save to cache items: - -```php -$data->setCacheTags(['my-tag', 'second-tag']); -``` - -These tags are then set for all future cached data via the Data Cache. - -To stop tags being saved against cache items, simply call the method without any arguments. This empties any previously set cache tags and disables tagging for future data requests. - -```php -$data->setCacheTags(); -``` - -## Invalidating the cache - -### Using the Data Cache directly - -Convenience methods exist in data providers to save items in the cache, all other functionality must be accessed via the cache object itself. To directly access the DataCache use: - -```php -/** @var Strata\Data\Cache\DataCache $cache */ -$cache = $data->getCache(); -``` - -### Expiration based invalidation - -By default, all data stored by `DataCache` has a cache lifetime and cache items are removed after this lifetime has expired. However, some cache adapters \(e.g. filesystem\) only expire cache items when they are requested, see [pruning old cache items](caching.md#pruning-old-cache-items) on how to solve this problem. - -### Key invalidation - -You can remove individual cache items via: - -```php -$data->getCache()->deleteItem($key); -``` - -Or to delete multiple items: - -```php -$data->getCache()->deleteItems([$key1, $key2]); -``` - -### Tag-based invalidation - -You can remove all cache items stored against a tag via: - -```php -$data->getCache()->invalidateTags(['custom-tag']); -``` - -### Delete everything - -To delete everything from the cache: - -```php -$data->getCache()->clear(); -``` - -### Pruning old cache items - -Some cache pools do not have automated mechanisms for pruning expired caches which under certain circumstances can cause diskpsace or memory usage issues. The `FilesystemAdapter` does not remove expired cache items until an individual item is explicitly requested and determined to be expired. - -This can be worked around by purging the cache on a regular basis. The DataCache can be purged via: - -```php -$data->getCache()->purge(); -``` - -By default, this runs a purge request on all items in a cache. To help increase performance, you can choose to only run a purge request a certain percentage of times. This helps if you want to call purge frequently but only run it every so often. - -To do this, pass the `$probability` argument which represents a number between 0 \(never runs\) to 1 \(always runs\). - -For example, to run 1 time in 10: - -```php -$data->getCache()->purge(0.1); -``` - -The following cache adapters support purge requests: - -* [Filesystem Cache Adapter](https://symfony.com/doc/current/components/cache/adapters/filesystem_adapter.html) -* [PDO & Doctrine DBAL Cache Adapter](https://symfony.com/doc/current/components/cache/adapters/pdo_doctrine_dbal_adapter.html) -* [PHP Array Cache Adapter](https://symfony.com/doc/current/components/cache/adapters/php_array_cache_adapter.html) -* [PHP Files Cache Adapter](https://symfony.com/doc/current/components/cache/adapters/php_files_adapter.html) - diff --git a/docs/usage/installation.md b/docs/usage/installation.md deleted file mode 100644 index ed931cc..0000000 --- a/docs/usage/installation.md +++ /dev/null @@ -1,14 +0,0 @@ -# Installation - -## Requirements - -* PHP 7.4+ -* [Composer](https://getcomposer.org/) - -## Standalone - -Install via Composer: - -``` -composer require strata/data:^0.8 -``` diff --git a/docs/usage/making-requests.md b/docs/usage/making-requests.md deleted file mode 100644 index e5665ca..0000000 --- a/docs/usage/making-requests.md +++ /dev/null @@ -1,37 +0,0 @@ -# Making a request - -## Setting up your data connection - -Instantiate a [data provider](../retrieving-data/data-providers.md), for example using the generic Http data provider: - -```php -use Strata\Data\Http\Http; - -$api = new Http('https://example.com/api/'); -``` - -If you need to setup any See authentication - -## Requests - -To make a request use a concrete method from the data provider, these are different for different types of providers. See [data providers](../retrieving-data/data-providers.md) for documentation. - -The generic [Http data provider](../retrieving-data/http.md) supports request methods such as `get()`, `post()` and `exists()`. - -The [Rest data provider](../retrieving-data/rest.md) automatically decodes data as JSON. - -The [GraphQL data provider]() supports request methods such as `ping()` and `query()`. - -an available Data class. RestApi supports things like get and post, GraphQL has a query method. Full details on available methods appear below. - -```php -$response = $api->get('posts'); -``` - -HTTP requests only run once you access data. For example: - -```php -$item = $response->getContents(); -``` - -At this point the HTTP request is made and if an error occurs an exception is thrown. diff --git a/docs/usage/validating.md b/docs/usage/validating.md deleted file mode 100644 index d7bd270..0000000 --- a/docs/usage/validating.md +++ /dev/null @@ -1,119 +0,0 @@ -# Validation - -You can validate incoming data based on a set of rules. One example validator is supplied, the [Validation rules validator](validating.md#validation-rules). - -You can create a custom validator by implementing the `Strata\Data\Validate\ValidatorInterface` interface. This requires a `isValid()` method to test whether data is valid and a `getErrorMessage()` method to return the error message for the last validation attempt. - -The basic flow is: - -* Instantiate the validator object -* Call the `isValid($data)` method, passing the data array or object you are validating -* Return any error message via `getErrorMessage()` -* Take any action you wish based on the validation result - -```php -$validator = new CustomValidator(); - -if ($validator->isValid($data)) { - // do something -} -``` - -## Validation rules - -A simple validation rules system exists, inspired by [Laravel's Validation](https://laravel.com/docs/validation). - -Simply create a new instance of `Strata\Data\Validate\ValidationRules` and pass in the validation requirements in the constructor. This is an array made up of the property path to the field you want to validate and the validation rule. See [how to write property paths](../changing-data/property-paths.md). - -The rules format is: - -```text -'[data property]' => 'rule|another rule:values separated by commas' -``` - -E.g. - -```php -$rules = [ - '[total]' => 'required|integer', - '[data][title]' => 'required', - '[data][type]' => 'required|in:1,2,3', -]; -``` - -In this example: - -* `$item['total']` must exist and be an integer. -* `$item['data']['title']` must exist. -* `$item['data']['type']` must exist and have a value of: 1, 2, 3. - -A complete example: - -```php -$validator = new ValidationRules([ - '[total]' => 'required|integer', - '[data][title]' => 'required', - '[data][type]' => 'required|in:1,2,3', -]; - -if ($validator->isValid($item)) { - // do something -} -``` - -Available validation rules: - -* [array](validating.md#array) -* [boolean](validating.md#boolean) -* [email](validating.md#email) -* [image](validating.md#image) -* [in](validating.md#in) -* [number](validating.md#number) -* [required](validating.md#required) -* [url](validating.md#url) - -### array - -Tests whether the property is an array. - -### boolean - -Tests whether the property is "1", "true", "on" and "yes". Returns false otherwise. - -### email - -Tests whether the property is a valid email address. - -### image - -Tests whether the property is an image filename \(jpg, jpeg, png, bmp, gif, svg, or webp\). - -### in - -Tests whether the property value is one of the allowed passed values. For example, a rule of `in:1,2,3` tests for whether the property value is 1, 2, or 3. - -### number - -Tests whether the property is a number, using PHP's [is\_numeric\(\)](https://www.php.net/is-numeric) rules. - -### required - -Tests whether the property exists and is non-empty. Empty is defined as null, an empty array or an empty string. - -### url - -Tests whether the property is a valid URL. - -### Custom validation rules - -You can also define your own custom validation rule by creating a class that implements the `Strata\Data\Validate\RuleInterface` interface. It is recommended to extend `Strata\Data\Validate\Rule\ValidatorRuleAbstract` which makes building custom rules easier. - -To use a custom validation rule pass an instance of the class as the rule: - -```php -$validator = new ValidationRules([ - 'total' => 'required|integer', - 'data.title' => new CustomRuleValidation(), -]; -``` - diff --git a/phpstan.neon b/phpstan.neon index c8b9993..b35f3d4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 1 + level: 5 paths: - src - tests diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8322434..08e9067 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,11 +1,6 @@ - - - - src - - + @@ -15,4 +10,9 @@ tests + + + src + + diff --git a/src/Cache/DataCache.php b/src/Cache/DataCache.php index 7962e67..f855f8a 100644 --- a/src/Cache/DataCache.php +++ b/src/Cache/DataCache.php @@ -48,11 +48,17 @@ public function __construct(CacheItemPoolInterface $cache, ?int $defaultLifetime * Set cache lifetime * * @param int $lifetime Lifetime in seconds - * @return $this] + * @return $this */ public function setLifetime(int $lifetime) { $this->lifetime = $lifetime; + return $this; + } + + public function getLifetime(): int + { + return $this->lifetime; } /** @@ -89,6 +95,7 @@ public function setTags(array $tags) throw new CacheException(sprintf('Tags are not supported by your cache adapter %s', get_class($this->cache))); } $this->tags = $tags; + return $this; } /** @@ -117,17 +124,20 @@ public function setCacheItemDefaults(ItemInterface $item): ItemInterface * @param string $key * The key for which to return the corresponding Cache Item. * - * @return CacheItemInterface + * @return CacheItem * The corresponding Cache Item. * @throws InvalidArgumentException * If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException * MUST be thrown. * */ - public function getItem($key): CacheItemInterface + public function getItem($key): CacheItem { $item = $this->cache->getItem($key); $this->setCacheItemDefaults($item); + + // @todo PHPStan: Method Strata\Data\Cache\DataCache::getItem() should return Symfony\Component\Cache\CacheItem but returns Psr\Cache\CacheItemInterface. + // @phpstan-ignore-next-line return $item; } @@ -147,7 +157,7 @@ public function getItem($key): CacheItemInterface * MUST be thrown. * */ - public function getItems(array $keys = array()) + public function getItems(array $keys = []): iterable { return $this->cache->getItems($keys); } @@ -169,7 +179,7 @@ public function getItems(array $keys = array()) * MUST be thrown. * */ - public function hasItem($key) + public function hasItem($key): bool { return $this->cache->hasItem($key); } @@ -177,12 +187,13 @@ public function hasItem($key) /** * Deletes all items in the pool. * + * @param string $prefix * @return bool * True if the pool was successfully cleared. False if there was an error. */ - public function clear(string $prefix = '') + public function clear(string $prefix = ''): bool { - return $this->cache->clear($prefix); + return $this->cache->clear(); } /** @@ -198,7 +209,7 @@ public function clear(string $prefix = '') * MUST be thrown. * */ - public function deleteItem($key) + public function deleteItem($key): bool { return $this->cache->deleteItem($key); } @@ -215,7 +226,7 @@ public function deleteItem($key) * MUST be thrown. * */ - public function deleteItems(array $keys) + public function deleteItems(array $keys): bool { return $this->cache->deleteItems($keys); } @@ -229,7 +240,7 @@ public function deleteItems(array $keys) * @return bool * True if the item was successfully persisted. False if there was an error. */ - public function save(CacheItemInterface $item) + public function save(CacheItemInterface $item): bool { return $this->cache->save($item); } @@ -243,7 +254,7 @@ public function save(CacheItemInterface $item) * @return bool * False if the item could not be queued or if a commit was attempted and failed. True otherwise. */ - public function saveDeferred(CacheItemInterface $item) + public function saveDeferred(CacheItemInterface $item): bool { return $this->cache->saveDeferred($item); } @@ -316,7 +327,7 @@ public function getResponseFromItem(CacheItem $item, string $method, string $uri * @return bool * True if all not-yet-saved items were successfully saved or there were none. False otherwise. */ - public function commit() + public function commit(): bool { return $this->cache->commit(); } @@ -329,11 +340,10 @@ public function commit() */ public function invalidateTags(array $tags): bool { - if (!$this->isTaggable()) { + if (!($this->cache instanceof TagAwareAdapterInterface)) { throw new CacheException('Cannot prune cache since cache adaptor does not implement TagAwareAdapterInterface'); } - /** @var TagAwareAdapterInterface */ return $this->cache->invalidateTags($tags); } @@ -351,9 +361,9 @@ public function invalidateTags(array $tags): bool * @param float $probability Set a value between 0 and 1 to run based on a % chance (0.5 = run on 50% of calls) * @see https://symfony.com/doc/current/components/cache/cache_pools.html#component-cache-cache-pool-prune */ - public function prune(float $probability = 1.0) + public function prune(float $probability = 1.0): bool { - if (!$this->isPruneable()) { + if (!($this->cache instanceof PruneableInterface)) { throw new CacheException('Cannot prune cache since cache adaptor does not implement PruneableInterface'); } @@ -363,10 +373,9 @@ public function prune(float $probability = 1.0) $number = mt_rand(0, 10); if ($number > $probability * 10) { - return; + return false; } - /** @var PruneableInterface */ - $this->cache->prune(); + return $this->cache->prune(); } } diff --git a/src/Cache/DataHistory.php b/src/Cache/DataHistory.php index a7f3c1a..9be5926 100644 --- a/src/Cache/DataHistory.php +++ b/src/Cache/DataHistory.php @@ -89,6 +89,20 @@ public function getAll($key): array return $history; } + /** + * Whether the item is stored in the data history + * + * @param $key + * @return bool + * @throws \Psr\Cache\InvalidArgumentException + */ + public function hasItem($key): bool + { + $item = $this->cache->getItem($this->getKey($key)); + + return $item->isHit(); + } + /** * Return last history log item * @@ -109,13 +123,10 @@ public function getLastItem($key, string $field = null) switch ($field) { case 'updated': return $item['updated']; - break; case 'content_hash': return $item['content_hash']; - break; case 'metadata': return $item['metadata']; - break; default: throw new CacheException(sprintf('Cannot return history field "%s" since not set', $field)); } diff --git a/src/Collection.php b/src/Collection.php index 57113fc..f56e8a0 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -4,7 +4,6 @@ namespace Strata\Data; -use Strata\Data\Helper\UnionTypes; use Strata\Data\Pagination\Pagination; use Strata\Data\Traits\IterableTrait; @@ -35,9 +34,8 @@ public function setCollection(array $collection) * Add an item to the collection * @param array|object $item */ - public function add($item) + public function add(array|object $item) { - UnionTypes::assert('$item', $item, 'array', 'object'); $this->collection[] = $item; } @@ -57,7 +55,7 @@ public function getPagination(): Pagination * @param mixed $offset * @return bool true on success or false on failure. */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return isset($this->collection[$offset]); } @@ -68,7 +66,7 @@ public function offsetExists($offset) * @param mixed $offset * @return mixed Can return all value types. */ - public function offsetGet($offset) + public function offsetGet($offset): mixed { return isset($this->collection[$offset]) ? $this->collection[$offset] : null; } @@ -80,7 +78,7 @@ public function offsetGet($offset) * @param mixed $value * @return void */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { if (is_null($offset)) { $this->collection[] = $value; @@ -95,7 +93,7 @@ public function offsetSet($offset, $value) * @param mixed $offset * @return void */ - public function offsetUnset($offset) + public function offsetUnset($offset): void { unset($this->collection[$offset]); } diff --git a/src/CollectionInterface.php b/src/CollectionInterface.php index 8fd5875..a78119f 100644 --- a/src/CollectionInterface.php +++ b/src/CollectionInterface.php @@ -13,7 +13,7 @@ interface CollectionInterface extends \SeekableIterator, \Countable, \ArrayAcces { public function setCollection(array $collection); - public function add($item); + public function add(array|object $item); public function setPagination(Pagination $pagination); diff --git a/src/DataProviderCommonTrait.php b/src/DataProviderCommonTrait.php index 226e2fa..52902df 100644 --- a/src/DataProviderCommonTrait.php +++ b/src/DataProviderCommonTrait.php @@ -8,6 +8,7 @@ use Strata\Data\Decode\DecoderInterface; use Strata\Data\Exception\BaseUriException; use Strata\Data\Exception\CacheException; +use Symfony\Component\Cache\Adapter\FilesystemTagAwareAdapter; use Symfony\Contracts\Cache\CacheInterface; /** @@ -122,14 +123,15 @@ public function hasCache(): bool /** * Enable cache for subsequent data requests * - * @param ?int $lifetime - * @return DataProviderCommonTrait Fluent interface + * @param int|null $lifetime + * @return self Fluent interface * @throws CacheException If cache not set */ - public function enableCache(?int $lifetime = null) + public function enableCache(?int $lifetime = null): self { if (!$this->hasCache()) { - throw new CacheException(sprintf('You must setup the cache via %s::setCache() before enabling it', get_class($this))); + $cache = new FilesystemTagAwareAdapter(); + $this->setCache($cache); } $this->lastCacheEnabled = $this->cacheEnabled; $this->cacheEnabled = true; @@ -137,6 +139,7 @@ public function enableCache(?int $lifetime = null) if ($lifetime !== null) { $this->cache->setLifetime($lifetime); } + return $this; } /** @@ -152,12 +155,13 @@ public function resetEnableCache() /** * Disable cache for subsequent data requests * - * @return DataProviderCommonTrait Fluent interface + * @return self Fluent interface */ - public function disableCache() + public function disableCache(): self { $this->lastCacheEnabled = $this->cacheEnabled; $this->cacheEnabled = false; + return $this; } /** diff --git a/src/DataProviderDecoratorTrait.php b/src/DataProviderDecoratorTrait.php index fcfc011..d46cb9b 100644 --- a/src/DataProviderDecoratorTrait.php +++ b/src/DataProviderDecoratorTrait.php @@ -149,7 +149,7 @@ public function setCacheTags(array $tags = []) */ public function addListener(string $eventName, callable $listener, int $priority = 0) { - return $this->dataProvider->addListener($eventName, $listener, $priority); + $this->dataProvider->addListener($eventName, $listener, $priority); } /** @@ -157,7 +157,7 @@ public function addListener(string $eventName, callable $listener, int $priority */ public function addSubscriber(EventSubscriberInterface $subscriber) { - return $this->dataProvider->addSubscriber($subscriber); + $this->dataProvider->addSubscriber($subscriber); } /** diff --git a/src/DataProviderInterface.php b/src/DataProviderInterface.php index 35084ff..4c42a7e 100644 --- a/src/DataProviderInterface.php +++ b/src/DataProviderInterface.php @@ -6,10 +6,13 @@ use Strata\Data\Cache\DataCache; use Strata\Data\Decode\DecoderInterface; +use Strata\Data\Exception\BaseUriException; use Strata\Data\Exception\CacheException; +use Strata\Data\Http\Response\CacheableResponse; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\EventDispatcher\Event; +use Symfony\Contracts\HttpClient\HttpClientInterface; interface DataProviderInterface { @@ -54,6 +57,12 @@ public function suppressErrors(bool $value = true); */ public function isSuppressErrors(): bool; + /** + * Reset suppress errors status to last value + * + */ + public function resetSuppressErrors(); + /** * Return default decoder * @@ -103,17 +112,17 @@ public function getCache(): DataCache; * Enable cache for subsequent data requests * * @param ?int $lifetime - * @return DataProviderCommonTrait Fluent interface + * @return self Fluent interface * @throws CacheException If cache not set */ - public function enableCache(?int $lifetime = null); + public function enableCache(?int $lifetime = null): self; /** * Disable cache for subsequent data requests * - * @return DataProviderCommonTrait Fluent interface + * @return self Fluent interface */ - public function disableCache(); + public function disableCache(): self; /** * Set cache tags to apply to all future saved cache items @@ -150,4 +159,14 @@ public function addSubscriber(EventSubscriberInterface $subscriber); * @return Event The passed $event MUST be returned */ public function dispatchEvent(Event $event, string $eventName): Event; + + public function runRequest(CacheableResponse $response): CacheableResponse; + + public function hasHttpClient(): bool; + + public function setHttpClient(HttpClientInterface $client); + + public function getHttpClient(): HttpClientInterface; + + public function prepareRequest($method, string $uri, array $options = [], ?bool $cacheable = null): CacheableResponse; } diff --git a/src/Decode/StringNormalizer.php b/src/Decode/StringNormalizer.php index 450a0e5..2bc1598 100644 --- a/src/Decode/StringNormalizer.php +++ b/src/Decode/StringNormalizer.php @@ -12,7 +12,7 @@ class StringNormalizer /** * Take a string or object, and return a string representing the content * - * @param string|object $data + * @param mixed $data * @return string * @throws DecoderException */ @@ -30,10 +30,8 @@ public static function getString($data): string if (method_exists($data, '__toString')) { return $data->__toString(); } - throw new DecoderException(sprintf('Cannot convert object of type "%s" into a string', get_class($data))); } - - throw new DecoderException(sprintf('$data must be a string or object, "%s" passed', gettype($data))); + throw new DecoderException(sprintf('Cannot convert %s into a string', gettype($data))); } } diff --git a/src/Event/RequestEventAbstract.php b/src/Event/RequestEventAbstract.php index f8dda45..d140254 100644 --- a/src/Event/RequestEventAbstract.php +++ b/src/Event/RequestEventAbstract.php @@ -27,13 +27,13 @@ public function __construct(string $requestId, string $uri, array $context = []) */ public function getRequestId(): string { - return $this->getRequestId(); + return $this->requestId; } /** * Return URI * - * @return ResponseInterface + * @return string */ public function getUri(): string { diff --git a/src/Event/Subscriber/LoggerSubscriber.php b/src/Event/Subscriber/LoggerSubscriber.php index 3ffc492..ec2ca31 100644 --- a/src/Event/Subscriber/LoggerSubscriber.php +++ b/src/Event/Subscriber/LoggerSubscriber.php @@ -42,10 +42,6 @@ public function logSuccess(SuccessEvent $event) public function logFailure(FailureEvent $event) { - if ($event->getException() instanceof \Exception) { - $this->logger->info(self::PREFIX . sprintf('Failed request to: %s, Error: %s', $event->getUri(), $event->getException()->getMessage()), $event->getContext()); - } else { - $this->logger->info(self::PREFIX . sprintf('Failed request to: %s', $event->getUri()), $event->getContext()); - } + $this->logger->error(self::PREFIX . sprintf('Failed request to: %s, Error: %s', $event->getUri(), $event->getException()->getMessage()), $event->getContext()); } } diff --git a/src/Event/Subscriber/StopwatchSubscriber.php b/src/Event/Subscriber/StopwatchSubscriber.php index b612f56..4a9ec04 100644 --- a/src/Event/Subscriber/StopwatchSubscriber.php +++ b/src/Event/Subscriber/StopwatchSubscriber.php @@ -30,11 +30,11 @@ public static function getSubscribedEvents(): array public function start(StartEvent $event) { - $this->stopwatch->start($event->getResponse()->getRequestId(), 'data'); + $this->stopwatch->start($event->getRequestId(), 'data'); } public function stop(SuccessEvent $event) { - $this->stopwatch->stop($event->getResponse()->getRequestId()); + $this->stopwatch->stop($event->getRequestId()); } } diff --git a/src/Helper/ContentHasher.php b/src/Helper/ContentHasher.php index 397ec91..74f6fe3 100755 --- a/src/Helper/ContentHasher.php +++ b/src/Helper/ContentHasher.php @@ -17,11 +17,11 @@ class ContentHasher /** * Return a hash based on passed content * - * @param string|array $content + * @param array|string $content * @return string Hash * @throws InvalidArgumentException */ - public static function hash($content): string + public static function hash(array|string $content): string { $content = self::normalise($content); return hash(self::HASH_ALGORITHM, $content); @@ -30,12 +30,12 @@ public static function hash($content): string /** * Determine whether content has changed based on the original hash * - * @param string|array $originalHash - * @param string $content + * @param array|string $originalHash + * @param array|string $content * @return bool * @throws InvalidArgumentException */ - public static function hasContentChanged(string $originalHash, $content): bool + public static function hasContentChanged(array|string $originalHash, array|string $content): bool { $content = self::normalise($content); $newContentHash = self::hash($content); @@ -50,18 +50,16 @@ public static function hasContentChanged(string $originalHash, $content): bool /** * Normalise content so it is a string and can be used to create a hash * - * @param $content + * @param array|string $content * @return string * @throws InvalidArgumentException */ - private static function normalise($content): string + private static function normalise(array|string $content): string { if (is_string($content)) { return $content; } elseif (is_array($content)) { return print_r($content, true); - } else { - throw new InvalidArgumentException(sprintf('$content argument must be a string or array, \'%s\' passed', gettype($content))); } } } diff --git a/src/Helper/UnionTypes.php b/src/Helper/UnionTypes.php deleted file mode 100644 index f45ded3..0000000 --- a/src/Helper/UnionTypes.php +++ /dev/null @@ -1,93 +0,0 @@ -isHit()) { $response = $this->runRequest($response); diff --git a/src/Http/Response/CacheableResponse.php b/src/Http/Response/CacheableResponse.php index 7898c32..33f6680 100644 --- a/src/Http/Response/CacheableResponse.php +++ b/src/Http/Response/CacheableResponse.php @@ -21,8 +21,8 @@ class CacheableResponse implements ResponseInterface, StreamableInterface * Constructor * * @param ResponseInterface $response Response we are decorating - * @param bool $hit Whether this response came from the cache (and has a cache hit) - * @param CacheItem $item Cache item + * @param ?bool $hit Whether this response came from the cache (and has a cache hit) + * @param ?ItemInterface $item Cache item */ public function __construct(ResponseInterface $response, ?bool $hit = null, ?ItemInterface $item = null) { diff --git a/src/Http/Response/DecoratedResponseTrait.php b/src/Http/Response/DecoratedResponseTrait.php index 8057947..4557aac 100644 --- a/src/Http/Response/DecoratedResponseTrait.php +++ b/src/Http/Response/DecoratedResponseTrait.php @@ -31,7 +31,7 @@ public function getDecorated(): ResponseInterface } /** - * Gets the HTTP status code of the response. + * Gets the HTTP status code of the response * * @throws TransportExceptionInterface when a network error occurs */ @@ -40,6 +40,25 @@ public function getStatusCode(): int return $this->decorated->getStatusCode(); } + /** + * Returns true if the response is successful + * + * @return bool + */ + public function isSuccessful(): bool + { + return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; + } + + /** + * Returns true if the response is not successful + * @return bool + */ + public function isFailed(): bool + { + return !$this->isSuccessful(); + } + /** * Gets the HTTP headers of the response. * @@ -54,6 +73,43 @@ public function getHeaders(bool $throw = true): array return $this->decorated->getHeaders($throw); } + public function getHeader(string $header, bool $throw = true): ?array + { + $headers = $this->decorated->getHeaders($throw); + if (isset($headers[$header])) { + return $headers[$header]; + } + return null; + } + + /** + * Returns true if the response is a redirect + * + * @return bool + */ + public function isRedirect(): bool + { + return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; + } + + /** + * The resolved location of redirect responses, null otherwise + * @return string|null + */ + public function getRedirectUrl(): ?string + { + return $this->getInfo('redirect_url'); + } + + /** + * The number of redirects followed while executing the request + * @return int + */ + public function getRedirectCount(): int + { + return $this->getInfo('redirect_count'); + } + /** * Gets the response body as a string. * @@ -94,7 +150,7 @@ public function cancel(): void * @return array|mixed|null An array of all available info, or one of them when $type is * provided, or null when an unsupported type is requested */ - public function getInfo(string $type = null) + public function getInfo(?string $type = null): mixed { return $this->decorated->getInfo($type); } diff --git a/src/Mapper/MapCollection.php b/src/Mapper/MapCollection.php index 08e04d2..ca0f19b 100644 --- a/src/Mapper/MapCollection.php +++ b/src/Mapper/MapCollection.php @@ -8,7 +8,6 @@ use Strata\Data\CollectionInterface; use Strata\Data\Exception\MapperException; use Strata\Data\Exception\PaginationException; -use Strata\Data\Helper\UnionTypes; use Strata\Data\Pagination\Pagination; use Strata\Data\Traits\PaginationPropertyTrait; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; @@ -21,12 +20,11 @@ class MapCollection extends MapperAbstract implements MapperInterface /** * Set data to extract pagination information from - * @param $data + * @param array|object $data * @return $this */ - public function fromPaginationData($data): MapCollection + public function fromPaginationData(array|object $data): MapCollection { - UnionTypes::assert('$data', $data, 'array', 'object'); $this->paginationData = $data; return $this; } @@ -36,7 +34,7 @@ public function fromPaginationData($data): MapCollection * * @param array $data Array of data to get pagination information from * @return Pagination - * @throws MapPaginationException If cannot read data properties to create Pagination + * @throws MapperException If cannot read data properties to create Pagination * @throws PaginationException If cannot setup Pagination object successfully */ public function paginationBuilder(array $data): Pagination @@ -99,7 +97,7 @@ public function map(array $data, ?string $rootProperty = null): CollectionInterf $collectionData = $this->getRootData($data, $rootProperty); // No data returned - if ($data === null) { + if (empty($data)) { $collection = new Collection(); $collection->setPagination(new Pagination(0)); return $collection; diff --git a/src/Mapper/MappingStrategy.php b/src/Mapper/MappingStrategy.php index 6387efc..6890106 100644 --- a/src/Mapper/MappingStrategy.php +++ b/src/Mapper/MappingStrategy.php @@ -5,7 +5,6 @@ namespace Strata\Data\Mapper; use Strata\Data\Exception\MapperException; -use Strata\Data\Helper\UnionTypes; use Strata\Data\Transform\Data\CallableData; use Strata\Data\Transform\PropertyAccessorInterface; use Strata\Data\Transform\PropertyAccessorTrait; @@ -71,9 +70,8 @@ public function getPropertyPaths(): array * @param array|object $item Destination item to map data to * @return mixed */ - public function mapItem(array $data, $item) + public function mapItem(array $data, array|object $item) { - UnionTypes::assert('$item', $item, 'array', 'object'); $propertyAccessor = $this->getPropertyAccessor(); // Loop through property paths to map to new item (destination => source) @@ -111,7 +109,7 @@ public function mapItem(array $data, $item) } // Invalid source type - if (!UnionTypes::is($source, 'string', 'array')) { + if (!is_string($source) && !is_array($source)) { $type = gettype($source); if ($type === 'object') { $type = get_class($source); diff --git a/src/Mapper/MappingStrategyInterface.php b/src/Mapper/MappingStrategyInterface.php index 8b27944..228a718 100644 --- a/src/Mapper/MappingStrategyInterface.php +++ b/src/Mapper/MappingStrategyInterface.php @@ -18,5 +18,5 @@ public function getPropertyAccessor(): PropertyAccessor; * @param array|object $item * @return mixed */ - public function mapItem(array $data, $item); + public function mapItem(array $data, array|object $item); } diff --git a/src/Mapper/WildcardMappingStrategy.php b/src/Mapper/WildcardMappingStrategy.php index 942f6cf..f40229d 100644 --- a/src/Mapper/WildcardMappingStrategy.php +++ b/src/Mapper/WildcardMappingStrategy.php @@ -4,8 +4,6 @@ namespace Strata\Data\Mapper; -use Strata\Data\Helper\UnionTypes; - class WildcardMappingStrategy extends MappingStrategy { private array $mapping = []; @@ -70,10 +68,8 @@ public function getMappingByRootElement(string $field): array * * @param array|string $field */ - public function addIgnore($field) + public function addIgnore(array|string $field) { - UnionTypes::assert('$field', $field, 'array', 'string'); - if (is_array($field)) { foreach ($field as $item) { $this->ignore[] = $this->normaliseFieldName($item); @@ -102,10 +98,8 @@ public function isRootElementInIgnore(string $field): bool * @param array|object $item * @return mixed */ - public function mapItem(array $data, $item) + public function mapItem(array $data, array|object $item) { - UnionTypes::assert('$item', $item, 'array', 'object'); - // Loop through all root data to build property path mapping $propertyPaths = []; foreach ($data as $field => $value) { diff --git a/src/Pagination/Pagination.php b/src/Pagination/Pagination.php index 0f1d630..6c65fc1 100755 --- a/src/Pagination/Pagination.php +++ b/src/Pagination/Pagination.php @@ -44,7 +44,7 @@ public function __construct(?int $totalResults = null, ?int $resultsPerPage = nu * * @return int */ - public function count() + public function count(): int { return $this->getTotalPages(); } diff --git a/src/Permissions.php b/src/Permissions.php index 078fd15..7866f32 100755 --- a/src/Permissions.php +++ b/src/Permissions.php @@ -83,19 +83,12 @@ public function getName(int $action): ?string switch ($action) { case self::READ: return 'READ'; - break; - case self::CREATE: return 'CREATE'; - break; - case self::UPDATE: return 'UPDATE'; - break; - case self::DELETE: return 'DELETE'; - break; } return null; } diff --git a/src/Query/BuildQuery/BuildGraphQLQuery.php b/src/Query/BuildQuery/BuildGraphQLQuery.php index 6c69987..879bb83 100644 --- a/src/Query/BuildQuery/BuildGraphQLQuery.php +++ b/src/Query/BuildQuery/BuildGraphQLQuery.php @@ -42,7 +42,7 @@ public function indent(int $depth = 1): string * Return parameters (key: "values") for use in an GraphQL query * * @param GraphQLQueryInterface $query - * @return array + * @return string */ public function getGraphQLParameters(GraphQLQueryInterface $query): string { diff --git a/src/Query/GraphQLQuery.php b/src/Query/GraphQLQuery.php index f2dfd41..a14d53b 100644 --- a/src/Query/GraphQLQuery.php +++ b/src/Query/GraphQLQuery.php @@ -5,6 +5,8 @@ namespace Strata\Data\Query; use Strata\Data\Collection; +use Strata\Data\CollectionInterface; +use Strata\Data\DataProviderInterface; use Strata\Data\Exception\GraphQLQueryException; use Strata\Data\Exception\QueryException; use Strata\Data\Http\GraphQL; @@ -16,6 +18,7 @@ use Strata\Data\Query\BuildQuery\BuildGraphQLQuery; use Strata\Data\Query\GraphQL\Fragment; use Strata\Data\Query\GraphQL\GraphQLTrait; +use Strata\Data\Traits\QueryStaticMethodsTrait; /** * Class to help craft a GraphQL API query @@ -23,6 +26,7 @@ class GraphQLQuery extends QueryAbstract implements GraphQLQueryInterface { use GraphQLTrait; + use QueryStaticMethodsTrait; protected ?string $name = null; private ?string $alias = null; @@ -46,10 +50,12 @@ public function getRequiredDataProviderClass(): string */ public function getDataProvider(): GraphQL { + if (!($this->dataProvider instanceof GraphQL)) { + throw new \Exception('Data provider not set'); + } return $this->dataProvider; } - /** * Return query name * @@ -349,7 +355,7 @@ public function get() /** * Return collection of data from a query response - * @return Collection + * @return CollectionInterface * @throws QueryException * @throws \Strata\Data\Exception\BaseUriException * @throws \Strata\Data\Exception\HttpException @@ -357,7 +363,7 @@ public function get() * @throws \Strata\Data\Exception\MapperException * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface */ - public function getCollection(): Collection + public function getCollection(): CollectionInterface { // Run response, if not already run if (!$this->hasResponseRun()) { diff --git a/src/Query/Query.php b/src/Query/Query.php index e30907e..97a42b3 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -5,6 +5,7 @@ namespace Strata\Data\Query; use Strata\Data\Collection; +use Strata\Data\CollectionInterface; use Strata\Data\Exception\QueryException; use Strata\Data\Http\Http; use Strata\Data\Http\Response\CacheableResponse; @@ -15,6 +16,7 @@ use Strata\Data\Mapper\WildcardMappingStrategy; use Strata\Data\Query\BuildQuery\BuildQuery; use Strata\Data\Traits\PaginationPropertyTrait; +use Strata\Data\Traits\QueryStaticMethodsTrait; /** * Class to represent a data query, also has methods to return data @@ -24,6 +26,7 @@ class Query extends QueryAbstract implements QueryInterface { use PaginationPropertyTrait; + use QueryStaticMethodsTrait; private string $method = 'GET'; @@ -147,7 +150,7 @@ public function get() /** * Return collection of data from a query response - * @return Collection + * @return CollectionInterface * @throws QueryException * @throws \Strata\Data\Exception\BaseUriException * @throws \Strata\Data\Exception\HttpException @@ -155,7 +158,7 @@ public function get() * @throws \Strata\Data\Exception\MapperException * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface */ - public function getCollection(): Collection + public function getCollection(): CollectionInterface { // Run response, if not already run if (!$this->hasResponseRun()) { diff --git a/src/Query/QueryAbstract.php b/src/Query/QueryAbstract.php index 246c3d8..4f95591 100644 --- a/src/Query/QueryAbstract.php +++ b/src/Query/QueryAbstract.php @@ -5,6 +5,7 @@ namespace Strata\Data\Query; use Strata\Data\Collection; +use Strata\Data\CollectionInterface; use Strata\Data\DataProviderInterface; use Strata\Data\Exception\CacheException; use Strata\Data\Exception\QueryException; @@ -99,7 +100,7 @@ public function setOptions(array $options): self /** * Return options for this query - * @return $this + * @return array */ public function getOptions(): array { @@ -557,15 +558,7 @@ abstract public function run(); /** * Return data from query response - * @param array Data to map to a collection * @return mixed */ abstract public function get(); - - /** - * Return collection of data from a query response - * @return Collection - * @throws \Strata\Data\Exception\MapperException - */ - abstract public function getCollection(): Collection; } diff --git a/src/Query/QueryInterface.php b/src/Query/QueryInterface.php index 631ccac..a51fc97 100644 --- a/src/Query/QueryInterface.php +++ b/src/Query/QueryInterface.php @@ -4,15 +4,27 @@ namespace Strata\Data\Query; -use Strata\Data\Collection; +use Strata\Data\CollectionInterface; use Strata\Data\DataProviderInterface; use Strata\Data\Http\Response\CacheableResponse; interface QueryInterface { + /** + * Static method to create a new query + * @return self + */ + public static function create(): self; + + /** + * Static method to create a new query with a URI + * @param string $uri + * @return self + */ + public static function uri(string $uri): self; + /** * Data provider class required for use with this query - * @var string Class name */ public function getRequiredDataProviderClass(): string; @@ -199,6 +211,12 @@ public function run(); */ public function getResponse(): ?CacheableResponse; + /** + * Clear response and mark to re-run query next time data is accessed + * @return self + */ + public function clearResponse(): self; + /** * Has the response run and retrieved data? * @return bool @@ -207,15 +225,14 @@ public function hasResponseRun(): bool; /** * Return data from query response - * @param array Data to map to a collection * @return mixed */ public function get(); /** * Return collection of data from a query response - * @return Collection + * @return CollectionInterface * @throws \Strata\Data\Exception\MapperException */ - public function getCollection(): Collection; + public function getCollection(): CollectionInterface; } diff --git a/src/Query/QueryManager.php b/src/Query/QueryManager.php index 55ab067..40449f7 100644 --- a/src/Query/QueryManager.php +++ b/src/Query/QueryManager.php @@ -5,6 +5,7 @@ namespace Strata\Data\Query; use Strata\Data\Collection; +use Strata\Data\CollectionInterface; use Strata\Data\DataProviderInterface; use Strata\Data\Exception\CacheException; use Strata\Data\Exception\MissingDataProviderException; @@ -420,11 +421,12 @@ public function get(string $queryName, ?string $rootPropertyPath = null) * * Default functionality is to return decoded data as an array with pagination * + * @return CollectionInterface * @param string $queryName * @param string|null $rootPropertyPath Property path to root element to select data from * @throws QueryManagerException */ - public function getCollection(string $queryName, ?string $rootPropertyPath = null): Collection + public function getCollection(string $queryName, ?string $rootPropertyPath = null): CollectionInterface { if (!$this->hasQuery($queryName)) { throw new QueryManagerException(sprintf('Cannot find query with query name "%s"', $queryName)); diff --git a/src/Traits/EventDispatcherTrait.php b/src/Traits/EventDispatcherTrait.php index 56c1983..ae835a7 100644 --- a/src/Traits/EventDispatcherTrait.php +++ b/src/Traits/EventDispatcherTrait.php @@ -50,21 +50,22 @@ public function getEventDispatcher(): EventDispatcherInterface * @param callable $listener The listener * @param int $priority The higher this value, the earlier an event * listener will be triggered in the chain (defaults to 0) + * @return void */ - public function addListener(string $eventName, callable $listener, int $priority = 0) + public function addListener(string $eventName, callable $listener, int $priority = 0): void { - return $this->getEventDispatcher()->addListener($eventName, $listener, $priority); + $this->getEventDispatcher()->addListener($eventName, $listener, $priority); } /** * Adds an event subscriber * * @param EventSubscriberInterface $subscriber - * @return mixed + * @return void */ - public function addSubscriber(EventSubscriberInterface $subscriber) + public function addSubscriber(EventSubscriberInterface $subscriber): void { - return $this->getEventDispatcher()->addSubscriber($subscriber); + $this->getEventDispatcher()->addSubscriber($subscriber); } /** diff --git a/src/Traits/IterableTrait.php b/src/Traits/IterableTrait.php index b32a626..a25419e 100644 --- a/src/Traits/IterableTrait.php +++ b/src/Traits/IterableTrait.php @@ -50,7 +50,7 @@ public function getCollection(): array * Return current item * @return mixed */ - public function current() + public function current(): mixed { return $this->collection[$this->position]; } @@ -67,7 +67,7 @@ public function key(): int /** * Increment position in collection by one */ - public function next() + public function next(): void { ++$this->position; } @@ -75,7 +75,7 @@ public function next() /** * Rewind to start of collection */ - public function rewind() + public function rewind(): void { $this->position = 0; } @@ -84,7 +84,7 @@ public function rewind() * Is the current position in the collection valid? * @return bool */ - public function valid() + public function valid(): bool { return isset($this->collection[$this->position]); } @@ -92,9 +92,9 @@ public function valid() /** * Seek to the passed position in the collection * @param $position - * @throws OutOfBoundsException + * @throws \OutOfBoundsException */ - public function seek($position) + public function seek($position): void { if (!isset($this->collection[$position])) { throw new \OutOfBoundsException(sprintf('Invalid seek position: %s', $position)); diff --git a/src/Traits/PaginationPropertyTrait.php b/src/Traits/PaginationPropertyTrait.php index 648f696..c616fd4 100644 --- a/src/Traits/PaginationPropertyTrait.php +++ b/src/Traits/PaginationPropertyTrait.php @@ -4,7 +4,6 @@ namespace Strata\Data\Traits; -use Strata\Data\Helper\UnionTypes; use Strata\Data\Query\QueryInterface; /** @@ -21,9 +20,8 @@ trait PaginationPropertyTrait * @param string|int $totalResults Property path to retrieve data from, or actual value * @return $this Fluent interface */ - public function setTotalResults($totalResults) + public function setTotalResults(string|int $totalResults) { - UnionTypes::assert('$totalResults', $totalResults, 'string', 'int'); $this->totalResults = $totalResults; return $this; } @@ -42,9 +40,8 @@ public function getTotalResults() * @param string|int $resultsPerPage Property path to retrieve data from, or actual value * @return $this Fluent interface */ - public function setResultsPerPage($resultsPerPage) + public function setResultsPerPage(string|int $resultsPerPage) { - UnionTypes::assert('$resultsPerPage', $resultsPerPage, 'string', 'int'); $this->resultsPerPage = $resultsPerPage; return $this; } @@ -63,9 +60,8 @@ public function getResultsPerPage() * @param string|int $currentPage Property path to retrieve data from, or actual value * @return $this Fluent interface */ - public function setCurrentPage($currentPage) + public function setCurrentPage(string|int $currentPage) { - UnionTypes::assert('currentPage', $currentPage, 'string', 'int'); $this->currentPage = $currentPage; return $this; } diff --git a/src/Traits/QueryStaticMethodsTrait.php b/src/Traits/QueryStaticMethodsTrait.php new file mode 100644 index 0000000..76e2827 --- /dev/null +++ b/src/Traits/QueryStaticMethodsTrait.php @@ -0,0 +1,29 @@ +setUri($uri); + return $query; + } +} diff --git a/src/Transform/Data/CallableData.php b/src/Transform/Data/CallableData.php index 3066174..a09efc9 100644 --- a/src/Transform/Data/CallableData.php +++ b/src/Transform/Data/CallableData.php @@ -4,8 +4,6 @@ namespace Strata\Data\Transform\Data; -use Strata\Data\Helper\UnionTypes; - class CallableData extends DataAbstract { private $callable; @@ -53,7 +51,7 @@ public function getCallable(): callable */ public function canTransform($data): bool { - return UnionTypes::is($data, 'array', 'object'); + return (is_array($data) || is_object($data)); } /** diff --git a/src/Transform/Data/Concatenate.php b/src/Transform/Data/Concatenate.php index 1ba63ad..d5606d3 100644 --- a/src/Transform/Data/Concatenate.php +++ b/src/Transform/Data/Concatenate.php @@ -4,8 +4,6 @@ namespace Strata\Data\Transform\Data; -use Strata\Data\Helper\UnionTypes; - class Concatenate extends DataAbstract { private array $propertyPaths; @@ -23,7 +21,7 @@ public function __construct(...$propertyPaths) */ public function canTransform($data): bool { - return UnionTypes::is($data, 'array', 'object'); + return (is_array($data) || is_object($data)); } /** diff --git a/src/Transform/Data/MapValues.php b/src/Transform/Data/MapValues.php index a5af010..eb02f8e 100644 --- a/src/Transform/Data/MapValues.php +++ b/src/Transform/Data/MapValues.php @@ -4,7 +4,6 @@ namespace Strata\Data\Transform\Data; -use Strata\Data\Helper\UnionTypes; use Strata\Data\Transform\NotTransformedInterface; use Strata\Data\Transform\NotTransformedTrait; @@ -77,7 +76,7 @@ public function setMapping(array $mapping) */ public function canTransform($data): bool { - return UnionTypes::is($data, 'array', 'object'); + return (is_array($data) || is_object($data)); } /** diff --git a/src/Transform/Data/RenameFields.php b/src/Transform/Data/RenameFields.php index 3faaf53..9ea55cb 100644 --- a/src/Transform/Data/RenameFields.php +++ b/src/Transform/Data/RenameFields.php @@ -4,7 +4,6 @@ namespace Strata\Data\Transform\Data; -use Strata\Data\Helper\UnionTypes; use Strata\Data\Transform\NotTransformedInterface; use Strata\Data\Transform\NotTransformedTrait; use Symfony\Component\PropertyAccess\PropertyPath; @@ -42,7 +41,7 @@ public function setPropertyPaths(array $propertyPaths) */ public function canTransform($data): bool { - return UnionTypes::is($data, 'array', 'object'); + return (is_array($data) || is_object($data)); } /** @@ -73,7 +72,7 @@ public function transform($data) $propertyPath = new PropertyPath($old); $parent =& $data; $elements = $propertyPath->getElements(); - if ($elements > 1) { + if (count($elements) > 1) { for ($x = 0; $x < count($elements) - 1; $x++) { $key = $elements[$x]; $parent =& $parent[$key]; diff --git a/src/Transform/PropertyAccessorTrait.php b/src/Transform/PropertyAccessorTrait.php index a38edd6..a9fa973 100644 --- a/src/Transform/PropertyAccessorTrait.php +++ b/src/Transform/PropertyAccessorTrait.php @@ -15,7 +15,6 @@ trait PropertyAccessorTrait /** * Property accessor * @see https://symfony.com/doc/current/components/property_access.html - * @see PropertyAccessorInterface * @var PropertyAccessor */ private ?PropertyAccessor $propertyAccessor = null; @@ -43,9 +42,11 @@ public function setPropertyAccessor(PropertyAccessor $propertyAccessor) public function getPropertyAccessor(): PropertyAccessor { if (!($this->propertyAccessor instanceof PropertyAccessor)) { - $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + // @var PropertyAccessor $propertyAccessor + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->enableExceptionOnInvalidIndex() ->getPropertyAccessor(); + $this->setPropertyAccessor($propertyAccessor); } return $this->propertyAccessor; } diff --git a/src/Transform/Value/BaseValue.php b/src/Transform/Value/BaseValue.php index 5e9e89a..66d91d8 100644 --- a/src/Transform/Value/BaseValue.php +++ b/src/Transform/Value/BaseValue.php @@ -4,7 +4,6 @@ namespace Strata\Data\Transform\Value; -use Strata\Data\Helper\UnionTypes; use Strata\Data\Transform\PropertyAccessorTrait; class BaseValue implements MapValueInterface @@ -17,9 +16,8 @@ class BaseValue implements MapValueInterface * BaseValue constructor. * @param string|array $propertyPath Property path to read data from */ - public function __construct($propertyPath) + public function __construct(string|array $propertyPath) { - UnionTypes::assert('$propertyPath', $propertyPath, 'string', 'array'); $this->propertyPath = $propertyPath; } diff --git a/src/Transform/Value/CallableValue.php b/src/Transform/Value/CallableValue.php index 0330b67..cc47647 100644 --- a/src/Transform/Value/CallableValue.php +++ b/src/Transform/Value/CallableValue.php @@ -4,8 +4,6 @@ namespace Strata\Data\Transform\Value; -use Strata\Data\Helper\UnionTypes; - class CallableValue extends BaseValue { private $callable; @@ -39,7 +37,7 @@ public function getCallable(): callable */ public function canTransform($data): bool { - return UnionTypes::is($data, 'array', 'object'); + return (is_array($data) || is_object($data)); } /** diff --git a/src/Transform/Value/DateTimeValue.php b/src/Transform/Value/DateTimeValue.php index 33b45b9..062b687 100644 --- a/src/Transform/Value/DateTimeValue.php +++ b/src/Transform/Value/DateTimeValue.php @@ -31,9 +31,9 @@ public function __construct($propertyPath, ?string $format = null, ?\DateTimeZon * Return property as a DateTime object * * @param $objectOrArray Data to read property from - * @return DateTime|null + * @return \DateTime|null */ - public function getValue($objectOrArray) + public function getValue($objectOrArray): ?\DateTime { $value = parent::getValue($objectOrArray); diff --git a/src/Transform/Value/FloatValue.php b/src/Transform/Value/FloatValue.php index 68e1de4..1c15071 100644 --- a/src/Transform/Value/FloatValue.php +++ b/src/Transform/Value/FloatValue.php @@ -10,9 +10,9 @@ class FloatValue extends BaseValue * Return property as a float * * @param $objectOrArray Data to read property from - * @return DateTime|null + * @return float|null */ - public function getValue($objectOrArray) + public function getValue($objectOrArray): ?float { $value = parent::getValue($objectOrArray); diff --git a/src/Transform/Value/IntegerValue.php b/src/Transform/Value/IntegerValue.php index 1de6255..eadcc78 100644 --- a/src/Transform/Value/IntegerValue.php +++ b/src/Transform/Value/IntegerValue.php @@ -10,9 +10,9 @@ class IntegerValue extends BaseValue * Return property as an integer * * @param $objectOrArray Data to read property from - * @return DateTime|null + * @return int|null */ - public function getValue($objectOrArray) + public function getValue($objectOrArray): ?int { $value = parent::getValue($objectOrArray); diff --git a/src/Validate/Rule/ValidatorRuleAbstract.php b/src/Validate/Rule/ValidatorRuleAbstract.php index b2ec461..def8626 100644 --- a/src/Validate/Rule/ValidatorRuleAbstract.php +++ b/src/Validate/Rule/ValidatorRuleAbstract.php @@ -90,10 +90,10 @@ public function getPropertyPath(): string /** * Return property from data, or null if not found * - * @param $data + * @param object|array $data * @return mixed|null */ - public function getProperty($data) + public function getProperty(object|array $data) { if (!$this->getPropertyAccessor()->isReadable($data, $this->propertyPath)) { return null; diff --git a/src/Validate/Rule/ValidatorRuleInterface.php b/src/Validate/Rule/ValidatorRuleInterface.php index feab29c..421db15 100644 --- a/src/Validate/Rule/ValidatorRuleInterface.php +++ b/src/Validate/Rule/ValidatorRuleInterface.php @@ -25,10 +25,10 @@ public function setPropertyPath(string $propertyPath); /** * Return property from data, or null if not found * - * @param $data + * @param object|array $data * @return mixed|null */ - public function getProperty($data); + public function getProperty(object|array $data); /** * Set array of values to use with this validation rule diff --git a/tests/Cache/DataHistoryTest.php b/tests/Cache/DataHistoryTest.php index ce007d2..69332ee 100644 --- a/tests/Cache/DataHistoryTest.php +++ b/tests/Cache/DataHistoryTest.php @@ -113,8 +113,7 @@ public function testGetLastItem() { $history = new DataHistory(new FilesystemAdapter(self::CACHE_NAMESPACE, 0, self::CACHE_DIR)); - // test empty - $this->assertNull($history->getLastItem(123)); + $this->assertFalse($history->hasItem(123)); $now = new \DateTimeImmutable(); $history->add('123', $this->data[1], ['message' => 'Hello']); diff --git a/tests/Decode/DecoderFactoryTest.php b/tests/Decode/DecoderFactoryTest.php index 6eedbf9..fa3bfe5 100644 --- a/tests/Decode/DecoderFactoryTest.php +++ b/tests/Decode/DecoderFactoryTest.php @@ -36,7 +36,7 @@ public function testResponseFactory($contentType, $expectedClass) $this->assertInstanceOf($expectedClass, DecoderFactory::fromResponse($response)); } - public function responseDataProvider() + public static function responseDataProvider() { return [ 'Json' => [ diff --git a/tests/Decode/JsonTest.php b/tests/Decode/JsonTest.php index 1749f87..5887644 100644 --- a/tests/Decode/JsonTest.php +++ b/tests/Decode/JsonTest.php @@ -64,6 +64,7 @@ public function testInvalidJson2() { $decoder = new Json(); $this->expectException(DecoderException::class); + // @phpstan-ignore-next-line $decoder->decode(null); } } diff --git a/tests/Decode/RssTest.php b/tests/Decode/RssTest.php index 4b64342..3ecbfdf 100644 --- a/tests/Decode/RssTest.php +++ b/tests/Decode/RssTest.php @@ -25,8 +25,8 @@ public function testRss() $this->assertInstanceOf('Laminas\Feed\Reader\Feed\FeedInterface', $feed); $this->assertEquals('News feed generator', $feed->getTitle()); - /** @var \Laminas\Feed\Reader\Entry\EntryInterface $item */ $x = 0; + /** @var \Laminas\Feed\Reader\Entry\EntryInterface $item */ foreach ($feed as $item) { $x++; switch ($x) { diff --git a/tests/Decode/YamlFrontMatterTest.php b/tests/Decode/YamlFrontMatterTest.php index cc0cc73..afe6289 100644 --- a/tests/Decode/YamlFrontMatterTest.php +++ b/tests/Decode/YamlFrontMatterTest.php @@ -33,16 +33,21 @@ public function testFrontMatter() { $decoder = new FrontMatter(); $data = $decoder->decode($this->text1); - $this->assertNull($data->title); + + /** + * PHPStan doesn't like testing dynamic properties returned via __get() + * @see https://phpstan.org/blog/solving-phpstan-access-to-undefined-property + */ + $this->assertNull($data->title); /* @phpstan-ignore-line */ $this->assertEquals($this->text1, $data->body()); $data = $decoder->decode($this->text2); - $this->assertEquals('Valid example', $data->title); - $this->assertEquals('test2', $data->layout); + $this->assertEquals('Valid example', $data->title); /* @phpstan-ignore-line */ + $this->assertEquals('test2', $data->layout); /* @phpstan-ignore-line */ $this->assertEquals(PHP_EOL . $this->text1, $data->body()); $data = $decoder->decode($this->text3); - $this->assertNull($data->title); + $this->assertNull($data->title); /* @phpstan-ignore-line */ $this->assertEquals($this->text3, $data->body()); } } diff --git a/tests/Helper/ContentHashTest.php b/tests/Helper/ContentHashTest.php index e7c908b..30dfc7b 100644 --- a/tests/Helper/ContentHashTest.php +++ b/tests/Helper/ContentHashTest.php @@ -56,12 +56,6 @@ public function testFileHash() $this->assertTrue($hash1 == $hash3); } - public function testInvalidType() - { - $this->expectException('InvalidArgumentException'); - ContentHasher::hash(new \DateTime()); - } - public function testArray() { $this->assertFalse(ContentHasher::hasContentChanged(ContentHasher::hash($this->arrayExamples[0]), $this->arrayExamples[1])); diff --git a/tests/Helper/UnionTypesTest.php b/tests/Helper/UnionTypesTest.php deleted file mode 100644 index 21f936d..0000000 --- a/tests/Helper/UnionTypesTest.php +++ /dev/null @@ -1,87 +0,0 @@ -assertFalse(UnionTypes::is(42, 'array', 'object')); - $this->assertFalse(UnionTypes::is(24.99, 'array', 'object')); - $this->assertFalse(UnionTypes::is('string', 'array', 'object')); - $this->assertFalse(UnionTypes::is('24', 'array', 'object')); - $this->assertTrue(UnionTypes::is([1, 2, 3], 'array', 'object')); - $this->assertTrue(UnionTypes::is(new \stdClass(), 'array', 'object')); - $this->assertFalse(UnionTypes::is(null, 'array', 'object')); - - $this->assertTrue(UnionTypes::is(new \DateTime(), 'DateTime', 'array')); - $this->assertFalse(UnionTypes::is(new \stdClass(), 'DateTime', 'array')); - } - - public function testStringOrInt() - { - $this->assertTrue(UnionTypes::is(42, 'string', 'int')); - $this->assertFalse(UnionTypes::is(24.99, 'string', 'int')); - $this->assertTrue(UnionTypes::is('string', 'string', 'int')); - $this->assertTrue(UnionTypes::is('24', 'string', 'int')); - $this->assertFalse(UnionTypes::is([1,2,3], 'string', 'int')); - $this->assertFalse(UnionTypes::is(new \stdClass(), 'string', 'int')); - $this->assertFalse(UnionTypes::is(null, 'string', 'int')); - } - - public function testStringOrObject() - { - $this->assertFalse(UnionTypes::is(42, 'string', 'object')); - $this->assertFalse(UnionTypes::is(24.99, 'string', 'object')); - $this->assertTrue(UnionTypes::is('string', 'string', 'object')); - $this->assertTrue(UnionTypes::is('24', 'string', 'object')); - $this->assertFalse(UnionTypes::is([1,2,3], 'string', 'object')); - $this->assertTrue(UnionTypes::is(new \stdClass(), 'string', 'object')); - $this->assertFalse(UnionTypes::is(null, 'string', 'object')); - - $this->assertTrue(UnionTypes::is(new \DateTime(), 'DateTime', 'string')); - $this->assertFalse(UnionTypes::is(new \stdClass(), 'DateTime', 'string')); - } - - public function testStringOrArray() - { - $this->assertFalse(UnionTypes::is(42, 'string', 'array')); - $this->assertFalse(UnionTypes::is(24.99, 'string', 'array')); - $this->assertTrue(UnionTypes::is('string', 'string', 'array')); - $this->assertTrue(UnionTypes::is('24', 'string', 'array')); - $this->assertTrue(UnionTypes::is([1,2,3], 'string', 'array')); - $this->assertFalse(UnionTypes::is(new \stdClass(), 'string', 'array')); - $this->assertFalse(UnionTypes::is(null, 'string', 'array')); - } - - public function testAssertStringArray() - { - $this->expectException('InvalidArgumentException'); - UnionTypes::assert('test', 42, 'string', 'array'); - } - - public function testAssertArrayInt() - { - $this->expectException('InvalidArgumentException'); - UnionTypes::assert('test', 'value', 'array', 'int'); - } - - public function testAssertObjectString() - { - $this->expectException('InvalidArgumentException'); - UnionTypes::assert('test', [1,2,3], 'object', 'string'); - } - - public function testInterface() - { - $this->assertFalse(UnionTypes::is([1,2,3], '\Countable')); - $this->assertTrue(UnionTypes::is(new \ArrayIterator([1,2,3]), '\Countable')); - $this->assertTrue(UnionTypes::is(new \ArrayIterator([1,2,3]), '\ArrayAccess')); - $this->assertTrue(UnionTypes::is(new \ArrayIterator([1,2,3]), '\ArrayIterator')); - } -} diff --git a/tests/Http/HttpTest.php b/tests/Http/HttpTest.php index 9ae491f..e67151a 100644 --- a/tests/Http/HttpTest.php +++ b/tests/Http/HttpTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Strata\Data\Cache\DataCache; +use Strata\Data\Exception\InvalidHttpMethodException; use Strata\Data\Helper\ContentHasher; use Strata\Data\Http\Http; use Strata\Data\Http\Rest; @@ -57,7 +58,8 @@ public function testValidMethod() public function testInvalidMethod() { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(\TypeError::class); + // @phpstan-ignore-next-line Http::validMethod(99); } @@ -231,9 +233,7 @@ public function testQuery() $this->assertSame('46', $item['id']); $this->assertSame("Test", $item['title']); - /** ideas */ - return; - +/* // Data manager (deals with logging, events, data transformations) $manager = new DataManager(); @@ -249,6 +249,7 @@ public function testQuery() $manager->addTransformer(new GraphQLTransformer()) ->addTransformer(new CollectionTransformer('entries')); $data = $mamager->transform($data); +*/ } public function testList() @@ -377,7 +378,6 @@ public function testManualConcurrent() $response = $api->runRequest($response); } - /** @phpstan-ignore-next-line */ $this->assertEquals('https://example.com/api/file-378.html', $response->getInfo('url')); $this->assertEquals(379, $api->getTotalHttpRequests()); } @@ -444,6 +444,44 @@ public function testBasicRestApiFunctions() $this->assertEquals(5, $api->getTotalHttpRequests()); } + public function testDefaultCache() + { + $api = new Rest('http://example.com/'); + $api->enableCache(); + + $this->assertTrue($api->isCacheEnabled()); + $this->assertEquals(60 * 60, $api->getCache()->getLifetime()); + } + + public function testStatusMethods() + { + $responses = [ + new MockResponse('OK'), + new MockResponse('ERROR', ['http_code' => 500]), + new MockResponse('REDIRECT', ['http_code' => 301, 'redirect_url' => 'http://example.com/new-url']), + ]; + + $api = new Rest('http://example.com/'); + $api->setHttpClient(new MockHttpClient($responses)); + $api->suppressErrors(); + + $response = $api->get('test'); + $this->assertTrue($response->isSuccessful()); + $this->assertFalse($response->isFailed()); + $this->assertFalse($response->isRedirect()); + + $response = $api->get('failed'); + $this->assertFalse($response->isSuccessful()); + $this->assertTrue($response->isFailed()); + $this->assertFalse($response->isRedirect()); + + $response = $api->get('redirect'); + $this->assertFalse($response->isSuccessful()); + $this->assertTrue($response->isFailed()); + $this->assertTrue($response->isRedirect()); + $this->assertEquals('http://example.com/new-url', $response->getRedirectUrl()); + } + public function testCacheableRequest() { $api = new Rest('http://example.com/'); diff --git a/tests/PermissionsTest.php b/tests/PermissionsTest.php index 81ce6ac..d55a487 100755 --- a/tests/PermissionsTest.php +++ b/tests/PermissionsTest.php @@ -7,7 +7,7 @@ use PHPUnit\Framework\TestCase; use Strata\Data\Permissions; -final class PermissionTest extends TestCase +final class PermissionsTest extends TestCase { public function testDefaultValues() { diff --git a/tests/Query/QueryManagerTest.php b/tests/Query/QueryManagerTest.php index 90fbea0..cf80db4 100644 --- a/tests/Query/QueryManagerTest.php +++ b/tests/Query/QueryManagerTest.php @@ -561,14 +561,17 @@ public function testSwitchHttpHeaders() ])); $manager->add('query3', (new Query())->setUri('path3')); + /** @var MockResponse $mockRequest */ $mockRequest = $manager->getResponse('query1')->getDecorated(); $authorizationRequestHeader = $mockRequest->getRequestOptions()['normalized_headers']['authorization'][0]; $this->assertSame('Authorization: Bearer ABC123', $authorizationRequestHeader); + /** @var MockResponse $mockRequest */ $mockRequest = $manager->getResponse('query2')->getDecorated(); $authorizationRequestHeader = $mockRequest->getRequestOptions()['normalized_headers']['authorization'][0]; $this->assertSame('Authorization: Bearer DEF456', $authorizationRequestHeader); + /** @var MockResponse $mockRequest */ $mockRequest = $manager->getResponse('query3')->getDecorated(); $authorizationRequestHeader = $mockRequest->getRequestOptions()['normalized_headers']['authorization'][0]; $this->assertSame('Authorization: Bearer ABC123', $authorizationRequestHeader); diff --git a/tests/Query/QueryTest.php b/tests/Query/QueryTest.php index dcb5d00..ee08681 100644 --- a/tests/Query/QueryTest.php +++ b/tests/Query/QueryTest.php @@ -18,6 +18,19 @@ class QueryTest extends TestCase { const CACHE_DIR = __DIR__ . '/../Cache/cache'; + public function testCreate() + { + $query = Query::create(); + $this->assertInstanceOf(Query::class, $query); + } + + public function testUri() + { + $query = Query::uri('test'); + $this->assertInstanceOf(Query::class, $query); + $this->assertEquals('test', $query->getUri()); + } + public function testSetDataProvider() { $query = new Query(); @@ -42,9 +55,9 @@ public function testRepeatQuery() $api = new Rest('https://example.com/'); $api->setHttpClient($http); - $query = new Query(); - $query->setUri('test') - ->setDataProvider($api) + $query = Query::create() + ->setUri('test') + ->setDataProvider($api) ; // Responses should be different diff --git a/tests/Traits/CheckPermissionsTraitTest.php b/tests/Traits/CheckPermissionsTraitTest.php index c687731..34d23d9 100644 --- a/tests/Traits/CheckPermissionsTraitTest.php +++ b/tests/Traits/CheckPermissionsTraitTest.php @@ -14,7 +14,7 @@ class TestPermissions use CheckPermissionsTrait; } -final class CheckPermissionsTest extends TestCase +final class CheckPermissionsTraitTest extends TestCase { public function testPermissionMethods() { diff --git a/tests/Traits/FlagsTraitTest.php b/tests/Traits/FlagsTraitTest.php index f84b032..5a48b78 100644 --- a/tests/Traits/FlagsTraitTest.php +++ b/tests/Traits/FlagsTraitTest.php @@ -18,7 +18,7 @@ class TestFlags const OPTION_E = 16; } -final class FlagsTest extends TestCase +final class FlagsTraitTest extends TestCase { public function testFlags() { diff --git a/tests/Validate/Rule/ArrayRuleTest.php b/tests/Validate/Rule/ArrayRuleTest.php index 18d390f..e3692e5 100755 --- a/tests/Validate/Rule/ArrayRuleTest.php +++ b/tests/Validate/Rule/ArrayRuleTest.php @@ -20,7 +20,8 @@ public function testValidationRule() $this->assertTrue($validator->validate($data)); $data = 'string content'; + $this->expectException(\TypeError::class); + // @phpstan-ignore-next-line $this->assertFalse($validator->validate($data)); - $this->assertStringContainsString('is not an array', $validator->getErrorMessage()); } } diff --git a/tests/Validate/Rule/EmailRuleTest.php b/tests/Validate/Rule/EmailRuleTest.php index 3ee76f7..efd5e3a 100755 --- a/tests/Validate/Rule/EmailRuleTest.php +++ b/tests/Validate/Rule/EmailRuleTest.php @@ -31,7 +31,7 @@ public function testInvalid(string $email) $this->assertFalse($validator->validate($data)); } - public function validDataProvider() + public static function validDataProvider() { return [ ['name@domain.com'], @@ -39,7 +39,7 @@ public function validDataProvider() ]; } - public function invalidDataProvider() + public static function invalidDataProvider() { return [ ['invalid @ domain.com'], diff --git a/tests/Validate/Rule/NumberRuleTest.php b/tests/Validate/Rule/NumberRuleTest.php index c27a000..d6a1c57 100755 --- a/tests/Validate/Rule/NumberRuleTest.php +++ b/tests/Validate/Rule/NumberRuleTest.php @@ -31,7 +31,7 @@ public function testInvalid($number) $this->assertFalse($validator->validate($data)); } - public function validDataProvider() + public static function validDataProvider() { return [ [1], @@ -40,7 +40,7 @@ public function validDataProvider() ]; } - public function invalidDataProvider() + public static function invalidDataProvider() { return [ ['one'], diff --git a/tests/Validate/Rule/RequiredRuleTest.php b/tests/Validate/Rule/RequiredRuleTest.php index ecb1043..c3b4c15 100755 --- a/tests/Validate/Rule/RequiredRuleTest.php +++ b/tests/Validate/Rule/RequiredRuleTest.php @@ -31,7 +31,7 @@ public function testInvalid(string $propertyPath) $this->assertFalse($validator->validate($data)); } - public function validDataProvider() + public static function validDataProvider() { return [ ['[email]'], @@ -39,7 +39,7 @@ public function validDataProvider() ]; } - public function invalidDataProvider() + public static function invalidDataProvider() { return [ ['[category]'],