diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ac9da56 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci +on: + push: + branches: [master] +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c36c42 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.cache diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 56603a9..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Innmind - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bdb3c7c --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +# This command is intended to be run on your computer +serve: + docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material + +build: + docker run --rm -it -v ${PWD}:/docs squidfunk/mkdocs-material build diff --git a/README.md b/README.md index 84c8fa4..daa304c 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,5 @@ # Innmind documentation -Most [packages](packages.md) you'll find in this organization are here to advance the organization [vision](vision.md). All of them are designed with the same [principles](design_choices.md) to simplify integration and context switching. +You can find the built version of this documentation at https://innmind.github.io/documentation/. -You can view the dependencies between the packages via this [macOS application](https://github.com/Innmind/macOS-tooling). - -> **Note** However some are here to solve recurring problems you may find in a professional context, or for intellectual curiosity. - -## Use cases - -- [Upload a local file via HTTP](use_cases/upload_local_file.md) -- [Copy a local directory to S3](use_cases/copy_local_directory_to_s3.md) -- [Serve a S3 file via an HTTP server](use_cases/serve_s3_file.md) -- [Persist a SQL result to a file](use_cases/persist_sql_result_to_file.md) -- [Persist crawled links to a database](use_cases/persist_crawled_links_to_database.md) +To view it on your machine, pull the repository and run `make serve` that will open the page http://0.0.0.0:8000/ diff --git a/design_choices.md b/design_choices.md deleted file mode 100644 index acc3634..0000000 --- a/design_choices.md +++ /dev/null @@ -1,30 +0,0 @@ -# Design choices - -## Core principle - -> make it easy to use it right, make it hard to use it wrong - -This is achieved by always following the same steps when building any package: -- make it work -- make it simple -- make it fast - -> **Note** However the last step is not a priority as there is still plenty of packages to build in order to reach the organization [vision](vision.md). But this doesn't mean speed is sacrified for the sake of simplicity. - -## Simplicity - -To keep the complexity low, packages use a mix of Functional Programming, Object Oriented Programming and a low usage of primitive types. - -Functional Programming allows to increase code robustness and testability by eliminating [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)) and using precise types allowing the use of [static analysis](https://psalm.dev/docs/). - -Object Oriented Programming allows to deal with side effects (filesystem, network, etc...) with less convolutions than Functional Programming. - -Primitive types (`int`, `string`, etc...) usage is very low as it generally brings too many implicits. [Value Object](https://en.wikipedia.org/wiki/Value_object)s are preferred as they alleviate those implicits. - -## Abstractions - -To reach the organization [vision](vision.md), abstractions need to compose. - -To keep the higher order abstractions complexity low, they use a declarative approach. The APIs focus on what we want to achieve and hides how to achieve it. This allows the abstraction to run in different environments without affecting the user code. - -In order to achieve this, the packages use a _capability-based approach_ instead of [ambient authority](https://en.wikipedia.org/wiki/Ambient_authority) (that is generally used in the PHP ecosystem). In other words this means using the [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) principle for everything outside of the process (time, filesystem, network, etc...). diff --git a/docs/assets/dependency-graph/dependencies.svg b/docs/assets/dependency-graph/dependencies.svg new file mode 100644 index 0000000..844c360 --- /dev/null +++ b/docs/assets/dependency-graph/dependencies.svg @@ -0,0 +1,792 @@ + + + + + + +packages + + +cluster_composer + + +composer + + + + +cluster_guzzlehttp + + +guzzlehttp + + + + +cluster_innmind + + +innmind + + + + +cluster_league + + +league + + + + +cluster_paragonie + + +paragonie + + + + +cluster_psr + + +psr + + + + +cluster_ralouphie + + +ralouphie + + + + +cluster_ramsey + + +ramsey + + + + +cluster_symfony + + +symfony + + + + + +composer__semver + + +semver@1.4.2 + + + + + +guzzlehttp__guzzle + + +guzzle@6.3.3 + + + + + +guzzlehttp__promises + + +promises@v1.3.1 + + + + + +guzzlehttp__guzzle->guzzlehttp__promises + + +^1.0 + + + +guzzlehttp__psr7 + + +psr7@1.5.2 + + + + + +guzzlehttp__guzzle->guzzlehttp__psr7 + + +^1.4 + + + +psr__http_message + + +http-message@1.0.1 + + + + + +guzzlehttp__psr7->psr__http_message + + +~1.0 + + + +ralouphie__getallheaders + + +getallheaders@2.0.5 + + + + + +guzzlehttp__psr7->ralouphie__getallheaders + + +^2.0.5 + + + +innmind__cli + + +cli@1.5.1 + + + + + +innmind__immutable + + +immutable@2.13.1 + + + + + +innmind__cli->innmind__immutable + + +^2.7 + + + +innmind__operating_system + + +operating-system@1.3.0 + + + + + +innmind__cli->innmind__operating_system + + +^1.0 + + + +innmind__stream + + +stream@1.4.0 + + + + + +innmind__cli->innmind__stream + + +^1.3 + + + +innmind__time_warp + + +time-warp@1.0.0 + + + + + +innmind__cli->innmind__time_warp + + +^1.0 + + + +innmind__url + + +url@2.0.3 + + + + + +innmind__cli->innmind__url + + +^2.0 + + + +innmind__colour + + +colour@2.1.0 + + + + + +innmind__colour->innmind__immutable + + +~2.10 + + + +innmind__filesystem + + +filesystem@3.3.0 + + + + + +innmind__filesystem->innmind__immutable + + +~2.10 + + + +innmind__filesystem->innmind__stream + + +^1.3 + + + +symfony__filesystem + + +filesystem@v4.2.2 + + + + + +innmind__filesystem->symfony__filesystem + + +^3.0|~4.0 + + + +symfony__finder + + +finder@v4.2.2 + + + + + +innmind__filesystem->symfony__finder + + +~3.0|~4.0 + + + +innmind__graphviz + + +graphviz@1.2.1 + + + + + +innmind__graphviz->innmind__colour + + +^2.0 + + + +innmind__graphviz->innmind__immutable + + +^2.6 + + + +innmind__graphviz->innmind__stream + + +^1.4 + + + +innmind__graphviz->innmind__url + + +^2.0 + + + +innmind__http + + +http@3.8.3 + + + + + +innmind__http->innmind__filesystem + + +~3.0 + + + +innmind__http->innmind__immutable + + +~2.1 + + + +innmind__http->innmind__stream + + +^1.3 + + + +innmind__time_continuum + + +time-continuum@1.3.0 + + + + + +innmind__http->innmind__time_continuum + + +^1.0 + + + +innmind__http->innmind__url + + +~2.0 + + + +innmind__http_transport + + +http-transport@4.0.0 + + + + + +innmind__http_transport->guzzlehttp__guzzle + + +^6.2 + + + +innmind__http_transport->innmind__http + + +~3.0 + + + +psr__log + + +log@1.1.0 + + + + + +innmind__http_transport->psr__log + + +^1.0 + + + +ramsey__uuid + + +uuid@3.8.0 + + + + + +innmind__http_transport->ramsey__uuid + + +^3.5 + + + +innmind__json + + +json@1.1.0 + + + + + +innmind__operating_system->innmind__time_continuum + + +^1.2 + + + +innmind__server_control + + +server-control@2.7.0 + + + + + +innmind__server_control->innmind__immutable + + +^2.10 + + + +innmind__server_control->innmind__stream + + +^1.3 + + + +innmind__server_control->innmind__url + + +^2.0 + + + +innmind__server_control->psr__log + + +^1.0 + + + +symfony__process + + +process@v4.2.2 + + + + + +innmind__server_control->symfony__process + + +^3.2|~4.0 + + + +innmind__stream->innmind__immutable + + +^2.3 + + + +innmind__stream->innmind__time_continuum + + +^1.0 + + + +innmind__time_warp->innmind__time_continuum + + +^1.2 + + + +innmind__url->innmind__immutable + + +~2.0 + + + +league__uri + + +uri@5.3.0 + + + + + +innmind__url->league__uri + + +~5.0 + + + +league__uri_components + + +uri-components@1.8.2 + + + + + +league__uri->league__uri_components + + +^1.8 + + + +league__uri_hostname_parser + + +uri-hostname-parser@1.1.1 + + + + + +league__uri->league__uri_hostname_parser + + +^1.1 + + + +league__uri_interfaces + + +uri-interfaces@1.1.1 + + + + + +league__uri->league__uri_interfaces + + +^1.0 + + + +league__uri_manipulations + + +uri-manipulations@1.5.0 + + + + + +league__uri->league__uri_manipulations + + +^1.5 + + + +league__uri_parser + + +uri-parser@1.4.1 + + + + + +league__uri->league__uri_parser + + +^1.4 + + + +league__uri_schemes + + +uri-schemes@1.2.1 + + + + + +league__uri->league__uri_schemes + + +^1.2 + + + +league__uri->psr__http_message + + +^1.0 + + + +league__uri_components->league__uri_hostname_parser + + +^1.1.0 + + + +psr__simple_cache + + +simple-cache@1.0.1 + + + + + +league__uri_hostname_parser->psr__simple_cache + + +^1 + + + +league__uri_manipulations->league__uri_components + + +^1.8.0 + + + +league__uri_manipulations->league__uri_interfaces + + +^1.0 + + + +league__uri_manipulations->psr__http_message + + +^1.0 + + + +league__uri_schemes->league__uri_interfaces + + +^1.1 + + + +league__uri_schemes->league__uri_parser + + +^1.4.0 + + + +league__uri_schemes->psr__http_message + + +^1.0 + + + +paragonie__random_compat + + +random_compat@v9.99.99 + + + + + +ramsey__uuid->paragonie__random_compat + + +^1.0|^2.0|9.99.99 + + + +symfony__polyfill_ctype + + +polyfill-ctype@v1.10.0 + + + + + +ramsey__uuid->symfony__polyfill_ctype + + +^1.8 + + + +symfony__filesystem->symfony__polyfill_ctype + + +~1.8 + + + diff --git a/docs/assets/dependency-graph/innmind.svg b/docs/assets/dependency-graph/innmind.svg new file mode 100644 index 0000000..b34c03e --- /dev/null +++ b/docs/assets/dependency-graph/innmind.svg @@ -0,0 +1,3786 @@ + + + + + + +packages + + +cluster_innmind + + +innmind + + + + +cluster_ramsey + + +ramsey + + + + +cluster_ovh + + +ovh + + + + +cluster_symfony + + +symfony + + + + +cluster_phpunit + + +phpunit + + + + +cluster_friendsofphp + + +friendsofphp + + + + +cluster_monolog + + +monolog + + + + +cluster_composer + + +composer + + + + +cluster_doctrine + + +doctrine + + + + +cluster_psr + + +psr + + + + +cluster_music_companion + + +music-companion + + + + +cluster_jeremykendall + + +jeremykendall + + + + +cluster_formal + + +formal + + + + +cluster_twig + + +twig + + + + +cluster_league + + +league + + + + + +innmind__immutable + + +immutable@5.3.0 + + + + + +innmind__acl + + +acl@3.1.0 + + + + + +innmind__acl->innmind__immutable + + +~4.0|~5.0 + + + +innmind__time_continuum + + +time-continuum@3.4.1 + + + + + +innmind__time_continuum->innmind__immutable + + +~4.0|~5.0 + + + +psr__log + + +log@3.0.0 + + + + + +innmind__time_continuum->psr__log + + +~3.0 + + + +innmind__math + + +math@6.1.0 + + + + + +innmind__math->innmind__immutable + + +~4.15|~5.0 + + + +innmind__url + + +url@4.3.0 + + + + + +innmind__url->innmind__immutable + + +~4.15|~5.0 + + + +league__uri_parser + + +uri-parser@1.4.1 + + + + + +innmind__url->league__uri_parser + + +~1.2 + + + +league__uri_components + + +uri-components@7.4.1 + + + + + +innmind__url->league__uri_components + + +~2.0 + + + +innmind__operating_system + + +operating-system@5.0.0 + + + + + +innmind__operating_system->innmind__time_continuum + + +~3.0 + + + +innmind__filesystem + + +filesystem@7.5.1 + + + + + +innmind__operating_system->innmind__filesystem + + +~7.1 + + + +innmind__stream + + +stream@4.2.0 + + + + + +innmind__operating_system->innmind__stream + + +~4.0 + + + +innmind__io + + +io@2.7.0 + + + + + +innmind__operating_system->innmind__io + + +~2.7 + + + +innmind__http_transport + + +http-transport@7.2.1 + + + + + +innmind__operating_system->innmind__http_transport + + +~7.2 + + + +innmind__server_control + + +server-control@5.2.1 + + + + + +innmind__operating_system->innmind__server_control + + +~5.0 + + + +innmind__time_warp + + +time-warp@3.0.0 + + + + + +innmind__operating_system->innmind__time_warp + + +~3.0 + + + +innmind__file_watch + + +file-watch@4.0.0 + + + + + +innmind__operating_system->innmind__file_watch + + +~4.0 + + + +innmind__server_status + + +server-status@4.1.0 + + + + + +innmind__operating_system->innmind__server_status + + +~4.0 + + + +innmind__socket + + +socket@6.1.0 + + + + + +innmind__operating_system->innmind__socket + + +~6.0 + + + +innmind__signals + + +signals@3.1.0 + + + + + +innmind__operating_system->innmind__signals + + +~3.0 + + + +formal__access_layer + + +access-layer@2.15.0 + + + + + +innmind__operating_system->formal__access_layer + + +^2.0 + + + +innmind__media_type + + +media-type@2.2.0 + + + + + +innmind__media_type->innmind__immutable + + +~4.15|~5.0 + + + +innmind__filesystem->innmind__immutable + + +~4.15|~5.0 + + + +innmind__filesystem->innmind__time_continuum + + +~3.4 + + + +innmind__filesystem->innmind__url + + +~4.2 + + + +innmind__filesystem->innmind__media_type + + +~2.1 + + + +innmind__filesystem->innmind__stream + + +~4.1 + + + +innmind__filesystem->innmind__io + + +~2.2 + + + +symfony__filesystem + + +filesystem@v7.0.7 + + + + + +innmind__filesystem->symfony__filesystem + + +~6.0|~7.0 + + + +innmind__filesystem->psr__log + + +~3.0 + + + +innmind__stream->innmind__immutable + + +~4.15|~5.0 + + + +innmind__stream->innmind__time_continuum + + +~3.3 + + + +innmind__stream->innmind__url + + +~4.2 + + + +innmind__stream->psr__log + + +~3.0 + + + +innmind__io->innmind__immutable + + +~5.2 + + + +innmind__io->innmind__stream + + +~4.0 + + + +innmind__io->innmind__socket + + +~6.1 + + + +innmind__amqp + + +amqp@5.0.0 + + + + + +innmind__amqp->innmind__immutable + + +~5.2 + + + +innmind__amqp->innmind__time_continuum + + +~3.1 + + + +innmind__amqp->innmind__math + + +~6.0 + + + +innmind__amqp->innmind__url + + +~4.1 + + + +innmind__amqp->innmind__operating_system + + +~5.0 + + + +innmind__amqp->innmind__media_type + + +~2.0 + + + +innmind__amqp->innmind__filesystem + + +~7.0 + + + +innmind__amqp->innmind__stream + + +~4.0 + + + +innmind__amqp->innmind__io + + +~2.6 + + + +ramsey__uuid + + +uuid@4.7.6 + + + + + +innmind__amqp->ramsey__uuid + + +~4.0 + + + +innmind__scaleway_sdk + + +scaleway-sdk@2.2.0 + + + + + +innmind__scaleway_sdk->innmind__immutable + + +~3.3 + + + +innmind__scaleway_sdk->innmind__time_continuum + + +~2.0 + + + +innmind__scaleway_sdk->innmind__url + + +^3.5.1 + + + +innmind__scaleway_sdk->innmind__filesystem + + +~4.0 + + + +innmind__json + + +json@1.4.0 + + + + + +innmind__scaleway_sdk->innmind__json + + +^1.1 + + + +innmind__scaleway_sdk->innmind__http_transport + + +~5.0 + + + +innmind__ip + + +ip@3.2.0 + + + + + +innmind__scaleway_sdk->innmind__ip + + +~2.0 + + + +innmind__scaleway_sdk->ramsey__uuid + + +^3.8|^4.0 + + + +innmind__ssh_key_provider + + +ssh-key-provider@3.2.0 + + + + + +innmind__ssh_key_provider->innmind__immutable + + +~4.9|~5.0 + + + +innmind__ssh_key_provider->innmind__filesystem + + +~7.1 + + + +innmind__ssh_key_provider->innmind__http_transport + + +~7.0 + + + +innmind__ark + + +ark@3.1.0 + + + + + +innmind__ark->innmind__immutable + + +~3.5 + + + +innmind__ark->innmind__url + + +~3.3 + + + +innmind__ark->innmind__operating_system + + +~2.0 + + + +innmind__ark->innmind__scaleway_sdk + + +~2.0 + + + +innmind__ark->innmind__ssh_key_provider + + +~2.0 + + + +innmind__ark->ramsey__uuid + + +^3.8 + + + +ovh__ovh + + +ovh@v3.3.0 + + + + + +innmind__ark->ovh__ovh + + +^2.0 + + + +innmind__mantle + + +mantle@2.1.0 + + + + + +innmind__mantle->innmind__immutable + + +~5.2 + + + +innmind__mantle->innmind__operating_system + + +~4.1|~5.0 + + + +innmind__mantle->innmind__filesystem + + +~7.3 + + + +innmind__http_parser + + +http-parser@2.1.0 + + + + + +innmind__http_parser->innmind__immutable + + +~5.2 + + + +innmind__http_parser->innmind__time_continuum + + +~3.2 + + + +innmind__http_parser->innmind__stream + + +~4.0 + + + +innmind__http_parser->innmind__io + + +~2.7 + + + +innmind__http + + +http@7.0.1 + + + + + +innmind__http_parser->innmind__http + + +~7.0 + + + +innmind__cli + + +cli@3.6.0 + + + + + +innmind__cli->innmind__immutable + + +~4.15|~5.0 + + + +innmind__cli->innmind__url + + +~4.0 + + + +innmind__cli->innmind__operating_system + + +~4.0|~5.0 + + + +innmind__cli->innmind__stream + + +~4.0 + + + +innmind__stack_trace + + +stack-trace@4.1.0 + + + + + +innmind__cli->innmind__stack_trace + + +~4.0 + + + +innmind__async_http_server + + +async-http-server@3.0.0 + + + + + +innmind__async_http_server->innmind__operating_system + + +~5.0 + + + +innmind__async_http_server->innmind__io + + +~2.7 + + + +innmind__async_http_server->innmind__mantle + + +~2.0 + + + +innmind__async_http_server->innmind__http_parser + + +~2.1 + + + +innmind__async_http_server->innmind__cli + + +^3.3 + + + +innmind__json->innmind__immutable + + +~5.3 + + + +innmind__black_box + + +black-box@5.6.3 + + + + + +innmind__black_box->innmind__json + + +^1.1 + + + +symfony__var_dumper + + +var-dumper@v7.0.7 + + + + + +innmind__black_box->symfony__var_dumper + + +~6.0|~7.0 + + + +phpunit__phpunit + + +phpunit@11.1.3 + + + + + +innmind__black_box->phpunit__phpunit + + +~10.0 + + + +phpunit__php_timer + + +php-timer@7.0.0 + + + + + +innmind__black_box->phpunit__php_timer + + +^6.0 + + + +phpunit__php_code_coverage + + +php-code-coverage@11.0.3 + + + + + +innmind__black_box->phpunit__php_code_coverage + + +^10.1 + + + +innmind__black_box_symfony + + +black-box-symfony@1.2.0 + + + + + +innmind__black_box_symfony->innmind__black_box + + +~5.3 + + + +symfony__framework_bundle + + +framework-bundle@v7.0.7 + + + + + +innmind__black_box_symfony->symfony__framework_bundle + + +~6.0|~7.0 + + + +symfony__browser_kit + + +browser-kit@v7.0.7 + + + + + +innmind__black_box_symfony->symfony__browser_kit + + +~6.0|~7.0 + + + +symfony__http_foundation + + +http-foundation@v7.0.7 + + + + + +innmind__black_box_symfony->symfony__http_foundation + + +~6.0|~7.0 + + + +symfony__http_kernel + + +http-kernel@v7.0.7 + + + + + +innmind__black_box_symfony->symfony__http_kernel + + +~6.0|~7.0 + + + +innmind__stack_trace->innmind__immutable + + +~4.1|~5.0 + + + +innmind__stack_trace->innmind__url + + +~4.0 + + + +innmind__graphviz + + +graphviz@3.4.0 + + + + + +innmind__stack_trace->innmind__graphviz + + +~3.1 + + + +innmind__coding_standard + + +coding-standard@2.0.1 + + + + + +friendsofphp__php_cs_fixer + + +php-cs-fixer@v3.56.0 + + + + + +innmind__coding_standard->friendsofphp__php_cs_fixer + + +~3.13 + + + +innmind__colour + + +colour@4.2.0 + + + + + +innmind__colour->innmind__immutable + + +~4.0|~5.0 + + + +innmind__http->innmind__immutable + + +~4.15|~5.0 + + + +innmind__http->innmind__time_continuum + + +~3.0 + + + +innmind__http->innmind__url + + +~4.0 + + + +innmind__http->innmind__media_type + + +^2.0.1 + + + +innmind__http->innmind__filesystem + + +~7.0 + + + +innmind__http->innmind__stream + + +~4.0 + + + +innmind__http->innmind__io + + +~2.2 + + + +innmind__http->ramsey__uuid + + +~4.7 + + + +innmind__url_resolver + + +url-resolver@5.1.0 + + + + + +innmind__url_resolver->innmind__immutable + + +~4.0|~5.0 + + + +innmind__url_resolver->innmind__url + + +~4.0 + + + +innmind__html + + +html@6.3.0 + + + + + +innmind__html->innmind__url + + +~4.0 + + + +innmind__html->innmind__filesystem + + +~7.1 + + + +innmind__xml + + +xml@7.6.0 + + + + + +innmind__html->innmind__xml + + +~7.6 + + + +symfony__dom_crawler + + +dom-crawler@v7.0.7 + + + + + +innmind__html->symfony__dom_crawler + + +~6.3|~7.0 + + + +innmind__http_transport->innmind__immutable + + +~4.15|~5.0 + + + +innmind__http_transport->innmind__time_continuum + + +~3.0 + + + +innmind__http_transport->innmind__url + + +~4.0 + + + +innmind__http_transport->innmind__filesystem + + +~7.1 + + + +innmind__http_transport->innmind__stream + + +~4.0 + + + +innmind__http_transport->innmind__io + + +~2.2 + + + +innmind__http_transport->innmind__http + + +~7.0 + + + +innmind__http_transport->innmind__time_warp + + +~3.0 + + + +innmind__http_transport->ramsey__uuid + + +^4.7 + + + +innmind__http_transport->psr__log + + +~3.0 + + + +innmind__crawler + + +crawler@6.1.0 + + + + + +innmind__crawler->innmind__immutable + + +~3.3 + + + +innmind__crawler->innmind__time_continuum + + +~2.0 + + + +innmind__crawler->innmind__math + + +~5.0 + + + +innmind__crawler->innmind__filesystem + + +~4.0 + + + +innmind__crawler->innmind__colour + + +~3.0 + + + +innmind__crawler->innmind__http + + +~4.0 + + + +innmind__crawler->innmind__url_resolver + + +~4.0 + + + +innmind__crawler->innmind__html + + +~5.0 + + + +innmind__crawler->innmind__http_transport + + +~5.0 + + + +innmind__robots_txt + + +robots-txt@6.2.0 + + + + + +innmind__robots_txt->innmind__immutable + + +~4.13|~5.0 + + + +innmind__robots_txt->innmind__url + + +~4.1 + + + +innmind__robots_txt->innmind__http_transport + + +~7.0 + + + +innmind__logger + + +logger@2.1.0 + + + + + +innmind__logger->innmind__url + + +~3.0 + + + +monolog__monolog + + +monolog@3.6.0 + + + + + +innmind__logger->monolog__monolog + + +~2.0 + + + +innmind__logger->psr__log + + +^1.0 + + + +innmind__rest_client + + +rest-client@8.1.0 + + + + + +innmind__homeostasis + + +homeostasis@4.1.0 + + + + + +innmind__homeostasis->innmind__immutable + + +~3.5 + + + +innmind__homeostasis->innmind__time_continuum + + +~2.2 + + + +innmind__homeostasis->innmind__math + + +~5.0 + + + +innmind__homeostasis->innmind__filesystem + + +~4.0 + + + +innmind__homeostasis->innmind__json + + +^1.2 + + + +innmind__homeostasis->innmind__server_status + + +~2.0 + + + +innmind__log_reader + + +log-reader@5.3.0 + + + + + +innmind__homeostasis->innmind__log_reader + + +~4.0 + + + +innmind__installation_monitor + + +installation-monitor@3.1.0 + + + + + +innmind__installation_monitor->innmind__immutable + + +~3.5 + + + +innmind__installation_monitor->innmind__operating_system + + +~2.0 + + + +innmind__installation_monitor->innmind__cli + + +~2.0 + + + +innmind__installation_monitor->innmind__json + + +^1.0 + + + +innmind__silent_cartographer + + +silent-cartographer@2.2.0 + + + + + +innmind__installation_monitor->innmind__silent_cartographer + + +~2.0 + + + +innmind__ipc + + +ipc@4.4.0 + + + + + +innmind__installation_monitor->innmind__ipc + + +~3.0 + + + +innmind__silent_cartographer->innmind__immutable + + +~3.5 + + + +innmind__silent_cartographer->innmind__url + + +~3.3 + + + +innmind__silent_cartographer->innmind__operating_system + + +~2.0 + + + +innmind__silent_cartographer->innmind__cli + + +~2.0 + + + +innmind__silent_cartographer->innmind__json + + +^1.1 + + + +innmind__silent_cartographer->innmind__ipc + + +~3.0 + + + +innmind__stack + + +stack@1.2.0 + + + + + +innmind__ipc->innmind__immutable + + +~4.15|~5.0 + + + +innmind__ipc->innmind__url + + +~4.0 + + + +innmind__ipc->innmind__operating_system + + +~5.0 + + + +innmind__ipc->innmind__media_type + + +~2.0 + + + +innmind__ipc->innmind__server_control + + +~5.0 + + + +innmind__ipc->innmind__socket + + +~6.0 + + + +innmind__genome + + +genome@3.1.0 + + + + + +innmind__genome->innmind__operating_system + + +^2.1 + + + +innmind__cli_framework + + +cli-framework@1.4.0 + + + + + +innmind__genome->innmind__cli_framework + + +^1.2 + + + +innmind__crawler_app + + +crawler-app@1.5.2 + + + + + +innmind__crawler_app->innmind__operating_system + + +~2.0 + + + +innmind__crawler_app->innmind__amqp + + +~3.0 + + + +innmind__crawler_app->innmind__cli + + +~2.0 + + + +innmind__crawler_app->innmind__json + + +^1.1 + + + +innmind__crawler_app->innmind__crawler + + +~6.0 + + + +innmind__crawler_app->innmind__robots_txt + + +~5.0 + + + +innmind__crawler_app->innmind__logger + + +~2.0 + + + +innmind__crawler_app->innmind__rest_client + + +~8.0 + + + +innmind__crawler_app->innmind__homeostasis + + +~4.0 + + + +innmind__crawler_app->innmind__installation_monitor + + +~3.0 + + + +innmind__crawler_app->innmind__silent_cartographer + + +~2.0 + + + +innmind__crawler_app->innmind__stack + + +^1.0 + + + +innmind__crawler_app->innmind__ipc + + +~3.0 + + + +innmind__crawler_app->innmind__genome + + +^3.0 + + + +innmind__crawler_app->innmind__cli_framework + + +^1.2 + + + +symfony__dotenv + + +dotenv@v7.0.7 + + + + + +innmind__crawler_app->symfony__dotenv + + +~5.0 + + + +innmind__crawler_app->monolog__monolog + + +~2.0 + + + +innmind__server_control->innmind__immutable + + +~4.15|~5.0 + + + +innmind__server_control->innmind__time_continuum + + +^3.1,<3.3|^3.4.1 + + + +innmind__server_control->innmind__url + + +~4.0 + + + +innmind__server_control->innmind__filesystem + + +~7.0 + + + +innmind__server_control->innmind__stream + + +~4.0 + + + +innmind__server_control->innmind__time_warp + + +^3.0 + + + +innmind__server_control->psr__log + + +~3.0 + + + +innmind__cron + + +cron@3.2.0 + + + + + +innmind__cron->innmind__immutable + + +~4.2|~5.0 + + + +innmind__cron->innmind__server_control + + +~4.1|~5.0 + + + +innmind__profiler + + +profiler@4.1.0 + + + + + +innmind__profiler->innmind__immutable + + +~5.2 + + + +innmind__profiler->innmind__operating_system + + +~4.1|~5.0 + + + +innmind__profiler->innmind__json + + +^1.3 + + + +innmind__profiler->innmind__html + + +~6.2 + + + +innmind__framework + + +framework@2.2.0 + + + + + +innmind__profiler->innmind__framework + + +~2.0 + + + +innmind__url_template + + +url-template@3.1.0 + + + + + +innmind__profiler->innmind__url_template + + +^3.0 + + + +innmind__profiler->ramsey__uuid + + +~4.7 + + + +innmind__framework->innmind__immutable + + +~5.2 + + + +innmind__framework->innmind__url + + +^4.1 + + + +innmind__framework->innmind__operating_system + + +~4.1|~5.0 + + + +innmind__framework->innmind__filesystem + + +~7.0 + + + +innmind__framework->innmind__cli + + +^3.1 + + + +innmind__di + + +di@2.1.0 + + + + + +innmind__framework->innmind__di + + +~2.1 + + + +innmind__http_server + + +http-server@4.1.0 + + + + + +innmind__framework->innmind__http_server + + +~4.0 + + + +innmind__router + + +router@4.1.0 + + + + + +innmind__framework->innmind__router + + +~4.1 + + + +innmind__framework->ramsey__uuid + + +^4.7 + + + +innmind__object_graph + + +object-graph@3.2.0 + + + + + +innmind__object_graph->innmind__immutable + + +~4.13|~5.0 + + + +innmind__object_graph->innmind__url + + +~4.1 + + + +innmind__object_graph->innmind__graphviz + + +~3.2 + + + +innmind__reflection + + +reflection@5.2.0 + + + + + +innmind__object_graph->innmind__reflection + + +~5.0 + + + +innmind__debug + + +debug@4.0.0 + + + + + +innmind__debug->innmind__stack_trace + + +~4.0 + + + +innmind__debug->innmind__http + + +~7.0 + + + +innmind__debug->innmind__profiler + + +~4.0 + + + +innmind__debug->innmind__framework + + +~2.0 + + + +innmind__debug->innmind__object_graph + + +~3.1 + + + +innmind__graphviz->innmind__immutable + + +~4.0|~5.0 + + + +innmind__graphviz->innmind__url + + +~4.0 + + + +innmind__graphviz->innmind__filesystem + + +~7.0 + + + +innmind__graphviz->innmind__colour + + +~4.0 + + + +innmind__dependency_graph + + +dependency-graph@3.6.0 + + + + + +innmind__dependency_graph->innmind__immutable + + +~5.2 + + + +innmind__dependency_graph->innmind__url + + +~4.1 + + + +innmind__dependency_graph->innmind__operating_system + + +~4.1|~5.0 + + + +innmind__dependency_graph->innmind__json + + +^1.1 + + + +innmind__dependency_graph->innmind__framework + + +~2.0 + + + +innmind__dependency_graph->innmind__graphviz + + +~3.1 + + + +composer__semver + + +semver@3.4.0 + + + + + +innmind__dependency_graph->composer__semver + + +~3.0 + + + +innmind__specification + + +specification@3.0.1 + + + + + +innmind__doctrine + + +doctrine@2.5.1 + + + + + +innmind__doctrine->innmind__immutable + + +~4.0|~5.0 + + + +innmind__doctrine->innmind__specification + + +~3.0 + + + +innmind__doctrine->ramsey__uuid + + +^4.0 + + + +doctrine__orm + + +orm@3.1.3 + + + + + +innmind__doctrine->doctrine__orm + + +^2.7 + + + +innmind__encoding + + +encoding@1.0.0 + + + + + +innmind__encoding->innmind__immutable + + +~5.1 + + + +innmind__encoding->innmind__time_continuum + + +~3.4 + + + +innmind__encoding->innmind__filesystem + + +~7.1 + + + +innmind__time_warp->innmind__time_continuum + + +~3.0 + + + +innmind__time_warp->psr__log + + +~3.0 + + + +innmind__file_watch->innmind__time_continuum + + +~3.0 + + + +innmind__file_watch->innmind__url + + +~4.0 + + + +innmind__file_watch->innmind__server_control + + +~5.0 + + + +innmind__file_watch->innmind__time_warp + + +~3.0 + + + +innmind__file_watch->psr__log + + +~3.0 + + + +innmind__http_server->innmind__operating_system + + +~4.0|~5.0 + + + +innmind__http_server->innmind__http + + +~7.0 + + + +innmind__router->innmind__immutable + + +~4.9|~5.0 + + + +innmind__router->innmind__url + + +~4.1 + + + +innmind__router->innmind__http + + +~7.0 + + + +innmind__router->innmind__url_template + + +~3.0 + + + +innmind__git + + +git@3.2.0 + + + + + +innmind__git->innmind__immutable + + +~4.0|~5.0 + + + +innmind__git->innmind__time_continuum + + +~3.0 + + + +innmind__git->innmind__url + + +~4.0 + + + +innmind__git->innmind__server_control + + +~4.1|~5.0 + + + +innmind__git_release + + +git-release@3.1.0 + + + + + +innmind__git_release->innmind__immutable + + +~4.2|~5.0 + + + +innmind__git_release->innmind__cli + + +~3.0 + + + +innmind__git_release->innmind__git + + +^3.0.2 + + + +innmind__hash + + +hash@1.5.0 + + + + + +innmind__hash->innmind__immutable + + +~4.5|~5.0 + + + +innmind__hash->innmind__filesystem + + +~7.0 + + + +innmind__server_status->innmind__immutable + + +~4.15|~5.0 + + + +innmind__server_status->innmind__time_continuum + + +~3.0 + + + +innmind__server_status->innmind__url + + +~4.0 + + + +innmind__server_status->innmind__server_control + + +~5.0 + + + +innmind__server_status->psr__log + + +~3.0 + + + +innmind__log_reader->innmind__immutable + + +~4.9|~5.0 + + + +innmind__log_reader->innmind__time_continuum + + +~3.1 + + + +innmind__log_reader->innmind__url + + +~4.1 + + + +innmind__log_reader->innmind__filesystem + + +~7.0 + + + +innmind__log_reader->innmind__json + + +^1.1 + + + +innmind__log_reader->innmind__http + + +~7.0 + + + +innmind__log_reader->psr__log + + +^3.0 + + + +innmind__xml->innmind__immutable + + +^4.7.1|~5.0 + + + +innmind__xml->innmind__filesystem + + +~7.0 + + + +innmind__http_authentication + + +http-authentication@4.0.0 + + + + + +innmind__http_authentication->innmind__http + + +~7.0 + + + +innmind__http_session + + +http-session@4.0.0 + + + + + +innmind__http_session->innmind__immutable + + +~4.9|~5.0 + + + +innmind__http_session->innmind__http + + +~7.0 + + + +innmind__infrastructure_amqp + + +infrastructure-amqp@3.1.0 + + + + + +innmind__infrastructure_amqp->innmind__genome + + +^3.0 + + + +innmind__rabbitmq_management + + +rabbitmq-management@3.2.0 + + + + + +innmind__infrastructure_amqp->innmind__rabbitmq_management + + +~2.0 + + + +innmind__infrastructure_neo4j + + +infrastructure-neo4j@3.1.0 + + + + + +innmind__infrastructure_neo4j->innmind__genome + + +^3.0 + + + +innmind__infrastructure_nginx + + +infrastructure-nginx@3.1.0 + + + + + +innmind__infrastructure_nginx->innmind__genome + + +^3.0 + + + +innmind__url_template->innmind__immutable + + +~4.3|~5.0 + + + +innmind__url_template->innmind__url + + +~4.1 + + + +innmind__infrastructure + + +infrastructure@1.3.0 + + + + + +innmind__infrastructure->innmind__crawler + + +^6.0 + + + +innmind__infrastructure->innmind__genome + + +^3.0 + + + +innmind__infrastructure->innmind__infrastructure_amqp + + +^3.0.2 + + + +innmind__infrastructure->innmind__infrastructure_neo4j + + +^3.0.2 + + + +innmind__infrastructure->innmind__infrastructure_nginx + + +^3.0.2 + + + +innmind__infrastructure->innmind__url_template + + +^2.0 + + + +innmind__rabbitmq_management->innmind__immutable + + +~4.5|~5.0 + + + +innmind__rabbitmq_management->innmind__time_continuum + + +~3.0 + + + +innmind__rabbitmq_management->innmind__url + + +~4.1 + + + +innmind__rabbitmq_management->innmind__server_control + + +~4.2|~5.0 + + + +innmind__socket->innmind__immutable + + +~4.0|~5.0 + + + +innmind__socket->innmind__url + + +~4.0 + + + +innmind__socket->innmind__stream + + +~4.0 + + + +innmind__socket->innmind__ip + + +~3.0 + + + +innmind__ip->innmind__immutable + + +~4.1|~5.0 + + + +innmind__validation + + +validation@1.4.0 + + + + + +innmind__validation->innmind__immutable + + +~5.3 + + + +innmind__validation->innmind__time_continuum + + +~3.4 + + + +innmind__kalmiya + + +kalmiya@2.0.0 + + + + + +innmind__kalmiya->innmind__ipc + + +~4.4 + + + +innmind__kalmiya->innmind__framework + + +~2.2 + + + +innmind__kalmiya->innmind__dependency_graph + + +~3.6 + + + +innmind__kalmiya->innmind__validation + + +^1.4 + + + +music_companion__apple_music + + +apple-music@4.0.0 + + + + + +innmind__kalmiya->music_companion__apple_music + + +~4.0 + + + +innmind__lab_station + + +lab-station@4.1.0 + + + + + +innmind__lab_station->innmind__immutable + + +~5.2 + + + +innmind__lab_station->innmind__url + + +~4.0 + + + +innmind__lab_station->innmind__operating_system + + +~5.0 + + + +innmind__lab_station->innmind__mantle + + +~2.0 + + + +innmind__lab_station->innmind__cli + + +~3.4 + + + +innmind__lab_station->innmind__json + + +^1.1 + + + +innmind__event_bus + + +event-bus@4.1.0 + + + + + +innmind__command_bus + + +command-bus@4.2.0 + + + + + +innmind__rest_server + + +rest-server@8.1.0 + + + + + +innmind__neo4j_onm + + +neo4j-onm@7.1.0 + + + + + +innmind__http_framework + + +http-framework@2.3.0 + + + + + +innmind__library + + +library@1.7.1 + + + + + +innmind__library->innmind__immutable + + +~3.5 + + + +innmind__library->innmind__time_continuum + + +~2.2 + + + +innmind__library->innmind__url + + +~3.3 + + + +innmind__library->innmind__cli + + +~2.0 + + + +innmind__library->innmind__colour + + +~3.1 + + + +innmind__library->innmind__logger + + +~2.0 + + + +innmind__library->innmind__installation_monitor + + +~3.0 + + + +innmind__library->innmind__silent_cartographer + + +~2.0 + + + +innmind__library->innmind__stack + + +^1.0 + + + +innmind__library->innmind__genome + + +^3.0 + + + +innmind__library->innmind__specification + + +~2.0 + + + +innmind__library->innmind__http_server + + +~2.0 + + + +innmind__library->innmind__http_authentication + + +~2.0 + + + +innmind__library->innmind__event_bus + + +~4.0 + + + +innmind__library->innmind__command_bus + + +~4.0 + + + +innmind__library->innmind__rest_server + + +~8.0 + + + +innmind__library->innmind__neo4j_onm + + +~7.0 + + + +innmind__library->innmind__http_framework + + +~2.0 + + + +jeremykendall__php_domain_parser + + +php-domain-parser@6.3.0 + + + + + +innmind__library->jeremykendall__php_domain_parser + + +~5.0 + + + +innmind__reflection->innmind__immutable + + +~4.0|~5.0 + + + +innmind__type + + +type@1.2.0 + + + + + +innmind__reflection->innmind__type + + +~1.0 + + + +innmind__signals->innmind__immutable + + +~4.0|~5.0 + + + +innmind__s3 + + +s3@4.1.0 + + + + + +innmind__s3->innmind__immutable + + +~4.15|~5.0 + + + +innmind__s3->innmind__time_continuum + + +~3.1 + + + +innmind__s3->innmind__url + + +~4.1 + + + +innmind__s3->innmind__operating_system + + +~4.0|~5.0 + + + +innmind__s3->innmind__filesystem + + +~7.5 + + + +innmind__s3->innmind__http + + +~7.0 + + + +innmind__s3->innmind__http_transport + + +~7.0 + + + +innmind__s3->innmind__hash + + +^1.1 + + + +innmind__s3->innmind__xml + + +^7.2 + + + +innmind__templating + + +templating@3.2.0 + + + + + +innmind__templating->innmind__immutable + + +~4.0|~5.0 + + + +innmind__templating->innmind__url + + +~4.0 + + + +innmind__templating->innmind__filesystem + + +~5.0|~6.0 + + + +twig__twig + + +twig@v3.9.3 + + + + + +innmind__templating->twig__twig + + +^3.3.8 + + + +innmind__tower + + +tower@2.2.0 + + + + + +innmind__tower->innmind__immutable + + +~3.5 + + + +innmind__tower->innmind__url + + +~3.3 + + + +innmind__tower->innmind__json + + +^1.1 + + + +innmind__tower->innmind__genome + + +^3.0 + + + +innmind__tower->innmind__cli_framework + + +^1.2 + + + +symfony__config + + +config@v7.0.7 + + + + + +innmind__tower->symfony__config + + +~5.0 + + + +symfony__yaml + + +yaml@v7.0.7 + + + + + +innmind__tower->symfony__yaml + + +~5.0 + + + +innmind__virtual_machine + + +virtual-machine@1.0.0 + + + + + +innmind__virtual_machine->innmind__cli + + +^2.2 + + + +innmind__warden + + +warden@2.2.0 + + + + + +innmind__warden->innmind__immutable + + +~3.5 + + + +innmind__warden->innmind__genome + + +^3.0 + + + +innmind__warden->innmind__cli_framework + + +^1.2 + + + +symfony__framework_bundle->symfony__http_foundation + + +^6.4|^7.0 + + + +symfony__framework_bundle->symfony__http_kernel + + +^6.4|^7.0 + + + +symfony__framework_bundle->symfony__filesystem + + +^6.4|^7.0 + + + +symfony__framework_bundle->symfony__config + + +^6.4|^7.0 + + + +symfony__browser_kit->symfony__dom_crawler + + +^6.4|^7.0 + + + +symfony__http_kernel->symfony__http_foundation + + +^6.4|^7.0 + + + +symfony__http_kernel->psr__log + + +^1|^2|^3 + + + +symfony__config->symfony__filesystem + + +^6.4|^7.0 + + + +phpunit__phpunit->phpunit__php_timer + + +^7.0 + + + +phpunit__phpunit->phpunit__php_code_coverage + + +^11.0 + + + +friendsofphp__php_cs_fixer->symfony__filesystem + + +^5.4 || ^6.0 || ^7.0 + + + +friendsofphp__php_cs_fixer->composer__semver + + +^3.4 + + + +monolog__monolog->psr__log + + +^2.0 || ^3.0 + + + +music_companion__apple_music->innmind__immutable + + +~4.4|~5.0 + + + +music_companion__apple_music->innmind__url + + +~4.1 + + + +music_companion__apple_music->innmind__operating_system + + +~4.0|~5.0 + + + +music_companion__apple_music->innmind__json + + +~1.4 + + + +music_companion__apple_music->innmind__colour + + +~4.0 + + + +music_companion__apple_music->innmind__validation + + +~1.4 + + + +formal__access_layer->innmind__immutable + + +~4.0|~5.0 + + + +formal__access_layer->innmind__url + + +~4.0 + + + +formal__access_layer->innmind__specification + + +^3.0.1 + + + +formal__access_layer->psr__log + + +~3.0 + + + diff --git a/docs/assets/dependency-graph/innmind_cli_dependencies.svg b/docs/assets/dependency-graph/innmind_cli_dependencies.svg new file mode 100644 index 0000000..4493994 --- /dev/null +++ b/docs/assets/dependency-graph/innmind_cli_dependencies.svg @@ -0,0 +1,1123 @@ + + + + + + +packages + + +cluster_innmind + + +innmind + + + + +cluster_psr + + +psr + + + + +cluster_league + + +league + + + + +cluster_symfony + + +symfony + + + + +cluster_brick + + +brick + + + + +cluster_ramsey + + +ramsey + + + + +cluster_formal + + +formal + + + + + +innmind__immutable + + +immutable@5.3.0 + + + + + +innmind__time_continuum + + +time-continuum@3.4.1 + + + + + +innmind__time_continuum->innmind__immutable + + +~4.0|~5.0 + + + +psr__log + + +log@3.0.0 + + + + + +innmind__time_continuum->psr__log + + +~3.0 + + + +innmind__url + + +url@4.3.0 + + + + + +innmind__url->innmind__immutable + + +~4.15|~5.0 + + + +league__uri_parser + + +uri-parser@1.4.1 + + + + + +innmind__url->league__uri_parser + + +~1.2 + + + +league__uri_components + + +uri-components@7.4.1 + + + + + +innmind__url->league__uri_components + + +~2.0 + + + +innmind__stream + + +stream@4.2.0 + + + + + +innmind__stream->innmind__immutable + + +~4.15|~5.0 + + + +innmind__stream->innmind__time_continuum + + +~3.3 + + + +innmind__stream->innmind__url + + +~4.2 + + + +innmind__stream->psr__log + + +~3.0 + + + +innmind__media_type + + +media-type@2.2.0 + + + + + +innmind__media_type->innmind__immutable + + +~4.15|~5.0 + + + +innmind__ip + + +ip@3.2.0 + + + + + +innmind__ip->innmind__immutable + + +~4.1|~5.0 + + + +innmind__socket + + +socket@6.1.0 + + + + + +innmind__socket->innmind__immutable + + +~4.0|~5.0 + + + +innmind__socket->innmind__url + + +~4.0 + + + +innmind__socket->innmind__stream + + +~4.0 + + + +innmind__socket->innmind__ip + + +~3.0 + + + +innmind__io + + +io@2.7.0 + + + + + +innmind__io->innmind__immutable + + +~5.2 + + + +innmind__io->innmind__stream + + +~4.0 + + + +innmind__io->innmind__socket + + +~6.1 + + + +innmind__filesystem + + +filesystem@7.5.1 + + + + + +innmind__filesystem->innmind__immutable + + +~4.15|~5.0 + + + +innmind__filesystem->innmind__time_continuum + + +~3.4 + + + +innmind__filesystem->innmind__url + + +~4.2 + + + +innmind__filesystem->innmind__stream + + +~4.1 + + + +innmind__filesystem->innmind__media_type + + +~2.1 + + + +innmind__filesystem->innmind__io + + +~2.2 + + + +innmind__filesystem->psr__log + + +~3.0 + + + +symfony__filesystem + + +filesystem@v7.0.7 + + + + + +innmind__filesystem->symfony__filesystem + + +~6.0|~7.0 + + + +innmind__time_warp + + +time-warp@3.0.0 + + + + + +innmind__time_warp->innmind__time_continuum + + +~3.0 + + + +innmind__time_warp->psr__log + + +~3.0 + + + +innmind__server_control + + +server-control@5.2.1 + + + + + +innmind__server_control->innmind__immutable + + +~4.15|~5.0 + + + +innmind__server_control->innmind__time_continuum + + +^3.1,<3.3|^3.4.1 + + + +innmind__server_control->innmind__url + + +~4.0 + + + +innmind__server_control->innmind__stream + + +~4.0 + + + +innmind__server_control->innmind__filesystem + + +~7.0 + + + +innmind__server_control->innmind__time_warp + + +^3.0 + + + +innmind__server_control->psr__log + + +~3.0 + + + +innmind__server_status + + +server-status@4.1.0 + + + + + +innmind__server_status->innmind__immutable + + +~4.15|~5.0 + + + +innmind__server_status->innmind__time_continuum + + +~3.0 + + + +innmind__server_status->innmind__url + + +~4.0 + + + +innmind__server_status->innmind__server_control + + +~5.0 + + + +innmind__server_status->psr__log + + +~3.0 + + + +innmind__http + + +http@7.0.1 + + + + + +innmind__http->innmind__immutable + + +~4.15|~5.0 + + + +innmind__http->innmind__time_continuum + + +~3.0 + + + +innmind__http->innmind__url + + +~4.0 + + + +innmind__http->innmind__stream + + +~4.0 + + + +innmind__http->innmind__media_type + + +^2.0.1 + + + +innmind__http->innmind__io + + +~2.2 + + + +innmind__http->innmind__filesystem + + +~7.0 + + + +ramsey__uuid + + +uuid@4.7.6 + + + + + +innmind__http->ramsey__uuid + + +~4.7 + + + +innmind__http_transport + + +http-transport@7.2.1 + + + + + +innmind__http_transport->innmind__immutable + + +~4.15|~5.0 + + + +innmind__http_transport->innmind__time_continuum + + +~3.0 + + + +innmind__http_transport->innmind__url + + +~4.0 + + + +innmind__http_transport->innmind__stream + + +~4.0 + + + +innmind__http_transport->innmind__io + + +~2.2 + + + +innmind__http_transport->innmind__filesystem + + +~7.1 + + + +innmind__http_transport->innmind__time_warp + + +~3.0 + + + +innmind__http_transport->innmind__http + + +~7.0 + + + +innmind__http_transport->psr__log + + +~3.0 + + + +innmind__http_transport->ramsey__uuid + + +^4.7 + + + +innmind__signals + + +signals@3.1.0 + + + + + +innmind__signals->innmind__immutable + + +~4.0|~5.0 + + + +innmind__file_watch + + +file-watch@4.0.0 + + + + + +innmind__file_watch->innmind__time_continuum + + +~3.0 + + + +innmind__file_watch->innmind__url + + +~4.0 + + + +innmind__file_watch->innmind__time_warp + + +~3.0 + + + +innmind__file_watch->innmind__server_control + + +~5.0 + + + +innmind__file_watch->psr__log + + +~3.0 + + + +innmind__specification + + +specification@3.0.1 + + + + + +innmind__operating_system + + +operating-system@5.0.0 + + + + + +innmind__operating_system->innmind__time_continuum + + +~3.0 + + + +innmind__operating_system->innmind__stream + + +~4.0 + + + +innmind__operating_system->innmind__socket + + +~6.0 + + + +innmind__operating_system->innmind__io + + +~2.7 + + + +innmind__operating_system->innmind__filesystem + + +~7.1 + + + +innmind__operating_system->innmind__time_warp + + +~3.0 + + + +innmind__operating_system->innmind__server_control + + +~5.0 + + + +innmind__operating_system->innmind__server_status + + +~4.0 + + + +innmind__operating_system->innmind__http_transport + + +~7.2 + + + +innmind__operating_system->innmind__signals + + +~3.0 + + + +innmind__operating_system->innmind__file_watch + + +~4.0 + + + +formal__access_layer + + +access-layer@2.15.0 + + + + + +innmind__operating_system->formal__access_layer + + +^2.0 + + + +innmind__colour + + +colour@4.2.0 + + + + + +innmind__colour->innmind__immutable + + +~4.0|~5.0 + + + +innmind__graphviz + + +graphviz@3.4.0 + + + + + +innmind__graphviz->innmind__immutable + + +~4.0|~5.0 + + + +innmind__graphviz->innmind__url + + +~4.0 + + + +innmind__graphviz->innmind__filesystem + + +~7.0 + + + +innmind__graphviz->innmind__colour + + +~4.0 + + + +innmind__stack_trace + + +stack-trace@4.1.0 + + + + + +innmind__stack_trace->innmind__immutable + + +~4.1|~5.0 + + + +innmind__stack_trace->innmind__url + + +~4.0 + + + +innmind__stack_trace->innmind__graphviz + + +~3.1 + + + +innmind__cli + + +cli@3.6.0 + + + + + +innmind__cli->innmind__immutable + + +~4.15|~5.0 + + + +innmind__cli->innmind__url + + +~4.0 + + + +innmind__cli->innmind__stream + + +~4.0 + + + +innmind__cli->innmind__operating_system + + +~4.0|~5.0 + + + +innmind__cli->innmind__stack_trace + + +~4.0 + + + +psr__http_message + + +http-message@2.0 + + + + + +psr__http_factory + + +http-factory@1.0.2 + + + + + +psr__http_factory->psr__http_message + + +^1.0 || ^2.0 + + + +league__uri_interfaces + + +uri-interfaces@7.4.1 + + + + + +league__uri_interfaces->psr__http_message + + +^1.1 || ^2.0 + + + +league__uri_interfaces->psr__http_factory + + +^1 + + + +league__uri + + +uri@7.4.1 + + + + + +league__uri->league__uri_interfaces + + +^7.3 + + + +league__uri_components->league__uri + + +^7.3 + + + +symfony__polyfill_ctype + + +polyfill-ctype@v1.29.0 + + + + + +symfony__polyfill_mbstring + + +polyfill-mbstring@v1.29.0 + + + + + +symfony__process + + +process@v7.0.7 + + + + + +symfony__filesystem->symfony__polyfill_ctype + + +~1.8 + + + +symfony__filesystem->symfony__polyfill_mbstring + + +~1.8 + + + +symfony__filesystem->symfony__process + + +^6.4|^7.0 + + + +brick__math + + +math@0.12.1 + + + + + +ramsey__collection + + +collection@2.0.0 + + + + + +ramsey__uuid->brick__math + + +^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 + + + +ramsey__uuid->ramsey__collection + + +^1.2 || ^2.0 + + + +formal__access_layer->innmind__immutable + + +~4.0|~5.0 + + + +formal__access_layer->innmind__url + + +~4.0 + + + +formal__access_layer->innmind__specification + + +^3.0.1 + + + +formal__access_layer->psr__log + + +~3.0 + + + diff --git a/docs/assets/dependency-graph/innmind_cli_dependents.svg b/docs/assets/dependency-graph/innmind_cli_dependents.svg new file mode 100644 index 0000000..0943717 --- /dev/null +++ b/docs/assets/dependency-graph/innmind_cli_dependents.svg @@ -0,0 +1,287 @@ + + + + + + +packages + + +cluster_innmind + + +innmind + + + + + +innmind__async_http_server + + +async-http-server@3.0.0 + + + + + +innmind__cli + + +cli@3.6.0 + + + + + +innmind__async_http_server->innmind__cli + + +^3.3 + + + +innmind__crawler_app + + +crawler-app@1.5.2 + + + + + +innmind__installation_monitor + + +installation-monitor@3.1.0 + + + + + +innmind__crawler_app->innmind__installation_monitor + + +~3.0 + + + +innmind__silent_cartographer + + +silent-cartographer@2.2.0 + + + + + +innmind__crawler_app->innmind__silent_cartographer + + +~2.0 + + + +innmind__crawler_app->innmind__cli + + +~2.0 + + + +innmind__debug + + +debug@4.0.0 + + + + + +innmind__framework + + +framework@2.2.0 + + + + + +innmind__debug->innmind__framework + + +~2.0 + + + +innmind__profiler + + +profiler@4.1.0 + + + + + +innmind__debug->innmind__profiler + + +~4.0 + + + +innmind__dependency_graph + + +dependency-graph@3.6.0 + + + + + +innmind__dependency_graph->innmind__framework + + +~2.0 + + + +innmind__framework->innmind__cli + + +^3.1 + + + +innmind__git_release + + +git-release@3.1.0 + + + + + +innmind__git_release->innmind__cli + + +~3.0 + + + +innmind__installation_monitor->innmind__silent_cartographer + + +~2.0 + + + +innmind__installation_monitor->innmind__cli + + +~2.0 + + + +innmind__kalmiya + + +kalmiya@2.0.0 + + + + + +innmind__kalmiya->innmind__dependency_graph + + +~3.6 + + + +innmind__kalmiya->innmind__framework + + +~2.2 + + + +innmind__lab_station + + +lab-station@4.1.0 + + + + + +innmind__lab_station->innmind__cli + + +~3.4 + + + +innmind__library + + +library@1.7.1 + + + + + +innmind__library->innmind__installation_monitor + + +~3.0 + + + +innmind__library->innmind__silent_cartographer + + +~2.0 + + + +innmind__library->innmind__cli + + +~2.0 + + + +innmind__profiler->innmind__framework + + +~2.0 + + + +innmind__silent_cartographer->innmind__cli + + +~2.0 + + + +innmind__virtual_machine + + +virtual-machine@1.0.0 + + + + + +innmind__virtual_machine->innmind__cli + + +^2.2 + + + diff --git a/docs/assets/dependency-graph/macOS-app.png b/docs/assets/dependency-graph/macOS-app.png new file mode 100644 index 0000000..9f7e7ad Binary files /dev/null and b/docs/assets/dependency-graph/macOS-app.png differ diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 0000000..08dee3a Binary files /dev/null and b/docs/assets/favicon.png differ diff --git a/docs/assets/fonts/MonaspaceNeon-Regular.woff b/docs/assets/fonts/MonaspaceNeon-Regular.woff new file mode 100644 index 0000000..ce0168b Binary files /dev/null and b/docs/assets/fonts/MonaspaceNeon-Regular.woff differ diff --git a/docs/assets/lab-station/overview.mov b/docs/assets/lab-station/overview.mov new file mode 100644 index 0000000..5eaf3cc Binary files /dev/null and b/docs/assets/lab-station/overview.mov differ diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..6a5d322 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/profiler/app_graph.png b/docs/assets/profiler/app_graph.png new file mode 100644 index 0000000..71dc0ac Binary files /dev/null and b/docs/assets/profiler/app_graph.png differ diff --git a/docs/assets/profiler/environment.png b/docs/assets/profiler/environment.png new file mode 100644 index 0000000..7c2d6e1 Binary files /dev/null and b/docs/assets/profiler/environment.png differ diff --git a/docs/assets/profiler/exception.png b/docs/assets/profiler/exception.png new file mode 100644 index 0000000..a8c9efa Binary files /dev/null and b/docs/assets/profiler/exception.png differ diff --git a/docs/assets/profiler/http.png b/docs/assets/profiler/http.png new file mode 100644 index 0000000..c1c6dfe Binary files /dev/null and b/docs/assets/profiler/http.png differ diff --git a/docs/assets/profiler/index.png b/docs/assets/profiler/index.png new file mode 100644 index 0000000..213154e Binary files /dev/null and b/docs/assets/profiler/index.png differ diff --git a/docs/assets/profiler/processes.png b/docs/assets/profiler/processes.png new file mode 100644 index 0000000..e42776c Binary files /dev/null and b/docs/assets/profiler/processes.png differ diff --git a/docs/assets/profiler/remote_http.png b/docs/assets/profiler/remote_http.png new file mode 100644 index 0000000..d1d4f74 Binary files /dev/null and b/docs/assets/profiler/remote_http.png differ diff --git a/docs/assets/profiler/remote_processes.png b/docs/assets/profiler/remote_processes.png new file mode 100644 index 0000000..48d8e81 Binary files /dev/null and b/docs/assets/profiler/remote_processes.png differ diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css new file mode 100644 index 0000000..e4aa2d4 --- /dev/null +++ b/docs/assets/stylesheets/extra.css @@ -0,0 +1,113 @@ +@font-face { + font-family: "Monaspace Neon"; + font-weight: normal; + font-style: normal; + src: url("../fonts/MonaspaceNeon-Regular.woff"); +} + +:root { + --md-code-font: "Monaspace Neon"; +} + +:root { + --light-md-code-hl-number-color: #f76d47; + --light-md-code-hl-function-color: #6384b9; + --light-md-code-hl-operator-color: #39adb5; + --light-md-code-hl-constant-color: #7c4dff; + --light-md-code-hl-string-color: #9fc06f; + --light-md-code-hl-punctuation-color: #39adb5; + --light-md-code-hl-keyword-color: #7c4dff; + --light-md-code-hl-variable-color: #80cbc4; + --light-md-code-hl-comment-color: #ccd7da; + --light-md-code-bg-color: #fafafa; + --light-md-code-fg-color: #ffb62c; + --light-md-code-hl-variable-color: #6384b9; + --dark-md-code-hl-number-color: #f78c6c; + --dark-md-code-hl-function-color: #82aaff; + --dark-md-code-hl-operator-color: #89ddff; + --dark-md-code-hl-constant-color: #c792ea; + --dark-md-code-hl-string-color: #c3e88d; + --dark-md-code-hl-punctuation-color: #89ddff; + --dark-md-code-hl-keyword-color: #c792ea; + --dark-md-code-hl-variable-color: #e8f9f9; + --dark-md-code-hl-comment-color: #546e7a; + --dark-md-code-bg-color: #263238; + --dark-md-code-fg-color: #ffcb6b; + --dark-md-code-hl-variable-color: #82aaff; +} + +@media (prefers-color-scheme: light) { + .language-php > * { + --md-code-hl-number-color: var(--light-md-code-hl-number-color); + --md-code-hl-function-color: var(--light-md-code-hl-function-color); + --md-code-hl-operator-color: var(--light-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--light-md-code-hl-constant-color); + --md-code-hl-string-color: var(--light-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--light-md-code-hl-comment-color); + --md-code-bg-color: var(--light-md-code-bg-color); + --md-code-fg-color: var(--light-md-code-fg-color); + } + + .language-php .na { + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + } +} + +[data-md-color-media="(prefers-color-scheme: light)"] .language-php > * { + --md-code-hl-number-color: var(--light-md-code-hl-number-color); + --md-code-hl-function-color: var(--light-md-code-hl-function-color); + --md-code-hl-operator-color: var(--light-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--light-md-code-hl-constant-color); + --md-code-hl-string-color: var(--light-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--light-md-code-hl-comment-color); + --md-code-bg-color: var(--light-md-code-bg-color); + --md-code-fg-color: var(--light-md-code-fg-color); +} + +[data-md-color-media="(prefers-color-scheme: light)"] .language-php .na { + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); +} + +@media (prefers-color-scheme: dark) { + .language-php > * { + --md-code-hl-number-color: var(--dark-md-code-hl-number-color); + --md-code-hl-function-color: var(--dark-md-code-hl-function-color); + --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color); + --md-code-hl-string-color: var(--dark-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color); + --md-code-bg-color: var(--dark-md-code-bg-color); + --md-code-fg-color: var(--dark-md-code-fg-color); + } + + .language-php .na { + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + } +} + +[data-md-color-media="(prefers-color-scheme: dark)"] .language-php > * { + --md-code-hl-number-color: var(--dark-md-code-hl-number-color); + --md-code-hl-function-color: var(--dark-md-code-hl-function-color); + --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color); + --md-code-hl-string-color: var(--dark-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color); + --md-code-bg-color: var(--dark-md-code-bg-color); + --md-code-fg-color: var(--dark-md-code-fg-color); +} + +[data-md-color-media="(prefers-color-scheme: dark)"] .language-php .na { + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); +} diff --git a/docs/getting-started/app/cli.md b/docs/getting-started/app/cli.md new file mode 100644 index 0000000..a95e05d --- /dev/null +++ b/docs/getting-started/app/cli.md @@ -0,0 +1,94 @@ +# CLI + +This package allows you to build scripts in a more structured way. + +## Installation + +```sh +composer require innmind/cli:~3.6 +``` + +## Usage + +```php title="cli.php" +output(Str::of("Hello world\n")); + } +}; +``` + +If you run `php cli.php` in your terminal it will print `Hello world`. + +You should already be familiar with the `$os` variable by now, if not go the [dedicated chapter](../operating-system/index.md). + +The `$env` variable is the abstraction to deal with everything inputed in your script and every output that comes out. It behaves like an immutable object, meaning you **must** always use the new instance returned by its methods. + +To change the returned exit code you can do `return $env->exit(1)`. + +If you only have one action in your script you can do everything in the `main` method. But if you want to expose multiple commands you can do: + +```php title="cli.php" +use Innmind\CLI\{ + Commands, + Console, + Command, +}; + +new class extends Main { + protected function main(Environment $env, OperatingSystem $os): Environment + { + $commands = Commands::of( + new class implements Command { + public function __invoke(Console $console): Console + { + return $console->output( + Str::of('Hello ')->append( + $console->arguments()->get('name'), + ), + ); + } + + public function usage(): string + { + return 'greet name'; + } + }, + new class implements Command { + public function __invoke(Console $console): Console + { + return $console->output( + Str::of($console->arguments()->get('name')) + ->toUpper() + ->prepend('Hello '), + ); + } + + public function usage(): string + { + return 'shout name'; + } + }, + ); + + return $commands($env); + } +}; +``` + +If you run `php cli.php greet Jane` it will output `Hello Jane` and if you run `php cli.php shout John` it will output `Hello JOHN`. + +??? info + For simplicity this example use anonymous classes but you can use any class as long as it implements `Command`. diff --git a/docs/getting-started/app/http.md b/docs/getting-started/app/http.md new file mode 100644 index 0000000..a5292b4 --- /dev/null +++ b/docs/getting-started/app/http.md @@ -0,0 +1,98 @@ +# HTTP + +This package allows to build simple HTTP applications by representing requests and responses via objects. + +## Installation + +```sh +composer require innmind/http-server:~4.1 +``` + +## Usage + +```php title="index.php" +protocolVersion(), + null, + Content::ofString('Hello world'), + ); + } +} +``` + +If you run the PHP server in the directory of this file via `php -S localhost:8080` and run `curl http://localhost:8080/` it will print `Hello world`. + +??? note + You can expose this script via any HTTP server that supports PHP. + +As you can see the response body is a file content, meaning it accepts any [file content](../operating-system/filesystem.md). + +```php title="index.php" +use Innmind\OperatingSystem\OperatingSystem; +use Innmind\Filesystem\{ + File, + Name, +}; +use Innmind\Http\{ + Headers, + Header\ContentType, +}; +use Innmind\Url\Path; +use Innmind\Immutable\Predicate\Instance; + +new class extends Main { + private OperatingSystem $os; + + protected function preload(OperatingSystem $os): void + { + $this->os = $os; + } + + protected function main(ServerRequest $request): Response + { + return Response::of( + Response\StatusCode::ok, + $request->protocolVersion(), + Headers::of( + ContentType::of('image', 'png'), + ), + $this + ->os + ->filesystem() + ->mount(Path::of('images/')) + ->get(Name::of('some-image.png')) + ->keep(Instance::of(File::class)) + ->match( + static fn(File $file) => $file->content(), + static fn() => throw new \RuntimeException(), + ), + ); + } +} +``` + +This example will send back the image at `images/some-image.png`. If the image is not found then it will throw an exception. + +??? note + The `main` function will catch all thrown exceptions and will return an empty `500` response. This is done to make sure no stack trace is ever shown to a user. + + During development if you want to see the exception you can catch all exceptions yourself and use `filp/whoops` to render it. Or you can use [Innmind's framework](../framework/index.md) and its [profiler](../framework/profiler.md). + +!!! info "" + For a very simple app this is enough, you can even do some routing manually by analyzing `$request->url()`. For any more than that you should start looking at the [framework](../framework/index.md). diff --git a/docs/getting-started/concurrency/async.md b/docs/getting-started/concurrency/async.md new file mode 100644 index 0000000..8520c4c --- /dev/null +++ b/docs/getting-started/concurrency/async.md @@ -0,0 +1,205 @@ +# Asynchronous code + +Since Innmind offers to access all I/O operations through the [operating system abstraction](../operating-system/index.md), it can easily execute these operations asynchronously. + +## Installation + +```sh +composer require innmind/mantle:~2.1 +``` + +## Usage + +Mantle works a bit like a reduce operation. The _reducer_ function allows to launch `Task`s and react to their results. Both the _reducer_ and tasks are run asynchronously. + +=== "Script" + ```php + use Innmind\Mantle\Forerunner; + use Innmind\Http\Response; + use Innmind\Immutable\Sequence; + + $run = Forerunner::of($os); + $responses = $run( + Carried::new(), + new Reducer, + ); + $responses; // instance of Sequence + ``` + +=== "Carried value" + Like in a real reduce operation you need a carried value that will be passed to the reducer every time it's called. + + Here we use a `Carried` class but you can use any type you want. + + ```php + use Innmind\Http\Response; + use Innmind\Immutable\Sequence; + + final readonly class Carried + { + /** @var Sequence */ + private function __construct( + private bool $tasksLaucnhed, + private Sequence $responses, + ) {} + + public static function new(): self + { + return new self(false, Sequence::of()); + } + + public function tasksLaunched(): self + { + return new self(true, $this->responses); + } + + public function needsToLaunchTasks(): bool + { + return !$this->tasksLaunched; + } + + public function got(?Response $response): self + { + return new self( + $this->tasksLaunched, + $this->responses->add($response), + ); + } + + /** @return Sequence */ + public function responses(): Sequence + { + return $this->responses; + } + } + ``` + + !!! warning "" + To avoid unexpected side effects you should always use an immutable value for the carried value. + +=== "Reducer" + ```php + use Innmind\Mantle\{ + Source\Continuation, + Task, + }; + use Innmind\OperatingSystem\OperatingSystem; + use Innmind\Http\Response; + use Innmind\Immutable\Sequence; + + final class Reducer + { + /** + * @param Continuation $continuation + * @param Sequence $results + * + * @return Continuation + */ + public function __invoke( + Carried $carried, + OperatingSystem $os, #(1) + Continuation $continuation, + Sequence $results, #(2) + ): Continuation { + if ($carried->needsToLaunchTasks()) { + return $continuation + ->carryWith($carried->tasksLaunched()) #(3) + ->launch(Sequence::of( + Task::of( #(4) + static fn(OperatingSystem $os) => MyTask::of( + $os, + 'https://github.com/' + ), + ), + Task::of( + static fn(OperatingSystem $os) => MyTask::of( + $os, + 'https://gitlab.com/' + ), + ), + )); + } + + $carried = $results->reduce( + $carried, + static fn( + Carried $carried, + ?Response $response + ) => $carried->got($response), + ); + + if ($carried->responses()->size() === 2) { + return $continuation + ->carryWith($carried) + ->terminate(); #(5) + } + + return $continuation->carryWith($carried); + } + } + ``` + + 1. This `$os` variable is a new instance built by Mantle and runs asynchronously. + 2. This will contain the values returned by the tasks as soon as available. + 3. We flip the flag in order to not launch the tasks each time the reducer is called. + 4. The `$os` variable below is a dedicated new instance for each task. + 5. This tells Mantle to stop calling the reducer and return the carried value. + + This `__invoke` method will be called once when starting the runner and then each time a task finishes. + + The flag to know if the tasks have been launched is stored in the carried value, but since we're in an object it could be placed as a property. This is done so you can better differentiate the carried values from the `$results` in this example. + +=== "MyTask" + ```php + use Innmind\OperatingSystem\OperatingSystem; + use Innmind\Http\{ + Request, + Response, + Method, + ProtocolVersion, + }; + use Innmind\Url\Url; + + final class MyTask { + public static function of( + OperatingSystem $os, + string $url, + ): ?Response { + return $os + ->remote() + ->http()(Request::of( + Url::of($url), + Method::get, + ProtocolVersion::v11, + )) + ->match( + static fn(Success $success) => $success->response(), + static fn() => null, + ); + } + } + ``` + + +## Advantages + +The first big advantage of this design is that your task is completely unaware that it is run asynchronously. It all depends on the `$os` variable injected (1). This means that you can easily experiment a piece of your program in an async context by what code calls it, your program logic itself doesn't have to change! +{.annotate} + +1. If it comes from Mantle it's async otherwise it's sync. + +The side effect of this is that you can test your code synchronously even though it's run asynchronously. + +The other advantage is that since all state is local you can compose async code inside sync code transparently. You can't affect a global state since there is none. + +## Limitations + +- CLI signals are currently not supported in this context +- HTTP calls are currently done via `cURL` and uses micro sleeps instead of watching sockets +- SQL queries are still run synchronously for now +- It seems there is a limit of 10k concurrent tasks before performance degradation + +Most of these limitations are planned to be fixed in the future. + +!!! warning "" + You may not want to use this in production just yet, or at least not for mission critical code. diff --git a/docs/getting-started/concurrency/distributed.md b/docs/getting-started/concurrency/distributed.md new file mode 100644 index 0000000..554ee04 --- /dev/null +++ b/docs/getting-started/concurrency/distributed.md @@ -0,0 +1,31 @@ +# Distributed + +Innmind intends to provide a way to build distributed programs with the same philosophy seen so far. + +This way you'll be able to move to a distributed program with little effort. + +## Actor Model + +The [Actor Model](https://en.wikipedia.org/wiki/Actor_model) is a way to build concurrent programs. It's built arount 3 concepts: + +- an Actor is a compute unit that handle state +- an Actor can create actors +- an Actor can send/receive messages + +An actor can only receive one message at a time, meaning there's no concurrency on a single actor. This simplify drastically the complexity of handling state and eliminate data races. + +The concurrency as a whole is handled by the tree of actors that spread the work. + +!!! abstract "" + You can think of this model as a queuing system that dynamically create new queues. + +But because having one process per actor would be too expensive in resources, it's required to be able to run multiple actors asynchronously inside a single process. Hence all the tools you've seen previously. + +## Work in progress + +The implementation of this model is still underway at [`innmind/witness`](https://github.com/Innmind/witness). + +There's been quite a gap in activity on this repository because early work on the implementation revealed that the use of exceptions was untenable for the system stability. + +!!! info "" + This is what motivated the move to the monadic approach across all Innmind packages. diff --git a/docs/getting-started/concurrency/http.md b/docs/getting-started/concurrency/http.md new file mode 100644 index 0000000..2e7d23f --- /dev/null +++ b/docs/getting-started/concurrency/http.md @@ -0,0 +1,171 @@ +# HTTP calls + +Traditionnaly the HTTP requests in PHP programs are synchronous for the sake of simplicity as PHP is single threaded. But this is wasteful when multiple requests could be sent at the same time. + +Innmind's HTTP client allows to move from synchronous calls to concurrent ones very easily. + +## Example + +Imagine you want to fetch 2 pages, this would be the synchronous code: + +```php +use Innmind\HttpTransport\Success; +use Innmind\Http\{ + Request, + Method, + ProtocolVersion, +}; +use Innmind\Url\Url; + +$http = $os->remote()->http(); +$github = $http(Request::of( + Url::of('https://github.com'), + Method::get, + ProtocolVersion::v11, +)); +$github->match( + static fn(Success $success) => doStuff($success->response()), + static fn() => failedToFetch(), +); +$gitlab = $http(Request::of( + Url::of('https://gitlab.com'), + Method::get, + ProtocolVersion::v11, +)); +$gitlab->match( + static fn(Success $success) => doStuff($success->response()), + static fn() => failedToFetch(), +); +``` + +Remember that the [value returned by `$http` calls](../operating-system/http.md) is an [`Either`](../handling-data/either.md). More precisely it uses a deferred `Either`, meaning that the value it represents will be evaluated when you try to extract the value via the `match` method. + +This means that to make the calls concurrent you only need to move all the `match` calls after asking to make requests: + +```php hl_lines="12-15" +$http = $os->remote()->http(); +$github = $http(Request::of( + Url::of('https://github.com'), + Method::get, + ProtocolVersion::v11, +)); +$gitlab = $http(Request::of( + Url::of('https://gitlab.com'), + Method::get, + ProtocolVersion::v11, +)); +$github->match( + static fn(Success $success) => doStuff($success->response()), + static fn() => failedToFetch(), +); +$gitlab->match( + static fn(Success $success) => doStuff($success->response()), + static fn() => failedToFetch(), +); +``` + +This way the HTTP client will execute all the requests planned before the first `match` is called. + +## Tips + +### Unsent requests + +Remember to always keep a reference to a returned `Either` before calling a `match` method otherwise the non referenced request won't be sent. While this may seem tedious this opens a feature that may be very useful in certain cases. + +This way you can plan a bunch of requests and afterward based on some logic unplan some requests by de-referencing the `Either`s before calling a `match` method. + +This allows better flexibility in the way you can decouple your logic. + +### Max concurrency + +By default the client will send all planned requests at once. But this can be problematic if you plan too many requests, the underlying `cURL` implementation may return some errors. + +You can configure the max concurrency at the start of your program and leave your business logic as is. You can do it this way: + +=== "Operating System" + ```php + use Innmind\OperatingSystem\{ + Factory, + Config, + }; + + $os = Factory::build( + Config::of()->limitHttpConcurrencyTo(20), + ); + + // rest of your script + ``` + +=== "Framework" + ```php + use Innmind\Framework\{ + Main\Cli, + Application, + }; + use Innmind\OperatingSystem\Config; + + new class(Config::of()->limitHttpConcurrencyTo(20)) extends Cli { + protected function configure(Application $app): Application + { + // configure your app here + return $app; + } + } + ``` + + Here we use the [`Cli` entrypoint](../framework/cli.md) but it works the same way for the [`Http` ones](../framework/http.md). + +=== "CLI app" + ```php + use Innmind\CLI\{ + Main, + Environment, + }; + use Innmind\OperatingSystem\{ + OperatingSystem, + Config, + }; + + new class(Config::of()->limitHttpConcurrencyTo(20)) extends Main { + protected function main(Environment $env, OperatingSystem $os): Environment + { + // your code here + return $env; + } + }; + ``` + + [Related chapter](../app/cli.md) + +=== "HTTP app" + ```php + use Innmind\HttpServer\Main; + use Innmind\Http\{ + ServerRequest, + Response, + }; + use Innmind\OperatingSystem\Config; + + new class(Config::of()->limitHttpConcurrencyTo(20)) extends Main { + protected function main(ServerRequest $request): Response + { + // your code here + return Response::of( + Response\StatusCode::ok, + $request->protocolVersion(), + ); + } + }; + ``` + + [Related chapter](../app/http.md) + +The examples here use a maximum of `20` but you should adapt it to the needs of your program. + +### Limits + +When calling a `match` method it will wait for all planned request to finish before giving you access to your request response. + +This means that you can't react as soon as a response is accessible. Your program can still stay idle for some time. + +If you need better reaction timing you should head to the [asynchronous chapter](async.md). diff --git a/docs/getting-started/concurrency/index.md b/docs/getting-started/concurrency/index.md new file mode 100644 index 0000000..d7bc0b0 --- /dev/null +++ b/docs/getting-started/concurrency/index.md @@ -0,0 +1,13 @@ +# Concurrency + +Concurrency is about executing multiple parts of a program in way to waste as little time as possible. There is 2 ways possible to achieve this: + +- asynchronicity +- parallelism + +Asynchronous code means there are at all times only one part of a program that's executed. But each part of the program advances one after the other in the same process. This mode is useful when your program is I/O bound, for example if a part of your program makes an HTTP call then another part can be executed while you wait for the response. However if your program is CPU bound then this mode has no usefulness. + +Parallel code means that multiple processes will be executed at the same time. Each process will be spread across the cores available on your CPU. This mode is useful when your program is CPU bound. But it comes with the disadvantage of coordinating the results of each process (when needed). + +!!! success "" + Innmind offers solutions for both needs. diff --git a/docs/getting-started/concurrency/ipc.md b/docs/getting-started/concurrency/ipc.md new file mode 100644 index 0000000..ed24010 --- /dev/null +++ b/docs/getting-started/concurrency/ipc.md @@ -0,0 +1,85 @@ +# Inter Process Communication + +When your program runs across multiple processes you may want to communicate between them to update some state. + +Innmind IPC use unix sockets to send messages between processes. + +## Installation + +```sh +composer require innmind/ipc:~4.4 +``` + +## Usage + +=== "Server" + ```php + use Innmind\IPC\{ + Factory, + Process\Name, + Message, + Continuation, + }; + + $ipc = Factory::of($os); + $serve = $ipc->listen(Name::of('server-name')); + $counter = $serve( + 0, #(1) + static function( + Message $message, + Continuation $continuation, + int $counter, #(2) + ): Continuation { + if ($counter === 42) { + return $continuation->stop($counter); + } + + return $continuation->respond( + $counter + 1, #(3) + Message\Generic::of('text/plain', 'pong'), + ); + }, + )->match( + static fn(int $counter) => $counter, + static fn() => throw new \RuntimeException('Unable to start the server'), + ); + ``` + + 1. This is the initial carried value. + 2. This is the carried value between every call of the function. + 3. This updates the carried value for the next message. + + The server behaves like a reduce operation, with a carried value and a function that's called every time a client sends a message. The carried value can be of any type. + + In this case the server will stop after receiving `42` messages. + + The returned value is an [`Either`](../handling-data/either.md) with the carried value on the right side or an error on the left side if the server failed to start. + +=== "Client" + ```php + use Innmind\IPC\{ + Factory, + Process, + Process\Name, + Message, + }; + + $ipc = Factory::of($os); + $ipc + ->wait(Name::of('server-name')) + ->flatMap(fn(Process $process) => $process->send(Sequence::of( + Message\Generic::of('text/plain', 'ping'), + ))) + ->flatMap(fn(Process $process) => $process->wait()) + ->match( + static fn(Message $message) => print( + 'server responded '.$message->content()->toString(), + ), + static fn() => print('no response from the server'), + ); + ``` + + This will wait for the server to be up then it will send a `ping` message and wait for the server to respond. Then it will print `server responded pong` since the server always repond with this message unless it has stopped in the meantime. + + ??? tip + If you want to immediately stop if the server is not up you can replace `$ipc->wait()` by `$ipc->get()`. diff --git a/docs/getting-started/concurrency/queues.md b/docs/getting-started/concurrency/queues.md new file mode 100644 index 0000000..467e1cc --- /dev/null +++ b/docs/getting-started/concurrency/queues.md @@ -0,0 +1,125 @@ +# Queues + +Innmind uses the [AMQP protocol](https://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol) to build queues. + +You need to first install a server that implements this protocol (1). The most well known server is [RabbitMQ](https://www.rabbitmq.com). +{.annotate} + +1. Only the version `0.9` is supported. (`1.0` is a completely different protocol) + +## Installation + +```sh +composer require innmind/amqp:~5.0 +``` + +## Usage + +```php +use Innmind\AMQP\{ + Factory, + Command\DeclareExchange, + Command\DeclareQueue, + Command\Bind, + Model\Exchange\Type, +}; +use Innmind\Socket\Internet\Transport; +use Innmind\TimeContinuum\Earth\Period\Second; +use Innmind\Url\Url; + +$client = Factory::of($os) + ->build( + Transport::tcp(), + Url::of('amqp://guest:guest@localhost:5672/'), + Second::of(1)->asElapsedPeriod(), // heartbeat + ) + ->with(DeclareExchange::of('crawler', Type::direct)) + ->with(DeclareQueue::of('parser')) + ->with(Bind::of('crawler', 'parser')); +``` + +This builds the basis of an AMQP client. As is it does nothing until it's run (more in a bit). The client is immutable and each call to `with` returns a new instance. In this case the `$client` will create an exchange named `crawler`, create a queue `parser` and will route every message published to `crawler` directly to `parser`. + +??? tip + You can head to the [RabbitMQ tutorials](https://www.rabbitmq.com/tutorials) to understand exchanges, queues and how to route your messages between the two. + +The first step is to publish messages before trying to consume them. + +```php +use Innmind\AMPQ\{ + Model\Basic\Message, + Command\Publish, + Failure, +}; +use Innmind\Immutable\Str; + +$message = Message::of( + Str::of('https://github.com'); +); +$client + ->with(Publish::one($message)->to('crawler')) + ->run(null) #(1) + ->match( + static fn() => null, // success + static fn(Failure $failure) => handleFailure($failure), + ); +``` + +1. For now don't worry about this `null`, just know that it's required. + +The client will execute anything only when the `run` method is called. In this case, because we reuse the client from above, it will create the exchange, the queue and bind them together and then publish one message that will end up in the queue. + +If everything works fine then it will return an [`Either`](../handling-data/either.md) with `null` on the right side. If any error occurs it will be a `Failure` on the left side. + +??? info + Using a client that always declare the the exchange and queues that it requires allows for a hot declaration of your infrastructure when you try to use the client. And if the exchanges, queues and bindings already exist it will silently continue to execute as the structure is the way you expect on the AMQP server. + +Then to consume the queue: + +```php +use Innmind\AMQP\{ + Command\Consume, + Model\Basic\Message, + Consumer\Continuation, + Failure, +}; + +$client + ->with(Consume::of('parser')->handle( + static function( + int $count, #(1) + Message $message, + Continuation $continuation, + ): Continuation { + doStuff($message); + + if ($count === 42) { + return $continuation->cancel($count); + } + + return $continuation->continue($count + 1); + }, + )) + ->run(0) #(2) + ->match( + static fn(int $count) => var_dump($count), + static fn(Failure $failure) => handleFailure($failure), + ); +``` + +1. This argument is a carried value between each call of this function. +2. This is the initial value passed to the function handling the messages. + +Here we reuse the client from the first example to make sure we indeed have a `parser` queue to work on. Then we _consume_ the queue, meaning we'll wait for incoming messages and call the function when one arrives. This function behaves like a reduce operation where the initial value is `0` and is incremented each time a message is received. On the 43th message we'll handle the message and ask the client to stop consuming the queue. + +At this point the `run` method will return `42` on the right side of the `Either` or a failure on the left side. + +In this case the carried value is an `int` but you can use any type you want. + +??? tip + If you only need to pull one message from the queue you should use `Innmind\AMQP\Command\Get` instead of `Consume`. + +??? tip + When consuming a queue by default the server will send as many messages as it can through the socket so there's no wait time when dealing the _next_ message. However depending on the throughput of your program it can send too many messages in advance. + + To prevent network saturation you can use `#!php Innmind\AMQP\Command\Qos::of(100)` where `100` is the number of messages to send in advance. Add this command before adding the `Consume` command to the client. diff --git a/docs/getting-started/framework/cli.md b/docs/getting-started/framework/cli.md new file mode 100644 index 0000000..0df1db9 --- /dev/null +++ b/docs/getting-started/framework/cli.md @@ -0,0 +1,122 @@ +# CLI + +## Usage + +The first step is to define the entrypoint: + +```php title="bin/console" +command( + static fn() => new class implements Command { + public function __invoke(Console $console): Console + { + return $console->output( + Str::of('Hello ')->append( + $console->arguments()->get('name'), + ), + ); + } + + public function usage(): string + { + return 'greet name'; + } + }, + ); + } +}; +``` + +You can now do `php bin/console greet John` to print `Hello John`. + +??? info + This is the same classes used in a small [CLI app](../app/cli.md). This allows you to easily migrate in case you app grows and you decide to use this framework. + +??? tip + The full definition of the function passed to the `command` method is: + + ```php + use Innmind\DI\Container; + use Innmind\OperatingSystem\OperatingSystem; + use Innmind\Framework\Environment; + use Innmind\CLI\Command; + + static fn(Container $container, OperatingSystem $os, Environment $env): Command; + ``` + + - `$container` is a service locator + - `$os` you've seen it in a previous chapter + - `$env` contains the environment variables + +You can add as many commands as you wish by chaining calls to the `command` method. + +## Composition + +You can decorate all commands to execute some code on every command like this: + +```php title="bin/console" +new class extends Cli { + protected function configure(Application $app): Application + { + return $app + ->mapCommand( + static fn(Command $command) => new class($command) implements Command { + public function __construct( + private Command $inner, + ) {} + + public function __invoke(Console $console): Console + { + // you can execute code before here + $console = ($this->inner)($console); + // you can execute code after here + + return $console; + } + + public function usage(): string + { + return $this->inner->usage(); + } + } + ) + ->command(/* ... */) + ->command(/* ... */) + ->command(/* ... */); + ); + } +}; +``` + +For example you can use this approach to prevent commands to be run during deployments by checking if a file exists on the filesystem. diff --git a/docs/getting-started/framework/extensions.md b/docs/getting-started/framework/extensions.md new file mode 100644 index 0000000..103b6de --- /dev/null +++ b/docs/getting-started/framework/extensions.md @@ -0,0 +1,12 @@ +# Extensions + +## Built-in + +This framework comes with these middlewares: + +- `Innmind\Framework\Middleware\Optional` to load a middleware only if the class exist, as seen in the [profiler chapter](profiler.md) +- `Innmind\Framework\Middleware\LoadDotEnv` to load a `.env` file and inject the values in the `Innmind\Framework\Environment` object + +## Others + +You can find other packages exposing middlewares via the virutal package `innmind/framework-middlewares` on [Packagist](https://packagist.org/providers/innmind/framework-middlewares). diff --git a/docs/getting-started/framework/http.md b/docs/getting-started/framework/http.md new file mode 100644 index 0000000..6f61529 --- /dev/null +++ b/docs/getting-started/framework/http.md @@ -0,0 +1,169 @@ +# HTTP + +## Usage + +The first step is to define the entrypoint: + +```php title="public/index.php" +route( + 'GET /', + static fn(ServerRequest $request) => Response::of( + Response\StatusCode::ok, + $request->protocolVersion(), + null, + Content::ofString('Hello world'), + ), + ); + } +}; +``` + +Now `curl http://localhost:8080/` will return a `200` response with the content `Hello world`. + +You can specify placeholders in your routes like this: + +```php title="public/index.php" hl_lines="1 7 10 15" +use Innmind\Router\Route\Variables; + +new class extends Http { + protected function configure(Application $app): Application + { + return $app->route( + 'GET /greet/{name}', + static fn( + ServerRequest $request, + Variables $variables + ) => Response::of( + Response\StatusCode::ok, + $request->protocolVersion(), + null, + Content::ofString('Hello '.$variables->get('name')), + ), + ); + } +}; +``` + +`curl http://localhost:8080/greet/Jane` will return `Hello Jane`. + +!!! info "" + The route template is defined by the [RFC6570](https://tools.ietf.org/html/rfc6570). You can learn more about its implementation in this [package](https://github.com/Innmind/UrlTemplate). + +??? tip + The full definition of the function passed to the `route` method is: + + ```php + use Innmind\Http\{ + ServerRequest, + Response, + }; + use Innmind\Router\Route\Variables; + use Innmind\DI\Container; + use Innmind\OperatingSystem\OperatingSystem; + use Innmind\Framework\Environment; + + static fn( + ServerRequest $request + Variable $variables, + Container $container, + OperatingSystem $os, + Environment $env + ): Response; + ``` + + - `$request` is the parsed request sent by a user + - `$variables` gathers all the values described by the route template + - `$container` is a service locator + - `$os` you've seen it in a previous chapter + - `$env` contains the environment variables + +## Composition + +You can decorate all routes to execute some code on every route like this: + +```php title="public/index.php" +use Innmind\Framework\Http\RequestHandler; + +new class extends Http { + protected function configure(Application $app): Application + { + return $app + ->mapRequestHandler( + static fn(RequestHandler $route) => new class($route) implements RequestHandler { + public function __construct( + private RequestHandler $inner, + ) {} + + public function __invoke(ServerRequest $request): Response + { + // you can execute code before here + $response = ($this->inner)($request); + // you can execute code after here + + return $response; + } + } + ) + ->route(/* ... */) + ->route(/* ... */) + ->route(/* ... */); + ); + } +}; +``` + +For example you can use this approach to prevent routes to be run during deployments by checking if a file exists on the filesystem. + +## Webserver + +### Apache, Nginx, Caddy, etc... + +In the example above we expose the program via a `public/` folder. You can expose this folder with any HTTP server you're familiar with. + +You'll need to enable url rewriting so all paths requested are redirected to the `index.php` file. + +### Built-in + +Instead of using the `Innmind\Framework\Main\Http` entrypoint you can use `Innmind\Framework\Main\Async\Http`. Now the PHP file is a CLI program that will open the port `8080` by default on your machine. + +You can send it `curl` requests just as before. The difference is that your code now runs asynchronously (1). +{.annotate} + +1. As long as you use the [Operating System abstraction](../operating-system/index.md). + +!!! warning "" + This execution mode however is limited. For example it doesn't support multipart requests. + + You should use this as an experiment to see how your code behave asynchronously. diff --git a/docs/getting-started/framework/index.md b/docs/getting-started/framework/index.md new file mode 100644 index 0000000..74f53c0 --- /dev/null +++ b/docs/getting-started/framework/index.md @@ -0,0 +1,22 @@ +# Framework + +## Installation + +```sh +composer require innmind/framework:~2.2 +``` + +## Concepts + +The framework is defined by an entrypoint that specify the context in which the framework will be run. Each entrypoint exposes a `configure` method to configure an immutable `Application` object. + +`Application` is the way to describe what your program can do. This is the same class no matter which entrypoint you choose. This allows you to switch the execution context without modifying any line of your code (1). +{.annotate} + +1. For example moving from a synchronous HTTP context to an async HTTP server. + +The other advantage of this approach is that if your program is accessible from both HTTP and CLI it can be [configured by the same code](middlewares.md). + +## Advanced usage + +The framework offers more than the features shown in this documentation, after reading the following chapters you should head to the [package](https://github.com/Innmind/framework/) to learn the extent of its capabilities. diff --git a/docs/getting-started/framework/middlewares.md b/docs/getting-started/framework/middlewares.md new file mode 100644 index 0000000..ec85007 --- /dev/null +++ b/docs/getting-started/framework/middlewares.md @@ -0,0 +1,146 @@ +# Middlewares + +So far you've configured each kind of app directly in its entrypoint. This is fine for small apps that you don't unit test. But as your program grows you'll need to better structure it and test it. + +The way to organise the framework configuration is through _middlewares_. And it looks like that: + +```php +use Innmind\Framework\{ + Application, + Middleware, +}; + +final class MyMiddleware implements Middleware +{ + public function __invoke(Application $app): Application + { + return $app + ->command(/* as seen previously */) + ->route(/* as seen previously */); + } +} +``` + +In this method you can configure the `Application` the same way you would do in the entrypoint. And since the configuration api is the same no matter the entry point chosen (HTTP or CLI) you can declare both CLI commands and HTTP routes inside the same middleware. + +And to use it: + +```php +use Innmind\Framework\{ + Main\Http, + Application, +}; + +new class extends Http { + protected function configure(Application $app): Application + { + return $app->map(new MyMiddleware); + } +}; +``` + +??? info + The notation `$app->map($middleware)` is just an invertion of who calls who for better chaining methods. If you look at the implementation it does `$middleware($app)`. + +Since the middleware is a plain old PHP object, you can also add parameters to it. + +Let's say your program is a website that is accessible both in french and english. Instead of adding a parameter in every route and pass it around in every layer of your program you could do: + +=== "Entrypoint" + ```php + use Innmind\Framework\{ + Main\Http, + Application, + }; + + new class extends Http { + protected function configure(Application $app): Application + { + return $app + ->map(new MyMiddleware('fr')) + ->map(new MyMiddleware('en')); + } + }; + ``` + +=== "Middleware" + ```php + final class MyMiddleware implements Middleware + { + private string $language; + + public function __construct(string $language) + { + $this->language = $language; + } + + public function __invoke(Application $app): Application + { + return $app + ->route("GET /{$this->language}", /* index handler */) + ->route("GET /{$this->language}/route1", /* handler */) + ->route("GET /{$this->language}/route2", /* handler */) + ->route("GET /{$this->language}/route/etc", /* handler */); + } + } + ``` + +??? tip + And since you have access to the language at the configuration time you could even use different databases. + + === "Middleware" + ```php + final class MyMiddleware implements Middleware + { + private string $language; + + public function __construct(string $language) + { + $this->language = $language; + } + + public function __invoke(Application $app): Application + { + return $app + ->service( + Services::database($this->language), + fn($_, $os) => $os + ->remote() + ->sql(Url::of(match ($this->language) { + 'en' => 'mysql://127.0.0.1:3306/website_en', + 'fr' => 'mysql://127.0.0.1:3306/website_fr', + })), + ) + ->route( + "GET /{$this->language}", + function( + $request, + $variables, + Container $get, + ) { + $sql = $get(Services::database($this->language)); + $someData = $sql(/* some Query */); + + return Response::of(/* build response with $someData */); + } + ); + } + } + ``` + + === "`Services`" + ```php + enum Services + { + case databaseEn; + case databaseFr; + + public static function database(string $language): self + { + return match ($language) { + 'en' => self::databaseEn, + 'fr' => self::databaseFr, + }; + } + } + ``` diff --git a/docs/getting-started/framework/profiler.md b/docs/getting-started/framework/profiler.md new file mode 100644 index 0000000..17418bb --- /dev/null +++ b/docs/getting-started/framework/profiler.md @@ -0,0 +1,92 @@ +# Profiler + +Innmind comes with an optional profiler to help you debug your program. + +!!! success "" + It works for both HTTP (sync or async) and CLI programs. + +## Installation + +```sh +composer require --dev innmind/debug:~4.0 +``` + +## Usage + +```php +use Innmind\Framework\{ + Application, + Main\Http, + Middleware\Optional, +}; +use Innmind\Profiler\Web\Kernel as Profiler; +use Innmind\Debug\Kernel as Debug; +use Innmind\Url\Path; + +new class extends Http { + protected function configure(Application $app): Application + { + return $app + ->map(Optional::of( + Debug::class, + static fn() => Debug::inApp()->operatingSystem(), #(1) + )) + ->map(new MyMiddleware) + ->map(Optional::of( + Profiler::class, + static fn() => Profiler::inApp(Path::of('var/profiler/')), #(2) + )) + ->map(Optional::of( + Debug::class, + static fn() => Debug::inApp()->app(), #(3) + )); + } +}; +``` + +1. This will record every calls made to the Operating System. +2. This exposes the profiler's HTTP routes. The path is where the profiles will be stored. +3. This will record the incoming HTTP requests and CLI commands and their results. + +!!! note "" + The `Optional` middleware will not call the underlying middleware if the class doesn't exist. This allows to automatically not declare the profiler in production, since it's a composer _dev_ dependency. + +You can then access the profiler via `GET /_profiler/`. It will show the list of recorded profiles: + +![](../../assets/profiler/index.png) + +:material-checkbox-blank-circle:{ style="color: rgb(104, 255, 101) " } is successful, :material-checkbox-blank-circle:{ style="color: rgb(255, 179, 48) " } is still running and :material-checkbox-blank-circle:{ style="color: rgb(255, 79, 86) " } failed. + +This profiler's advantages is that you can see the profiles while it's being recorded. This means that you don't have to wait for a long CLI command to finish to see what happened. + +The other advantage is the way profiles are stored: in a human readable way. If your program does a lot of things (like hundreds of HTTP calls) you can browse the profile's folder and look around individual calls. + +??? Tip "Screenshots" + === "HTTP" + ![](../../assets/profiler/http.png) + + === "Exception" + ![](../../assets/profiler/exception.png) + + !!! warning "" + You need to have [graphviz](https://graphviz.org) installed to view this. + + === "App graph" + ![](../../assets/profiler/app_graph.png) + + This is the object tree loaded to respond to a request/command. + + !!! warning "" + You need to have [graphviz](https://graphviz.org) installed to view this. + + === "Environment" + ![](../../assets/profiler/environment.png) + + === "Processes" + ![](../../assets/profiler/processes.png) + + === "Remote Processes" + ![](../../assets/profiler/remote_processes.png) + + === "Remote HTTP" + ![](../../assets/profiler/remote_http.png) diff --git a/docs/getting-started/handling-data/either.md b/docs/getting-started/handling-data/either.md new file mode 100644 index 0000000..aa21b67 --- /dev/null +++ b/docs/getting-started/handling-data/either.md @@ -0,0 +1,178 @@ +# Either + +The `Either` monad always represents a value but it's either on a _right side_ or _left side_. + +If you've understood [`Maybe`](maybe.md), it's an `Either` with the `Maybe` value on the _right side_ or `null` as the value on the _left side_. + +In essence: +```php +use Innmind\Immutable\Either; + +$right = Maybe::just(42); +$left = Maybe::nothing(); +// becomes +$right = Either::right(42); +$left = Either::left(null); +``` + +It is usually used to express a value for a nominal case on the right side and the errors that may occur on the left side. This means that it replaces the use of `Exception`s. + +Each example will show how to use `Either` and the imperative equivalent in plain old PHP via `Exception`s. + +??? tip + This is because `Maybe` and `Either` are very similar that you can switch for one type to another via `$maybe->either()` (1) or `$either->maybe()` (2). + {.annotate} + + 1. then the left side is `null` + 2. the left value is thrown away + +## Usage + +Let's say you want to create a user from an email and the function must fail in case the email already exist. You could do: + +=== "Innmind" + ```php + /** + * @return Either + */ + function createUser(string $email, string $name): Either { + if (/* some condition to check if email is already known*/) { + return Either::left(new EmailAlreadyUsed); + } + + /* code to insert the user in a db */ + $user = new User($email, $name); + + return Either::right($user); + } + + createUser('foo@example.com', 'John Doe')->match( + static fn(User $user) => doStuff($user), + static fn(EmailAlreadyUsed $error) => doOtherStuff(), + ); + ``` + +=== "Imperative" + ```php + /** + * @throws EmailAlreadyUsed + * @return User + */ + function createUser(string $email, string $name): User { + if (/* some condition to check if email is already known*/) { + throw new EmailAlreadyUsed; + } + + /* code to insert the user in a db */ + $user = new User($email, $name); + + return $user; + } + + try { + $user = createUser('foo@example.com', 'John Doe'); + doStuff($user); + } catch (EmailAlreadyUsed $e) { + doOtherStuff(); + } + ``` + +Here we use a `string` to represent an email, instead [we should use an object](../../philosophy/explicit.md#parse-dont-validate) to abstract it to make sure the value is indeed an email. + +=== "Innmind" + ```php + final class Email + { + /** + * @return Either + */ + public static function of(string $value): Either + { + if (/* check value */) { + return Either::right(new self($value)); + } + + return Either::left(new InvalidEmail); + } + } + + Email::of('foo@example.com') + ->flatMap(static fn(Email $email) => createUser($email, 'John Doe')) + ->match( + static fn(User $user) => doStuff($user), + static fn(InvalidEmail | EmailAlreadyUsed $error) => doOtherStuff(), + ); + ``` + +=== "Imperative" + ```php + final class Email + { + /** + * @throws InvalidEmail + * @return self + */ + public static function of(string $value): self + { + if (/* check value */) { + return new self($value); + } + + throw new InvalidEmail; + } + } + + try { + $email = Email::of('foo@example.com'); + $user = createUser($email, 'John Doe'); + doStuff($user); + } catch (InvalidEmail | EmailAlreadyUsed $e) { + doOtherStuff(); + } + ``` + +!!! success "" + Both approaches seem very similar but there's a big advantage to `Either`: a static analysis tool understands the flow of errors and can tell you if when calling `match` you don't handle all possible error values. No tool can help you do the same with exceptions. + +Just like `Maybe` you can recover in case of an error via the `otherwise` method. For example in the case the email is already used, instead of failing we can decide to update the stored user. + +=== "Innmind" + ```php + Email::of('foo@example.com') + ->flatMap( + static fn(Email $email) => createUser($email, 'John Doe')->otherwise( + static fn(EmailAlreadyUsed $error) => updateUser($email, 'John Doe'), + ), + ) + ->match( + static fn(User $user) => doStuff($user), + static fn(InvalidEmail $error) => doOtherStuff(), + ); + ``` + +=== "Imperative" + ```php + try { + $email = Email::of('foo@example.com'); + + try { + $user = createUser($email, 'John Doe'); + } catch (EmailAlreadyUsed $e) { + $user = updateUser($email, 'John Doe'); + } + + doStuff($user); + } catch (InvalidEmail $e) { + doOtherStuff(); + } + ``` + +In all examples you've seen the use of `flatMap` but you can also use the `map` to modify the value on the right side. And there's a `leftMap` to modify the value on the left side. + +## In the ecosystem + +`Either` is used when an action may have multiple cases of errors that you should handle, such as [HTTP calls](../operating-system/http.md) or when working with [queues](../concurrency/queues.md). + +But the beauty is that if you don't want to deal with the different errors you can throw them away by converting the `Either` to a `Maybe` via `$either->maybe()`. + +Like `Maybe` and `Sequence` is has a [deferred mode](sequence.md#deferred) that allows to postpone some actions as you'll see in the [concurrent HTTP calls chapter](../concurrency/http.md). diff --git a/docs/getting-started/handling-data/index.md b/docs/getting-started/handling-data/index.md new file mode 100644 index 0000000..c44b634 --- /dev/null +++ b/docs/getting-started/handling-data/index.md @@ -0,0 +1,16 @@ +# Handling data + +Handling data is an essential part of any program. Handling them simply and in a safe way can become difficult as a program becomes more complex. + +In this chapter you'll find the 3 most used data structures throughout Innmind. + +You'll learn how to them for simple cases and how they become indispensable as a program grows. + +??? note + Head to the [package documentation](https://github.com/Innmind/Immutable/) to learn about the other data structures. + +## Installation + +```sh +composer require innmind/immutable:~5.3 +``` diff --git a/docs/getting-started/handling-data/maybe.md b/docs/getting-started/handling-data/maybe.md new file mode 100644 index 0000000..d0fbb95 --- /dev/null +++ b/docs/getting-started/handling-data/maybe.md @@ -0,0 +1,222 @@ +# Maybe + +A `Maybe` monad represents the possible absence of a value. + +This is an equivalent of a nullable value, but a more faithful representation would be an `array` containing 0 or 1 value. + +In essence: +```php +use Innmind\Immutable\Maybe; + +$valueExist = 42; +$valueDoesNotExist = null; +// or +$valueExist = [42]; +$valueDoesNotExist = []; +// becomes +$valueExist = Maybe::just(42); +$valueDoesNotExist = Maybe::nothing(); +``` + +Each example will show how to use `Maybe` and the imperative equivalent in plain old PHP (1). +{.annotate} + +1. The nullable approach is used as it's the dominant approach in PHP programs. + +## Usage + +=== "Innmind" + ```php + /** + * @return Maybe + */ + function getUser(int $id): Maybe { + return match ($id) { + 42 => Maybe::just(new User), + default => Maybe::nothing(), + }; + } + ``` + +=== "Imperative" + ```php + function getUser(int $id): ?User { + return match ($id) { + 42 => new User, + default => null, + }; + } + ``` + +In this function we represent the fact that they're may be not a `User` (1) for every id. To work with the user, if there's any, you would do: +{.annotate} + +1. This is a fake class. + +=== "Innmind" + ```php + getUser(42)->match( + static fn(User $user) => doStuff($user), + static fn() => userDoesntExist(), + ); + ``` + +=== "Imperative" + ```php + match ($user = getUser(42)) { + null => userDoesntExist(), + default => doStuff($user), + }; + ``` + +As you can see the 2 approaches are very similar for now. + +In this example the user is directly used as an argument to a function but we often want to extract some data before calling some function. A use case could be to extract the brother id out of this user (1) and call again our function. +{.annotate} + +1. Via a method `#!php function getBrotherId(): int`. + +=== "Innmind" + ```php + getUser(42) + ->map(static fn(User $user) => $user->getBrotherId()) + ->flatMap(static fn(int $id) => getUser($id)) + ->match( + static fn(User $brother) => doStuff($brother), + static fn() => brotherDoesNotExist(), + ); + ``` + +=== "Imperative" + ```php + $user = getUser(42); + + if (\is_null($user)) { + brotherDoesNotExist(); + + return; + } + + $brother = getUser($user->getBrotherId()); + + if (\is_null($brother)) { + brotherDoesNotExist(); + + return; + } + + doStuff($brother); + ``` + +This example introduces the `map` and `flatMap` methods. They behave the same way as their `Sequence` counterpart. + +- `map` will apply the function in case the `Maybe` contains a value +- `flatMap` is similar to `map` except that the function passed to it must return a `Maybe`, instead of having the return type `Maybe>` (1) you'll have a `Maybe` +{.annotate} + + 1. as you would by using `map` instead of `flatMap` + +What this example shows is that with `Maybe` you only need to deal with the possible absence of the data when you extract it. While with the imperative style you need to deal with it each time you call a function. + +This becomes even more flagrant if the method that returns the brother id itself may not return a value (1). The signature becomes `#!php function getBrotherId(): Maybe`. +{.annotate} + +1. as one may not have one + +=== "Innmind" + ```php hl_lines="2" + getUser(42) + ->flatMap(static fn(User $user) => $user->getBrotherId()) #(1) + ->flatMap(static fn(int $id) => getUser($id)) + ->match( + static fn(User $brother) => doStuff($brother), + static fn() => brotherDoesNotExist(), + ); + ``` + + 1. This is the only change, `map` is replaced by `flatMap` do deal with the possible absence. + +=== "Imperative" + ```php hl_lines="9-15" + $user = getUser(42); + + if (\is_null($user)) { + brotherDoesNotExist(); + + return; + } + + $brotherId = $user->getBrotherId(); + + if (\is_null($brotherId)) { + brotherDoesNotExist(); + + return; + } + + $brother = getUser($brotherId); + + if (\is_null($brother)) { + brotherDoesNotExist(); + + return; + } + + doStuff($brother); + ``` + +So far we _do nothing_ in case our user doesn't have a brother. But what if we want to check if he has a sister in case he doesn't have a brother ? `Maybe` has an expressive way to describe such case: + +=== "Innmind" + ```php hl_lines="5" + getUser(42) + ->flatMap( + static fn(User $user) => $user + ->getBrotherId() + ->otherwise(static fn() => $user->getSistserId()), + ) + ->flatMap(static fn(int $id) => getUser($id)) + ->match( + static fn(User $sibling) => doStuff($sibling), + static fn() => brotherDoesNotExist(), + ); + ``` + +=== "Imperative" + ```php hl_lines="9" + $user = getUser(42); + + if (\is_null($user)) { + brotherDoesNotExist(); + + return; + } + + $siblingId = $user->getBrotherId() ?? $user->getSisterId(); + + if (\is_null($siblingId)) { + brotherDoesNotExist(); + + return; + } + + $sibling = getUser($siblingId); + + if (\is_null($sibling)) { + brotherDoesNotExist(); + + return; + } + + doStuff($sibling); + ``` + +## In the ecosystem + +`Maybe` is used to express the abscence of data (1) or the possible failure of an operation (2). For the latter it is expressed via `Maybe`, meaning if it contains a `SideEffect` the operation as succeeded otherwise it failed. +{.annotate} + +1. Such as the absence of a [file on the filesystem](../operating-system/filesystem.md) or the absence of an [entity from a storage](../orm/index.md). +2. Such as failing to [upload a file to an S3 bucket](https://github.com/Innmind/S3). + +It also has a [deferred mode like `Sequence`](sequence.md#deferred) that allows to not directly load in memory a value when you call `$sequence->get($index)`. The returned `Maybe` in this case will load the value when you call the `match` method. diff --git a/docs/getting-started/handling-data/sequence.md b/docs/getting-started/handling-data/sequence.md new file mode 100644 index 0000000..66de6c9 --- /dev/null +++ b/docs/getting-started/handling-data/sequence.md @@ -0,0 +1,600 @@ +# Sequence + +A `Sequence` monad represents a succession of values. In plain old PHP this is an array where you don't specify any key. + +In essence: +```php +use Innmind\Immutable\Sequence; + +$values = ['foo', 'bar', 'baz']; +// becomes +$values = Sequence::of('foo', 'bar', 'baz'); +``` + +Of course just holding to multiple values is not very useful in itself. You'll see below how to manipulate this list of values. + +Each example will show how to use the `Sequence` and how to do the same in plain old PHP in a declarative and imperative style. So you can better grasp what's happening. + +## Pipelining + +When dealing with a list of values we tend to apply multiple logic to it in succession. The more steps to transform our values the more complex it becomes. + +The `Sequence` helps better break down each step. + +### Transformations + +If we reuse the example with the strings and we want to uppercase the first letter of each value we would do: + +=== "Innmind" + ```php + $values = Sequence::of('foo', 'bar', 'baz') + ->map(static fn(string $string) => \ucfirst($string)) + ->toList(); + $values === ['Foo', 'Bar', 'Baz']; // returns true + ``` + +=== "Declarative" + ```php + $values = \array_map( + static fn(string $string) => \ucfirst($string), + ['foo', 'bar', 'baz'], + ); + $values === ['Foo', 'Bar', 'Baz']; // returns true + ``` + +=== "Imperative" + ```php + $values = []; + + foreach (['foo', 'bar', 'baz'] as $string) { + $values[] = \ucfirst($string); + } + + $values === ['Foo', 'Bar', 'Baz']; // returns true + ``` + +The `map` method returns a new object `Sequence` with all the values modified by the function passed as argument. And the original object returned by `Sequence::of()` is not altered, meaning you can reuse it safely to do other operations. + +??? tip + The notation `#!php static fn(string $string) => \ucfirst($string)` can be shortened to `#!php \ucfirst(...)`. + +With `map` the new object will contain the same number of values as the initial object. But some times for each value you want to return multiple values and in the end have an `array` with only one dimension. + +Let's take the example where each `string` represent a username and we want to retrieve their addresses: + +=== "Innmind" + ```php + /** + * @return Sequence + */ + function getAddresses(string $username): Sequence { + // this is a fake implementation + return Sequence::of( + "$username address 1", + "$username address 2", + "$username address 3", + ); + } + + $addresses = Sequence::of('foo', 'bar', 'baz') + ->flatMap(static fn(string $username) => getAdresses($username)) + ->toList(); + $addresses === [ + 'foo address 1', + 'foo address 2', + 'foo address 3', + 'bar address 1', + 'bar address 2', + 'bar address 3', + 'baz address 1', + 'baz address 2', + 'baz address 3', + ]; // returns true + ``` + +=== "Declarative" + ```php + /** + * @return list + */ + function getAddresses(string $username): array { + // this is a fake implementation + return [ + "$username address 1", + "$username address 2", + "$username address 3", + ]; + } + + $addressesPerUser = \array_map( + static fn(string $username) => getAddresses($username), + ['foo', 'bar', 'baz'], + ); + $addresses = \array_merge(...$addressesPerUser); + $addresses === [ + 'foo address 1', + 'foo address 2', + 'foo address 3', + 'bar address 1', + 'bar address 2', + 'bar address 3', + 'baz address 1', + 'baz address 2', + 'baz address 3', + ]; // returns true + ``` + +=== "Imperative" + ```php + /** + * @return list + */ + function getAddresses(string $username): array { + // this is a fake implementation + return [ + "$username address 1", + "$username address 2", + "$username address 3", + ]; + } + + $addresses = []; + + foreach (['foo', 'bar', 'baz'] as $username) { + foreach (getAddresses($username) as $address) { + $addresses[] = $address; + } + } + + $addresses === [ + 'foo address 1', + 'foo address 2', + 'foo address 3', + 'bar address 1', + 'bar address 2', + 'bar address 3', + 'baz address 1', + 'baz address 2', + 'baz address 3', + ]; // returns true + ``` + +Here you can see that `flatMap` is a combination of `map` that would return the type `Sequence>` and then flattens it to obtain a `Sequence`, hence the name `flatMap`. + +You can also already see that the `Sequence` is simpler to chain actions because there is no need to assign the intermediate values to a new variable. In plain old PHP you could also avoid the intermediate values by inlining the calls but you'll quickly end up with a code harder to read with a lot of indentation. + +`map` and `flatMap` are the only 2 methods to apply a modification to a `Sequence`. + +### Composition + +Since you'll not always have all the values known when creating a `Sequence`, you need to know how to add new values. + +=== "Innmind" + ```php + $values = Sequence::of('foo') + ->add('bar') + ->add('baz') + ->toList(); + $values = ['foo', 'bar', 'baz']; // return true + ``` + +=== "Declarative" + ```php + $values = \array_merge( + ['foo'], + ['bar'], + ['baz'], + ); + + $values === ['foo', 'bar', 'baz']; // returns true + ``` + +=== "Imperative" + ```php + $values = ['foo']; + $values[] = 'bar'; + $values[] = 'baz'; + + $values === ['foo', 'bar', 'baz']; // returns true + ``` + +??? tip + You may also come across the notation `#!php $values = Sequence::of('foo')('bar')('baz')` in the ecosystem. This is a more _math like_ notation to look like a matrix augmentation. + + You check the implementation of `Sequence::add()` you'll see that it is an alias to the `__invoke` method that allows this notation. + +If instead of adding a single value to the list you need to add multiple ones you would do: + +=== "Innmind" + ```php + $values = Sequence::of('foo', 'bar') + ->append(Sequence::of('baz', 'other', 'string')) + ->toList(); + $values === ['foo', 'bar', 'baz', 'other', 'string']; // returns true + ``` + +=== "Declarative" + ```php + $values = \array_merge( + ['foo', 'bar'], + ['baz', 'other', 'string'], + ); + $values === ['foo', 'bar', 'baz', 'other', 'string']; // returns true + ``` + +=== "Imperative" + ```php + $values = ['foo', 'bar']; + + foreach (['baz', 'other', 'string'] as $string) { + $values[] = $string; + } + + $values === ['foo', 'bar', 'baz', 'other', 'string']; // returns true + ``` + +### Filtering + +Instead of adding values you may want to remove values from a list you're given to only keep the ones you really want. + +For example let's you have a list of cities and you only want to keep the french ones: + +=== "Innmind" + ```php + $cities = Sequence::of( + 'Paris, France', + 'New York, USA', + 'London, UK', + 'Lyon, France', + 'etc...', + ) + ->filter(static fn(string $city) => \str_contains($city, 'France')) + ->toList(); + $cities === ['Paris, France', 'Lyon, France']; // returns true + ``` + +=== "Declarative" + ```php + $values = \array_filter( + [ + 'Paris, France', + 'New York, USA', + 'London, UK', + 'Lyon, France', + 'etc...', + ], + static fn(string $city) => \str_contains($city, 'France'), + ); + $cities === ['Paris, France', 'Lyon, France']; // returns true + ``` + +=== "Imperative" + ```php + $cities = [ + 'Paris, France', + 'New York, USA', + 'London, UK', + 'Lyon, France', + 'etc...', + ]; + $values = []; + + foreach ($cities as $city) { + if (\str_contains($city, 'France')) { + $values[] = $city; + } + } + + $cities === ['Paris, France', 'Lyon, France']; // returns true + ``` + +??? tip + And if instead you want all the cities outside of France you can replace `filter` by `exclude`. + +The `filter` method is fine if you don't need the new `Sequence` type to change, here we go from `Sequence` to `Sequence`. But if you have a `Sequence` and you want to remove the `null` values then `filter`, even though will do the job, will return a `Sequence`. This is a limitation of [Psalm](../../philosophy/development.md#type-strictness). + +To overcome this problem you should use the method `keep` that expect a `Predicate` as argument. Technically the implementation of the predicate will be the same as the function passed to `filter` but it has a mechanism that allows Psalm to understand what you intend to do. + +For our example you'd use it like this: + +```php +use Innmind\Immutable\Predicate\Instance; + +$values = Sequence::of(null, new \SplFileObject('some file.txt'), /* etc */) + ->keep(Instance::of(\SplFileObject::class)); +$values; // Sequence<\SplFileObject> +``` + +### Pipeline + +So far you've only seen how to do one action at a time. The simplicity of `Sequence` starts to shine when chaining multiple actions. + +Let's try to retrieve all the visited cities for each username, keep the french ones and remove the country from the name. + +=== "Innmind" + ```php + /** + * @return Sequence + */ + function getCities(string $username): Sequence { + // fake implementation + return match ($username) { + 'foo' => Sequence::of('Paris, France', 'London, UK'), + 'bar' => Sequence::of('New York, USA', 'London, UK'), + 'baz' => Sequence::of('New York, USA', 'Lyon, France'), + default => Sequence::of(), + }; + } + + $cities = Sequence::of('foo', 'bar', 'baz') + ->flatMap(static fn(string $username) => getCities($username)) + ->filter(static fn(string $city) => \str_contains($city, 'France')) + ->map(static fn(string $city) => \substr($city, 0, -8)) + ->toList(); + $cities === ['Paris', 'Lyon']; + ``` + +=== "Declarative" + ```php + /** + * @return list + */ + function getCities(string $username): array { + // fake implementation + return match ($username) { + 'foo' => ['Paris, France', 'London, UK'], + 'bar' => ['New York, USA', 'London, UK'], + 'baz' => ['New York, USA', 'Lyon, France'], + default => [], + }; + } + + $citiesPerUser = \array_map( + static fn(string $username) => getCities($username), + ['foo', 'bar', 'baz'], + ); + $cities = \array_merge(...$citiesPerUser); + $cities = \array_filter( + $cities, + static fn(string $city) => \str_contains($city, 'France'), + ); + $cities = \array_map( + static fn(string $city) => \substr($city, 0, -8), + $cities, + ); + $cities === ['Paris', 'Lyon']; + ``` + +=== "Imperative" + ```php + /** + * @return list + */ + function getCities(string $username): array { + // fake implementation + return match ($username) { + 'foo' => ['Paris, France', 'London, UK'], + 'bar' => ['New York, USA', 'London, UK'], + 'baz' => ['New York, USA', 'Lyon, France'], + default => [], + }; + } + + $cities = []; + + foreach (['foo', 'bar', 'baz'] as $username) { + foreach (getCities($username) as $city) { + if (\str_contains($city, 'France')) { + $city = \substr($city, 0, -8); + + $cities[] = $city; + } + } + } + + $cities === ['Paris', 'Lyon']; + ``` + +With the declarative and imperative approach you have to deal with either a lot of indentation or a lot of variables. With a `Sequence` you just keep chaining methods. + +Another nice upside to `Sequence` is when you try to build a pipeline and want to see the different results if you switch some logic around. To achieve it you only need to move a method call up or down, while the other approaches you need to be aware of conflicting variables. + +## Extracting data + +At some point you'll need to extract the values contained in a `Sequence` (1). So far you've only seen `toList` that return all the values in an `array`. +{.annotate} + +1. For persisting them to a database, sending them in an HTTP response, etc... + +### Computing a value + +=== "Innmind" + ```php + $sum = Sequence::of(1, 2, 3, 4)->reduce( + 0, + static fn(int $carry, int $value) => $carry + $value, + ); + $sum === 10; // returns true + ``` + +=== "Declarative" + ```php + $sum = \array_reduce( + [1, 2, 3, 4], + static fn(int $carry, int $value) => $carry + $value, + 0, + ); + $sum === 10; // returns true + ``` + +=== "Imperative" + ```php + $sum = 0; + + foreach ([1, 2, 3, 4] as $value) { + $sum += $value; + } + + $sum === 10; // returns true + ``` + +### Fetching a value at an index + +=== "Innmind" + ```php + $values = Sequence::of(1, 2, 3, 4); + $value1 = $values->get(1)->match( + static fn(int $value) => $value, + static fn() => null, + ); + $value2 = $values->get(100)->match( + static fn(int $value) => $value, + static fn() => null, + ); + $value1 === 2; // returns true + $value2 === null; // returns true + ``` + +=== "Imperative" + ```php + $values = [1, 2, 3, 4]; + + if (\array_key_exists(1, $values)) { + $value1 = $values[1]; + } else { + $value1 = null; + } + + if (\array_key_exists(100, $values)) { + $value2 = $values[100]; + } else { + $value2 = null; + } + + $value1 === 2; // returns true + $value2 === null; // returns true + ``` + +??? info + The imperative approach could be simplified via `#!php $values[$index] ?? null`, but then if the value at the index is itself `null` you can't differentiate if the index exists or not. + +### And more + +Above are a few examples of the way to extract data. You should look at all the methods available on the `Sequence` class to see if one fit your needs. + +## Execution mode + +The power of `Sequence` is that you can change the way its implementation behave depending on your needs, without rearchitecting your whole program. You'll usually switch the mode for performance reasons. + +### In memory + +This is the mode you've seen so far. When calling `Sequence::of()` you specify all the values and they're kept in memory. + +### Deferred + +Instead of specyfying the values you can use a `Generator` to populate the `Sequence`. Once a value is loaded it's kept in memory. The advantage is that you can loop over the same generator multiple times (1). +{.annotate} + +1. Using a `Generator` directly requires to call again the function that created it. But this means you may not end up with the same values (especially if generating objects). + +```php +$values = Sequence::of(1, 2, 3, 4); +// becomes +$values = Sequence::defer((static function() { + yield 1; + yield 2; + yield 3; + yield 4; +})()); +``` + +The `Sequence` is then used exactly the same way as an in memory one. + +!!! tip "" + You should use this mode when loading values may be expensive and you're not sure all the values will be loaded. This way you save a bit of time and memory by not fetching the values you don't end up needing. + +### Lazy + +With this mode you build a `Sequence` by passing a function that returns a `Generator`. This function will be called each time you try to extract some data from the `Sequence`. + +```php +$values = Sequence::lazy(static function() { + $file = \fopen('some file.txt', 'r'); + + while ($chunk = \fgets($file, 256)) { + yield $chunk; + } +}); +``` + +The `Sequence` is then used exactly the same way as an in memory one. + +!!! tip "" + You should use this mode to handle an infinite list of values or a list of values that can't fit in memory (1). + {.annotate} + + 1. Such as reading a multi gigabyte file or reading from a socket. + +??? info + This is where lies the root of the power of Innmind. Being able to work with infinte volumes of data as if it were in memory. + +### Tips + +#### Lazyness + +When using `::defer()` or `::lazy()` your code won't be called until you try to extract data (1) or call the `memoize` method. +{.annotate} + +1. Any method that return something else than a monad (`Sequence`, `Set` or `Maybe`). + +For example if you want to print all the lines from a file, this will do nothing: + +```php +Sequence::lazy(static function() { + $file = \fopen('some file.txt', 'r'); + + while ($line = \fgets($file)) { + yield $line; + } +})->map(static function($line) { + echo $line; +}); +``` + +This does nothing because `map` returns a new lazy `Sequence` with a `null` value for each line. Instead you should do: + +```php +Sequence::lazy(static function() { + $file = \fopen('some file.txt', 'r'); + + while ($line = \fgets($file)) { + yield $line; + } +})->foreach(static function($line) { + echo $line; +}); +``` + +`foreach` returns a `Innmind\Immutable\SideEffect` so the `Sequence` knows that it needs to call all the logic you specified. + +#### Psalm + +If you call the `foreach` method you won't be able to use the returned value as it's an object that does nothing. It's returned because `Sequence` is an immutable class, meaning all methods **must** return a value otherwise Psalm tells that the method is useless. + +But you still need to assigned the returned value to a variable `$_` (1) otherwise Psalm will tell you that the call to `foreach` does nothing. +{.annotate} + +1. Called a sink. Psalm won't run any analysis on this variable because it starts with an underscore. + +## In the ecosystem + +You'll find this class used pretty much everywhere in this ecosystem at it allows to describe: + +- a list of values +- a file as a lazy list of chunks +- a file as a lazy list of lines +- a directory as lazy list of files +- a socket as a lazy list of frames +- a SQL result as a lazy list of rows +- a process output as a lazy list of chunks +- and more... diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 0000000..079f3db --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,6 @@ +# Getting started + +In this chapter you'll learn all the basic information to create simple scripts to handle data up to distributed programs. + +!!! note + For advanced usage of a particular package you should head to the package documentation. diff --git a/docs/getting-started/operating-system/clock.md b/docs/getting-started/operating-system/clock.md new file mode 100644 index 0000000..6ab435f --- /dev/null +++ b/docs/getting-started/operating-system/clock.md @@ -0,0 +1,90 @@ +# Clock + +PHP allows to access time anywhere in a program in various ways (1) but this becomes problematic when you want to test your program. Especially if it depends heavily on time. +{.annotate} + +1. `time`, `new \DateTime`, etc... + +By using a _clock_ object that you inject everywhere you need to access time you'll be able to configure the date on which your program is tested. This means that you can test your program in the past (1), present and future (2). +{.annotate} + +1. So you can reproduce bugs that appeared in production. +2. You can anticipate problematic times such as leap years, daylight saving time, etc... + +And to ease the manipulation, all objects are immutable. + +## Accessing time + +```php +use Innmind\TimeContinuum\{ + PointInTime, + Earth\Format\ISO8601, +}; + +echo $os + ->clock() + ->now() // returns a PointInTime object + ->format(new ISO8601); +``` + +This will print something like `2024-05-04T13:05:01+02:00`. + +!!! tip "" + You can specify your own formats by implementing the `Innmind\TimeContinuum\Format` interface. The format itself is described by a string that must be understood by the `\DateTimeInterface::format()` method. + +??? info + The format to transform a `PointInTime` to a `string` has to specified via an object to force you to give a name to a specific format in order to avoid to spread magic strings in your whole codebase. + +On a `PointInTime` you can access every part of the time it references (year, month, day, etc...), and has methods to modify the time to move forward or backward in time. + +## Parsing time from a string + +When you receive a `string` (1) that represent a date and you want to convert it to a `PointInTime` you should also use the clock: +{.annotate} + +1. from an HTTP request or loading it from a database + +```php +$point = $os + ->clock() + ->at($string, new ISO8601) #(1) + ->match( + static fn(PointInTime $point) => $point, + static fn() => throw new \RuntimeException("'$string' is not a valid date"), + ); +``` + +1. The format is optional but you **should** specify one to avoid implicit convertions. + +The `at` method returns the `Maybe` type to make sure you always handle the case the `$string` is invalid. + +## Calculating elapsed time + +We need to calculate elapsed time, among other cases, when handling heartbeats when dealing with sockets or in tests to make sure some code is executed in a certain amount of time. + +The usual approach is to use a call to `microtime()` at the start and an another at the end and subtract them. The problem with this approach is that you can end up with a negative durations. This happens when your machine re-synchronise its clock via the [NTP protocol](https://en.wikipedia.org/wiki/Network_Time_Protocol) and sometimes it can go back in time (1). +{.annotate} + +1. To avoid this problem the solution is to use a monotonic clock (via the `hrtime()` PHP function). + +With Innmind you don't have to worry about that! + +```php +$start = $os->clock()->now(); + +// do some stuff + +$duration = $os + ->clock() + ->now() + ->elapsedSince($start); +``` + +Here `$duration` is an instance of `Innmind\TimeContinuum\ElapsedPeriod` that contains the number of milliseconds between the 2 points in time. And it handles the case that your machine may go back in time. + +??? info + The time shift is handled when working with objects coming from `$clock->now()`, this is not the case when working with objects coming from `$clock->at()`. + +## In the ecosystem + +All packages that depend on this abstraction use this clock, but this abstraction itself also uses this clock. So no matter the level of abstractions you work on you can change the clock implementation in your tests. diff --git a/docs/getting-started/operating-system/filesystem.md b/docs/getting-started/operating-system/filesystem.md new file mode 100644 index 0000000..a7e3e57 --- /dev/null +++ b/docs/getting-started/operating-system/filesystem.md @@ -0,0 +1,266 @@ +# Filesystem + +## Access + +### Concepts + +Files and directories are accessed via `Adapter`s that are _mounted_ through the `$os`. + +A `Directory` is represented by a `Name` and an immutable [`Sequence`](../handling-data/sequence.md) of files and directories. + +A `File` is represented by a `Name`, a `MediaType` and a `Content`. + +A `Content` is either viewed as an immutable `Sequence` of `Line`s or of chunks. This allows to handle human readable files line by line and alter them like any other `Sequence`. And to handle binary files as a `Sequence` of `Str` chunks. + +!!! tip "" + Since a `Content` can be described via a `Sequence`, anytime you see a `Sequence` you have an opportunity to convert it into a `Content`. + +??? note + Even though a `Content` is immutable it loads the content from the filesystem upon use. This means that if a process deletes the file between the time you retrieved the `File` and the time you work with its `Content` your program will fail. + + So be careful of the concurrency in your program! + +Via these immutable structures you can describe your filesystem structures in a [_pure_ code](../../philosophy/oop-fp.md#purity) and apply it later on in your program. + +### Accessing files + +```php +use Innmind\Filesystem\{ + File, + Name, +}; +use Innmind\Url\Path; +use Innmind\Immutable\Predicate\Instance; + +$os + ->filesystem() + ->mount(Path::of('some directory/')) #(1) + ->get(Name::of('some-file.txt')) + ->keep(Instance::of(File::class)) + ->match( + static fn(File $file) => doStuff($file->content()->toString()), + static fn() => fileDoesntExist(), + ); +``` + +1. The path must end with a `/`. + +This reads the content of the file at `some directory/some-file.txt`. But if your file is located under a `sub folder` you would do: + +```php +use Innmind\Filesystem\Directory; + +$os + ->filesystem() + ->mount(Path::of('some directory/')) + ->get(Name::of('sub folder')) + ->keep(Instance::of(Directory::class)) + ->flatMap(static fn(Directory $directory) => $directory->get( + Name::of('some-file.txt'), + )) + ->match( + static fn(File $file) => doStuff($file->content()->toString()), + static fn() => fileDoesntExist(), + ); +``` + +??? note + You can use any level of directory nesting, as long as it's supported by your machine's filesystem. + +If you want to access all the files at the root of the adapter you can do: + +```php +$files = $os + ->filesystem() + ->mount(Path::of('some directory/')) + ->root() + ->all() + ->keep(Instance::of(File::class)); +$files; // instance of Sequence +``` + +### Persisting files + +To add a file at the root of the adapter you can do: + +```php +$os + ->filesystem() + ->mount(Path::of('some directory/')) + ->add(File::named( + 'some name', + Content::ofString('the file content'), + )); +``` + +??? note + If the write fails for any reason it will throw an exception. But since the files and directories are immutable you can retry them safely. + +You can construct the content of a file either via: + +- `Content::ofString()` where the string is the whole file, but beware of memory allocation +- `Content::ofLines()` that expect a `Sequence`, this automatically handles the lines feed character +- `Content::ofChunks()` that expect a `Sequence` +- `Content::none()` to create an empty file + +If you want to create a file inside a directory you can do: + +```php +$os + ->filesystem() + ->mount(Path::of('some directory/')) + ->add( + Directory::named('sub folder')->add( + File::named( + 'some name', + Content::ofString('the file content'), + ), + ), + ); +``` + +!!! note + If the `sub folder/` already exist it will add your file, any other file inside it won't be affected. + +### Removing files + +If you want to remove a file/directory at the root of the adapter you can do: + +```php +$os + ->filesystem() + ->mount(Path::of('some directory/')) + ->remove(Name::of('some file')); +``` + +!!! note + If you delete a directory it will automatically remove all files inside it! + +If the file/directory doesn't exist it will do nothing, since the end result is the same (the absence of the file/directory). + +To remove a file inside a directory you _add a new version of the directory_: + +```php +$os + ->filesystem() + ->mount(Path::of('some directory/')) + ->add( + Directory::named('sub folder')->remove( + Name::of('some file'), + ), + ); +``` + +??? info + Alternatively you can also retrieve the directory, remove the file and re-add the new directory object. However this will be less performant. + +### Modifying file content + +Let's say you have a log file that you want to duplicate but containing only the errors you can do: + +```php +$adapter = $os->filesystem()->mount(Path::of('logs/')); +$adapter + ->get(Name::of('prod.log')) + ->keep(Instance::of(File::class)) + ->map( + static fn(File $file) => $file + ->rename(Name::of('errors.log')) + ->withContent( + $file + ->content() + ->filter( + static fn(Line $line) => $line + ->str() + ->contains('app.ERROR'), + ), + ), + ) + ->match( + static fn(File $file) => $adapter->add($file), + static fn() => null, // prod.log doesn't exist + ); +``` + +You can use `Content::map()` to change each line of a file. `Content::flatMap()` allows to replace one line by multiple ones, you can use this to merge multiple files together. + +??? warning + You can't write to the file you're trying to modify. This means you can't do this: + + ```php + $adapter + ->get(Name::of('some file')) + ->keep(Instance::of(File::class)) + ->map(static fn(File $file) => $file->withContent( + $file + ->content() + ->map(static fn(Line $line) => Line::of(Str::of('some value'))), + )) + ->match( + static fn(File $file) => $adapter->add($file), + static fn() => null, + ); + ``` + + You need to write the modified version to a temporary file, read this file to write it to the original file. But a [feature is planned](https://github.com/Innmind/Filesystem/issues/3) to allow to do in place modification. + +## Watching for changes + +Let's say you have a directory and you want to execute some code every time someone adds a file to it. You can do this: + +```php +use Innmind\FileWatch\Continuation; +use Innmind\Url\Path; + +$watch = $os->filesystem()->watch(Path::of('some directory/')); +$result = $watch( + 0, + static function(int $count, Continuation $continuation) { + if ($count === 42) { + return $continuation->stop($count); + } + + doStuff(); + + return $continuation->continue($count + 1); + }, +); +``` + +Here you'll react to `42` modifications of the directory `some directory/` and then assign `42` to `$result`. In essence this acts as a _reduce_ operation that could be infinite. + +!!! note "" + `0` and `int $count` are a carried value between each call of the function. Here it's an `int` but you can use any type you want. + +??? warning + You should **not** use this method in production as it executes a `stat` command every second. + +## Loading PHP files + +Let's say you have a script that may be configured by an external PHP file. The config file may or may not exist and your script need to adapt to that. + +```php title="config.php" +return [ + 'some' => 'value', + 'key' => 'foo', +]; +``` + +In your script you can do: + +```php +$config = $os + ->filesystem() + ->require(Path::of('config.php')) + ->match( + static fn(array $config) => $config, + static fn() => [ + 'some' => 'default value', + 'key' => 'default value', + ], + ); +``` + +If the file exist then the return value from `config.php` is passed to the first callable passed to `match` otherwise the second callable is called. + +Here the returned value is an `array` but it can be any value. diff --git a/docs/getting-started/operating-system/http.md b/docs/getting-started/operating-system/http.md new file mode 100644 index 0000000..04ad877 --- /dev/null +++ b/docs/getting-started/operating-system/http.md @@ -0,0 +1,193 @@ +# HTTP + +This HTTP client uses the immutable objects describing the protocol from the [`innmind/http` package](https://github.com/Innmind/Http). + +## Usage + +```php +use Innmind\HttpTransport\Success; +use Innmind\Http\{ + Request, + Method, + ProtocolVersion, +}; +use Innmind\Url\Url; + +$http = $os->remote()->http(); + +$request = Request::of( + Url::of('https://github.com/'), + Method::get, + ProtocolVersion::v11, +); +$http($request)->match( + static fn(Success $success) => var_dump( + $success + ->response() + ->body() + ->toString(), + ), + static fn(object $error) => throw new \RuntimeException(), +); +``` + +When sending an HTTP request it will return an `Either`, where each of this classes are located in the `Innmind\HttpTransport\` namespace. This type may be scarry at first but it allows you to use static analysis to deal with every possible situation (or not by throwing an exception like in the example). No more surprises of uncaught exceptions in production! + +??? info + Responses are wrapped in classes such as `Success`, `Redirection`, etc... to avoid confusion as a response can be on both sides of the `Either`. This way you now for sure a `2XX` response is on the right side and the other ones on the left one. + +You can specify headers on your requests like this: + +```php +use Innmind\Http\{ + Headers, + Header\Header, + Header\Value\Value, +}; + +$request = Request::of( + Url::of('https://github.com/'), + Method::get, + ProtocolVersion::v11, + Headers::of( + new Header('User-Agent', new Value('your custom user agent string')), + ), +); +``` + +??? tip + `innmind/http` comes with a lot of header classes to simplify some common cases. + +You can always specify a body like so: + +```php +use Innmind\Filesystem\File\Content; +use Innmind\Http\Header\ContentType; +use Innmind\Json\Json; + +$request = Request::of( + Url::of('https://your-service.com/api/some-endpoint'), + Method::post, + ProtocolVersion::v11, + Headers::of( + ContentType::of('application', 'json'), + ), + Content::ofString(Json::encode(['some' => 'payload'])), #(1) +); +``` + +1. see [`innmind/json`](../../packages.md#json) + +Here we send some json but you can send anything you want. + +The body of a `Request`, and a `Response`, is expressed via the `Content` class from the filesystem abstraction. This means that it can contain any valid file content. + +You'll learn more on this `Content` in the [next chapter](filesystem.md). + +## Following redirections + +By default the client returned by `$os->remote()->http()` doesn't follow redirections. In order to do so you need to decorate the client like this: + +```php +use Innmind\HttpTransport\FollowRedirections; + +$http = FollowRedirections::of($os->remote()->http()); +``` + +This decorator will follow to up to `5` redirections. + +??? info + Redirections are handled this way so you can compose all the decorators the way you need. For example you want to [apply exponential backoff](#retry-with-exponential-backoff) between each redirection. + +## Resiliency + +We tend to think networks are always stable or services as always up, but at some point failures **will** happen. This abstraction comes with 2 strategies to deal with them. + +### Circuit breaker + +If you need to call a service a lot but at some point becomes unavailable (for maintenance for example), you don't want to continue to try to call this service for a certain amount of time. + +The [circuit breaker](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern) is a pattern that will automatically return an error response (without doing the actual call) if the service failed in the previous `x` amount of time. + +You apply this pattern via this decorator: + +```php +use Innmind\HttpTransport\CircuitBreaker; +use Innmind\TimeContinuum\Earth\Period\Second; + +$http = CircuitBreaker::of( + $os->remote()->http(), + $os->clock(), + Second::of(10), +); +``` + +!!! note "" + The circuit breaks on a per domain name logic. + +??? info + In case a circuit is open then the error response will be a `503 Service Unavailable` with a custom header `X-Circuit-Opened` so you can understand who responded. + +### Retry with exponential backoff + +When a call fail you can automatically retry the call after a certain amount of time. You can apply the retries like this: + +```php +use Innmind\HttpTransport\ExponentialBackoff; + +$http = ExponentialBackoff::of( + $os->remote()->http(), + $os->process()->halt(...), +); +``` + +This will retry all errors `5XX` responses and connection failures at most 5 times and will wait `100ms`, `271ms`, `738ms`, `2s` and `5.4s` between each retry. + +??? tip + You can improve the resiliency of the whole operating system abstractions like this: + + ```php + use Innmind\OperatingSystem\OperatingSystem\Resilient; + + $os = Resilient::of($os); + ``` + + Even though for now it only applies this strategy to the HTTP client, you future prood yourself by using this decorator. + +## Traps + +### Unsent requests + +The HTTP client doesn't send the request when you call `$http($request)` to allow for [concurrent calls](../concurrency/http.md). The call is actually done when you call `->match()` on the returned `Either`. + +This means that this code will not send the request: + +```php +$http = $os->remote()->http(); +$http(Request::of( + Url::of('https://your-service.com/api/some-resource'), + Method::delete, + ProtocolVersion::v11, +)); +``` + +Even if you don't care about the response you need to do this: + +```php hl_lines="6-9" +$http = $os->remote()->http(); +$http(Request::of( + Url::of('https://your-service.com/api/some-resource'), + Method::delete, + ProtocolVersion::v11, +))->match( + static fn() => null, + static fn() => null, +); +``` + +### Streaming + +The default client uses `cURL` under the hood and the way it is structured prevents the streaming of requests/responses. + +??? info + However the work of the [distributed abstraction](../concurrency/distributed.md) will require the default client to switch to an implementation based on sockets that will open the door to streaming. diff --git a/docs/getting-started/operating-system/index.md b/docs/getting-started/operating-system/index.md new file mode 100644 index 0000000..266f2ad --- /dev/null +++ b/docs/getting-started/operating-system/index.md @@ -0,0 +1,48 @@ +# Operating System + +This package allows you to deal with all interactions with the operating system in a declarative way. + +## Installation + +```sh +composer require innmind/operating-system:~5.0 +``` + +## Usage + +```php +use Innmind\OperatingSystem\{ + Factory, + OperatingSystem, +}; + +$os = Factory::build(); +$os instanceof OperatingSystem; // returns true +``` + +You'll see in the following chapters all the ways you can use this object. + +!!! info "" + From this point on everytime you see the variable `$os` it refers to this object. + +??? warning + This package is not compatible with Windows. + +## Configuration + +By default the configuration of the `$os` should be fine for all use cases, but you can change some aspects via the `Config` object: + +```php +use Innmind\OperatingSystem\Config; + +$os = Factory::build( + Config::of() + ->disableSSLVerification() + ->caseInsensitiveFilesystem(), +); +``` + +Here we tell the abstraction that we work on a case insensitive filesystem and that the HTTP client should not check the SSL certificates (1). But this class allows more configuration, you should take a look at all its methods. +{.annotate} + +1. You should do this only when working locally. Do **NOT** do this in production. diff --git a/docs/getting-started/operating-system/monitoring.md b/docs/getting-started/operating-system/monitoring.md new file mode 100644 index 0000000..287ca44 --- /dev/null +++ b/docs/getting-started/operating-system/monitoring.md @@ -0,0 +1,107 @@ +# Monitoring + +## Processes + +You can access information on all processes currently running on the machine via: + +```php +use Innmind\Server\Status\Server\Process; + +$os + ->status() + ->processes() + ->all() + ->foreach(static function(Process $process): void { + \printf( + '%s is running %s', + $process->user()->toString(), + $process->command()->toString(), + ); + }); +``` + +On each process you have access to: + +- its pid +- the user that started it +- the current cpu percentage +- the current amount of memory used +- when it started (may not always be available) +- the command + +The cpu and memory usage is a snapshot of when you called `->all()`. If you want an updated value you need to refetch the process via: + +```php +$updatedProcess = $os + ->status() + ->processes() + ->get($process->pid()); +``` + +??? info + Since the process may have finished in the meantime `->get()` returns a `Maybe`. + +## Disk + +You can access all the mounted volumes via: + +```php +use Innmind\Server\Status\Server\Disk\Volume; + +$os + ->status() + ->disk() + ->volumes() + ->foreach(static function(Volume $volume): void { + \printf( + '%s uses %s', + $volume->mountPoint()->toString(), + $volume->used()->toString(), + ); + }); +``` + +On each volume you have access to its mount point and its usage. The values are a snapshot of when you called `->volumes()`, if you want an updated value you need to refetch the volumes. + +## CPU + +```php +$cpu = $os + ->status() + ->cpu(); +``` + +On `$cpu` you have access to a snapshot of the percentage of cpu used by the user or the system. You also have access to the number of cores available, you can use this information to adapt your program if you want to start child processes. + +## Memory + +```php +$memory = $os + ->status() + ->memory(); +``` + +On `$memory` you have access to a snapshot of the memory used, the swap used and the total memory available. + +## Load average + +```php +$load = $os + ->status() + ->loadAverage(); +$load->lastMinute(); +$load->lastFiveMinutes(); +$load->lastFifteenMinutes(); +``` + +You can use this load average to know if you can handle more work in your program or start throttling. + +## Temporary directory + +```php +$tmp = $os + ->status() + ->tmp(); +``` + +`$tmp` is a `Innmind\Url\Path` that you can use to [mount a filesystem](filesystem.md) or use as a working directory when [launching processes](processes.md). diff --git a/docs/getting-started/operating-system/network.md b/docs/getting-started/operating-system/network.md new file mode 100644 index 0000000..84d9a95 --- /dev/null +++ b/docs/getting-started/operating-system/network.md @@ -0,0 +1,182 @@ +# Network + +## Unix socket + +If you look to communicate between processes you should head to the [IPC chapter](../concurrency/ipc.md). + +### Server + +The first part to build a socket server is to accept incoming connections at an address: + +```php +use Innmind\IO\Sockets\Server; +use Innmind\Socket\Address\Unix; +use Innmind\Immutable\Sequence; + +$server = $os + ->sockets() + ->open(Unix::of('/tmp/some-socket-name')) + ->match( + static fn(Server $server) => $server, + static fn() => throw new \RuntimeException('Failed to open socket'), + ); +$clients = Sequence::of(); + +while (true) { + $clients = $server + ->watch() + ->accept() + ->toSequence() + ->append($clients); +} +``` + +This will wait forever for a connection to open, when it does it's added to `$clients` and then resume watching for new connections. + +The next step is to define a protocol. Let's take a silly example where when connecting a client must send `hello world\n` followed by their message ending with `\n`. You can define such protocol like this: + +```php +use Innmind\IO\Readable\Frame\{ + Chunk, + Line, +}; +use Innmind\Immutable\Str; + +$protocol = Chunk::of(12) + ->flatMap(static fn(Str $hello) => Line::new()) + ->map(Str $message) => $message->rightTrim("\n")); +``` + +`Chunk::of(12)` expresses the expected hello world string. `flatMap` expresses what to do next with the read value, in this case we tell that we want a line ending with `\n`. `map` transform the line read previously to remove the `\n` at the end since it's not part of the message. + +??? info + You can explore the `Innmind\IO\Readable\Frame\` namespace to see the other kind of frames you can use. And you can also define your own. + + You can look at [`innmind/http-parser`](https://github.com/Innmind/http-parser) or [`innmind/amqp`](https://github.com/Innmind/AMQP) for concrete examples of protocols defined this way. + +??? tip + Here the `map` only modifies the message but you can change the value to any type you wish. It's even encouraged to encapsulate the data in your own classes to make sure it's the format you expect. + +You can use then the procol like this: + +```php hl_lines="11-12" +use Innmind\IO\Sockets\Client; +use Innmind\Immutable\Str; + +$server + ->watch() + ->accept() + ->flatMap( + static fn(Client $client) => $client + ->toEncoding(Str\Encoding::ascii) + ->watch() + ->frames($protocol) + ->one(), + ) + ->match( + static fn(Str $message) => $message, + static fn() => null, // either no connection or failed to read the message + ); +``` + +Sending data to the incoming connection is the same way as from the client side ([see below](#client)). + +### Client + +To connect to a server you can do: + +```php +use Innmind\IO\Sockets\Client; +use Innmind\Socket\Address\Unix; + +$client = $os + ->sockets() + ->connectTo(Unix::of('/tmp/some-socket-name')) + ->match( + static fn(Client $client) => $client, + static fn() => throw new \RuntimeException('Failed to connect'), + ); +``` + +Then to send data: + +```php +use Innmind\Immutable\{ + Str, + Sequence, +}; + +$client + ->toEncoding(Str\Encoding::ascii) + ->send(Sequence::of( + Str::of("hello world\nThis is your message\n"), + )) + ->match( + static fn() => null, // message sent + static fn() => throw new \RuntimeException('Failed to send'), + ); +``` + +??? info + As you can see `send` expect a `Sequence` of messages meaning you can send multiple ones. This is so you don't have to loop yourself. + + In case you use a lazy sequence and you want to abort midway (say because a [signal tells you to stop](php-process.md#handling-cli-signals)), you can do it like this: + + ```php + $signaled = false; + $client + ->abortWhen(static function() use (&$signaled) { + return $signaled; + }) + ->send($messages) + ->match( + static fn() => null, // message sent + static fn() => throw new \RuntimeException('Failed to send'), + ); + ``` + + If the sending is aborted then it will always reach the error case, here meaning it will throw the exception. + +If you want to read data coming from the server you'd do it the same way the server does ([see above](#server)). + +## Over the wire + +This works exactly the same way as unix sockets except for the method to open the server and the method to connect to it: + +=== "Open server" + ```php + use Innmind\IO\Sockets\Server; + use Innmind\Socket\Internet\Transport; + use Innmind\IP\IP; + use Innmind\Url\Authority\Port; + + $server = $os + ->ports() + ->open( + Transport::tcp(), + IP::v4('0.0.0.0'), + Port::of(8080), + ) + ->match( + static fn(Server $server) => $server, + static fn() => throw new \RuntimeException(), + ); + ``` + +=== "Open connection" + ```php + use Innmind\IO\Sockets\Client; + use Innmind\Socket\Internet\Transport; + use Innmind\Url\Url; + + $client = $os + ->remote() + ->socket( + Transport::tcp(), + Url::of('tcp://machine-ip:8080/')->authority(), + ) + ->match( + static fn(Client $client) => $client, + static fn() => throw new \RuntimeException(), + ); + ``` diff --git a/docs/getting-started/operating-system/php-process.md b/docs/getting-started/operating-system/php-process.md new file mode 100644 index 0000000..40c848d --- /dev/null +++ b/docs/getting-started/operating-system/php-process.md @@ -0,0 +1,82 @@ +# PHP Process + +## Pausing + +If you need to pause your program to wait for external thing to happen (or any other reason), you can pause it this way: + +```php +use Innmind\TimeContinuum\Earth\Period\Second; + +$os + ->process() + ->halt(Second::of(10)); +``` + +You can use any unit of period except months because it's not a absolute value. + +??? info + If you want to wait for years it will compute that as `365` days. But if you need to do this there may be a design problem in your program. + +## Handling CLI Signals + +Any process can receive signals to tell them a user (or the system) wants to shut them down allowing the process to terminate gracefully (1). +{.annotate} + +1. This is the prevalent usage, but [there are more](https://en.wikipedia.org/wiki/Signal_(IPC)). + +For example let's say you need to import a large csv file into a database but you want to be able to stop it gracefully. You can do: + +```php hl_lines="13 15 18-21 31-33" +use Innmind\Signals\Signal; +use Innmind\Filesystem\{ + File, + File\Content\Line, + Name, +}; +use Innmind\Url\Path; +use Innmind\Immutable\{ + Sequence, + Predicate\Instance, +}; + +$signaled = false; +$stop = static function() use (&$signaled): void { + $signaled = true; +}; + +$os + ->process() + ->signals() + ->listen(Signal::interrupt, $stop); + +$os + ->filesystem() + ->mount(Path::of('data/')) + ->get(Name::of('users.csv')) + ->keep(Instance::of(File::class)) + ->map(static fn(File $file) => $file->content()->lines()) + ->toSequence() #(1) + ->flatMap(static fn(Sequence $lines) => $lines) + ->takeWhile(static function() use (&$signaled) { + return !$signaled; + }) + ->foreach(static fn(Line $line) => importToDb($line)); + +$os + ->process() + ->signals() + ->remove($stop); +``` + +1. `Maybe->toSequence()->flatMap(fn($sequence) => $sequence)` is a way to _swallow_ the fact that the file may not exist. + +This way if a user sends a signal to interrupt the script, it will: + +- pause the execution +- call the `$stop` function +- modify the `$signaled` flag +- resume the execution +- the next line that is attempted to be read won't be done because of `takeWhile` + +??? tip + You can specify multiple listeners for a single signal and they'll be executed in the order you added them. diff --git a/docs/getting-started/operating-system/processes.md b/docs/getting-started/operating-system/processes.md new file mode 100644 index 0000000..fba3b65 --- /dev/null +++ b/docs/getting-started/operating-system/processes.md @@ -0,0 +1,130 @@ +# Launching Processes + +## Usage + +```php +use Innmind\Server\Control\Server\{ + Command, + Process\Success, + Process\TimedOut, + Process\Failed, + Process\Signaled, +}; + +$process = $os + ->control() + ->processes() + ->execute( + Command::foreground('apt-get') + ->withArgument('install') + ->withArgument('cowsay') + ->withShortOption('y'), + ); +$process + ->wait() + ->match( + static fn(Success $success) => doStuff(), + static fn(TimedOut|Failed|Signaled $error) => throw new RuntimeException(); + ); +``` + +This example waits for the installation of [`cowsay`](https://en.wikipedia.org/wiki/Cowsay) before continuing via `doStuff()` or it will fail with an exception. + +??? note + By default the process is executed with no environment variables. If you try to execute a command that is reachable only because you modified your `$PATH` environment variable, you'll need to specify it via `Command::foreground('command')->withEnvironment('$PATH', 'your path value')`. + + This may seem restrictive at first but it's done to force your program to be [explicit](../../philosophy/explicit.md). And it will help other developers to understand what's needed for the command to be run. + +??? info + If you don't want to wait for a process to finish you can replace `#!php Command::foreground()` by `#!php Command::background()` and remove the code `#!php $process->wait()`. + +If you dont't really care about the process failing or not and simply want to _forward_ its output you can use: + +```php +use Innmind\Server\Control\Server\Process\Output\Type; +use Innmind\Immutable\Str; + +$process + ->output() + ->foreach(static function(Str $chunk, Type $type): void { + // $type is either Type::output or Type::error + echo $chunk->toString(); + }); +``` + +This code will print the output of the underlying process in real time. The `foreach` call will return when the process is finished. + +??? tip + If you still need to check the result of the process you can still call `#!php $process->wait()`, it will immediately return the result. + +You can also send content to the `STDIN` of the process via: + +```php +use Innmind\Filesystem\File\Content; + +echo $os + ->control() + ->processes() + ->execute( + Command::foreground('echo') + ->withInput(Content::ofString('some input')), + ) + ->output() + ->toString(); +``` + +The input can be [any valid `Content`](filesystem.md) object, even lazy ones. + +## Streaming + +By default the process output is kept in memory so you can use it multiple times. However for some commands the output can be quite large and it won't fit in memory. + +For example you want to run an archive command that you want to stream to the output. + +```php hl_lines="10" +$os + ->control() + ->processes() + ->execute( + Command::foreground('zip') + ->withShortOption('q') + ->withShortOption('r') + ->withArgument('-') + ->withArgument('some folder/') + ->streamOutput(), + ) + ->output() + ->foreach(static function(Str $chunk) { + echo $chunk->toString(); + }); +``` + +!!! note "" + You won't be able to reuse the output twice, if you try it will throw a `\LogicException`. + +??? info + However if you've walked over the whole output you can still call `#!php $process->wait()` to check if there was an error or not. + +## SSH + +You can execute commands on a remote machine through SSH the same way you'd do it on the local machine via: + +```php hl_lines="4-5" +use Innmind\Url\Url; + +$process = $os + ->remote() + ->ssh(Url::of('ssh://user@machine-name-or-ip:22/')) + ->processes() + ->execute( + Command::foreground('apt-get') + ->withArgument('install') + ->withArgument('cowsay'), + ); +``` + +!!! note "" + You can't specify the password to connect to the machine via the url. It's done to force you to use SSH keys. + +??? info + For now it's not possible to use an input when running commands through SSH. diff --git a/docs/getting-started/operating-system/sql.md b/docs/getting-started/operating-system/sql.md new file mode 100644 index 0000000..1b6149f --- /dev/null +++ b/docs/getting-started/operating-system/sql.md @@ -0,0 +1,204 @@ +# SQL + +The SQL client is structured by 2 concepts: immutable `Query` objects as input and [`Sequence`s](../handling-data/sequence.md) of immutable `Row`s as output. + +## Usage + +```php +use Formal\AccessLayer\{ + Query\SQL, + Row, +}; +use Innmind\Url\Url; + +$sql = $os + ->remote() + ->sql(Url::of('mysql://user:password@127.0.0.1:3306/database_name')); + +$sql(SQL::of('SELECT * FROM users'))->foreach( + static fn(Row $row) => var_dump($row->toArray()), +); +``` + +## Prepared queries + +If you need to inject data in your queries you should use parameters. + +!!! warning "" + Do **not** use string concatenation as it can lead to [SQL injection](https://en.wikipedia.org/wiki/SQL_injection). + +```php +use Formal\AccessLayer\Query\Parameter; + +$query = SQL::of('INSERT INTO users VALUES (:id, :username)') + ->with(Parameter::named('id', 'some-id')) + ->with(Parameter::named('username', 'some-username')); + +$sql($query); +``` + +Here named parameters are used via the format `:parameter_name` with the named specified again in `#!php Paramater::named()`. + +You can also bind parameters by indices via the format `?` and then `#!php Parameter::of('value')`. This way you don't duplicate strings, but the order you add the parameters via the `with` method matters. + +## Query builder + +The `SQL` class allows you to specify the exact query you want to execute. But if you want to generate queries programmatically you should use the other classes from the `Formal\AccessLayer\Query\` namespace, such as `Select` or `Insert`. + +=== "Select" + ```php + use Formal\AccessLayer\{ + Query\Select, + Table\Name, + Table\Column, + }; + + $select = Select::from(Name::of('users'))->columns( #(1) + Column\Name::of('id'), + Column\Name::of('username'), + ); + + $sql($select); + ``` + + 1. If you don't specify the columns it will retrieve them all by default. + +=== "Insert" + ```php + use Formal\AccessLayer\{ + Query\Insert, + Row, + }; + + $insert = Insert::into( + Name::of('users'), + Row::of([ + 'id' => 'id-1', + 'username' => 'john', + ]), + Row::of([ + 'id' => 'id-2', + 'username' => 'jane', + ]), + // etc... + ); + + $sql($insert); + ``` + +=== "etc..." + Other query builders include: + + - `CreateTable` + - `DropTable` + - `Delete` + - `Update` + +## Filtering + +When selecting from a table you can restrict the rows by specifying them manually in the `SQL` class. But if you need to programmatically construct the _where_ clause you can use the [specification pattern](https://en.wikipedia.org/wiki/Specification_pattern) via the `Select` class. + +For example let's say you want to retrieve all users whose username starts with `a`. The first step is to create a specification: + +```php +use Innmind\Specification\{ + Comparator, + Composable, + Sign, +}; + +/** @psalm-immutable */ +final class Username implements Comparator +{ + use Composable; + + private string $value; + + private function __construct(string $value) + { + $this->value = $value; + } + + /** @psalm-pure */ + public static function startsWith(string $value): self + { + return new self($value); + } + + public function property(): string + { + return 'username'; #(1) + } + + public function sign(): Sign + { + return Sign::startsWith; + } + + public function value(): string + { + return $this->value; + } +} +``` + +1. This is the column name. + +And then you use it like this: + +```php +$select = Select::from(Name::of('users')) + ->where(Username::startsWith('a')); + +$sql($select); +``` + +!!! success "" + The big advantage of specifications is that you can easily compose them. For example if you want users starting with `a` or `b` you'd do `#!php Username::startsWith('a')->or(Username::startsWith('b'))`; and if you want all except these ones you can chain a `->not()` to negate the whole condition. + + Another advantage is that this composition forces you to think about precedence of your conditions to reduce the risk of implicit behaviours. + +## Laziness + +All the queries you've seen so far return [deferred `Sequence`s](../handling-data/sequence.md#deferred) meaning that the queries are executed immediately but the returned rows will be loaded (and kept) in memory when you use the returned sequence. + +For most queries this is fine. But if you want to select a large amount of data that may not fit in memory you should use lazy queries. + +To do so instead of using `#!php SQL::of()`/`#!php Select::from()` use `#!php SQL::onDemand()`/`#!php Select::onDemand()`. + +```php +$select = Select::onDemand(Name::of('users')); + +$sql($select)->foreach( + static fn(Row $row) => doStuff($row), +); +``` + +With this even if the result contains a million rows there'll only be one at a time in memory. + +??? info + However this means that if you call `foreach` twice it will run the query twice. The returned rows may change between the 2 calls, if you need the results to be the same you can't use lazy queries! + +## Transactions + +To run queries inside a transaction you need to run the corresponding sql queries like this: + +```php +use Formal\AccessLayer\Query\{ + StartTransaction, + Commit, + Rollback, +}; + +try { + $sql(new StartTransaction); + + // run your queries here + + $sql(new Commit); +} catch (\Throwable $e) { + $sql(new Rollback); + + throw $e; +} +``` diff --git a/docs/getting-started/orm/development.md b/docs/getting-started/orm/development.md new file mode 100644 index 0000000..ebcb8e2 --- /dev/null +++ b/docs/getting-started/orm/development.md @@ -0,0 +1,410 @@ +# Development + +## Setup + +For this chapter we'll work with a `User` class that can have multiple `Address` objects. + +To keep things simple we'll work with an in memory persistence. You'll learn how to really persist them in the [next chapter](production.md). + +```php +use Formal\ORM\Manager; +use Innmind\Filesystem\Adapter\InMemory; + +$orm = Manager::filesystem(InMemory::emulateFilesystem()); #(1) +``` + +1. From this point on every time you see `$orm` it will come from this example. + +=== "`User`" + ```php + use Formal\ORM\{ + Id, + Definition\Contains, + }; + use Innmind\Immutable\Set; + + final readonly class User + { + /** + * @param Id $id + * @param Set
$addresses + */ + private function __construct( + private Id $id, + private string $username, + #[Contains(Address::class)] #(1) + private Set $addresses, + ) {} + + public static function new(string $username): self + { + return new self( + Id::new(self::class), + $username, + Set::of(), + ); + } + + public function addAdress(Address $address): self + { + return new self( + $this->id, + $this->username, + $this->addresses->add($address), + ); + } + } + ``` + + 1. This allows the ORM to know how to persist the data inside the collection. + + A `User` is called an aggregate in this ORM. This is the root object that have ownership of every data inside it (more on that below). + + An aggregate must have an `Id $id` property. All the other properties will be automatically stored if the ORM understands the type defined on the property. + + ??? info + A `Set` is an immutable unsorted collection of unique objects. + +=== "`Address`" + ```php + final readonly class Address + { + public function __construct( + private string $street, + private string $zipcode, + private string $city, + ) {} + } + ``` + + An `Address` is called an entity in this ORM. As you can see it doesn't have any id. The ORM knows an object of this class belongs to a given user because it is found inside its `$addresses` property. + + Of course nothing prevents you to add your own id to an entity, but the ORM will treat it as any other property. + +## Persisting a new aggregate + +```php +use Innmind\Immutable\Either; + +$repository = $orm->repository(User::class); +$orm->transactional( + static function() use ($repository) { + $repository->put(User::new('john')); + $repository->put(User::new('jane')); + + return Either::right(null); + }, +); +``` + +In order to persist aggregates you need to first access their repository. You can think of this `$repository` as a persistent collection of all your objects for a given class. + +To modify (1) any data it has to be done in a transaction via `$orm->transactional()`. This is done to make sure your program is structurally correct. If you try to modify the data outside it will throw an exception, this prevents unforeseen modifications outside of the context you expect. Note that this applies to calls on a repository methods, not the aggregate objects. +{.annotate} + +1. `#!php $repository->put()` or `#!php $repository->remove()` + +The function passed to `transactional` has to return an [`Either`](../handling-data/either.md). If it contains a value on the _right_ side then it will commit the transaction and if it contains a value on the _left_ side (or throws an exception) it will rollback the transaction. The `transactional` method will return the `Either` as you'd expect. + +In our case we return `null` on the right side as we don't have any business logic that can fail. + +Let's say now that we want to create 2 users that live in the same city: + +```php +$address = new Address('somewhere', '75001', 'Paris'); +$john = User::new('john')->addAddress($address); +$jane = User::new('jane')->addAddress($address); + +$repository = $orm->repository(User::class); +$orm->transactional( + static function() use ($repository, $john, $jane) { + $repository->put($john); + $repository->put($jane); + + return Either::right(null); + }, +); +``` + +Even though we use the same `Address` object for both users the address will be stored twice. This is possible because the `Address` is an immutable object that represents data, the object _reference_ has no relevance for the ORM. + +!!! success "" + This design as a HUGE benefit: you can't mess up your objects relations. + +## Retrieving an aggregate + +Once you persisted an aggregate you'll need to retrieve it, which is pretty straight forward: + +```php +$repository + ->get(Id::of(User::class, 'some-uuid')) + ->match( + static fn(User $user) => doStuff($user), + static fn() => userDoesntExist(), + ); +``` + +You should replace `'some-uuid'` with the string representation of and id (via the `toString` method). + +Since the user you're asking for may not exist in the storage, the repository returns a [`Maybe`](../handling-data/maybe.md) so you're forced to handle both cases. + +## Modifying an aggregate + +To modify an aggregate you need to _re-add_ it to the repository since the objects are immutable. + +```php +$orm->transactional( + static function() use ($repository) { + $_ = $repository + ->get(Id::of(User::class, 'some-uuid')) + ->map(static fn(User $user) => $user->addAddress( + new Address('somewhere', 'SW9 9SL', 'London'), + )) + ->match( + static fn(User $user) => $repository->put($user), + static fn() => null, + ); + + return Either::right(null); + }, +); +``` + +The benefit here is that you can't persist data by accident. All modifications to the persistence are [explicit](../../philosophy/explicit.md). + +## Deleting an aggregate + +```php +$orm->transactional( + static function() use ($repository) { + $repository->remove(Id::of(User::class, 'some-uuid')); + + return Either::right(null); + }, +); +``` + +Whether any aggregate with this id existed or not it will return nothing and won't throw an exception. + +## Retrieving a collection of aggregates + +The simplest way is to retrieve all aggregates: + +```php +$repository + ->all() + ->foreach(static fn(User $user) => doStuff($user)); +``` + +Even if you have thousands of aggregates in your storage this code will work because the ORM keeps track of an aggregate as long as _you_ keep it in memory. + +Usually you won't want to retrieve all aggregates, you need only a subset. You could use `#!php $repository->all()->filter()` but this is fairly innefficient as it retrieve all aggregates and throw out the ones you don't use. + +The best approach is to filter directly at the storage level. You do this via the [specification pattern](../operating-system/sql.md#filtering). + +Let's say we want all users with an address in `London`. First we need to build a specification: + +```php +use Innmind\Specification\{ + Comparator, + Composable, + Sign, +}; + +/** @psalm-immutable */ +final class City implements Comparator +{ + use Composable; + + private function __construct( + private string $city, + ) {} + + /** @psalm-pure */ + public static function of(string $city): self + { + return new self($city); + } + + public function property(): string + { + return 'city'; #(1) + } + + public function sign(): Sign + { + return Sign::equality; + } + + public function value(): string #(2) + { + return $this->city; + } +} +``` + +1. This is the name of the property in the `Address` class. +2. This return type has to be the same as the one on the property. + +And you use it like this: + +```php +use Formal\ORM\Specification\Child; + +$repository + ->matching(Child::of( + 'addresses', #(1) + City::of('London'), + )) + ->foreach(static fn(User $user) => doStuff($user)); +``` + +1. This is the property name on the `User` class. + +And if you want to target `London` or `Paris` you can do `#!php City::of('London')->or(City::of('Paris'))`. + +!!! success "" + This is the same approach as the [pure SQL one](../operating-system/sql.md#filtering). So you can more easily upgrade from one to the other. + +You can of course also limit the number of aggregates to retrieve via `#!php $repository->matching($specification)->drop($x)->take($y)`. + +## Custom types + +So far you've only seen how to persist `string` properties. But you can use your own types. + +For example let's you want to create a `Username` class to prevent using empty usernames. + +```php +final readonly class Username +{ + private string $value; + + public function __construct(string $value) + { + if ($value === '') { + throw new \DomainException; + } + + $this->value = $value; + } + + public function toString(): string + { + return $this->value; + } +} +``` + +Now you need to update the aggregate: + +```php hl_lines="15 20" +use Formal\ORM\{ + Id, + Definition\Contains, +}; +use Innmind\Immutable\Set; + +final readonly class User +{ + /** + * @param Id $id + * @param Set
$addresses + */ + private function __construct( + private Id $id, + private Username $username, + #[Contains(Address::class)] + private Set $addresses, + ) {} + + public static function new(Username $username): self + { + return new self( + Id::new(self::class), + $username, + Set::of(), + ); + } + + public function addAdress(Address $address): self + { + return new self( + $this->id, + $this->username, + $this->addresses->add($address), + ); + } +} +``` + +The last part is to tell the ORM how to convert this type. You need to create a class implementing the `Type` interface. + +```php +use Formal\ORM\Definition\{ + Type, + Types, +}; +use Innmind\Type\{ + Type as Concrete, + ClassName, +}; +use Innmind\Immutable\Maybe; + +/** + * @psalm-immutable + * @implements Type + */ +final class UsernameType implements Type +{ + /** + * @psalm-pure + * + * @return Maybe + */ + public static function of(Types $types, Concrete $type): Maybe + { + return Maybe::just($type) + ->filter(static fn($type) => $type->accepts( + ClassName::of(Username::class) #(1) + )) + ->map(static fn() => new self); + } + + public function normalize(mixed $value): null|string|int|bool + { + return $value->toString(); + } + + public function denormalize(null|string|int|bool $value): mixed + { + if (!\is_string($value)) { + throw new \LogicException; + } + + return new Username($value); + } +} +``` + +1. This is what tells the ORM the type this class supports converting. + +??? tip + You don't need to handle the `null` value in your type, the ORM already does that for you. + +And you register this class when creating the ORM: + +```php hl_lines="10-12" +use Formal\ORM\{ + Manager, + Definition\Aggregates, + Definition\Types, +}; +use Innmind\Filesystem\Adapter\InMemory; + +$orm = Manager::filesystem( + InMemory::emulateFilesystem(), + Aggregates::of(Types::of( + Username::of(...), + )), +); +``` diff --git a/docs/getting-started/orm/index.md b/docs/getting-started/orm/index.md new file mode 100644 index 0000000..86c2ce0 --- /dev/null +++ b/docs/getting-started/orm/index.md @@ -0,0 +1,48 @@ +# ORM + +This ORM focuses to simplify data manipulation. + +!!! success "" + It can handle any amount of data by being memory safe and reduces the complexity of data lifecycle by using immutable objects. + +??? info + Its monadic design allows it to be compatible with [Innmind's asynchronous context](../concurrency/async.md). + +## Installation + +```sh +composer require formal/orm:~2.0 +``` + +## Example + +```php +use Formal\ORM\{ + Manager, + Sort, +}; +use Innmind\Url\Url; + +$manager = Manager::sql( + $os + ->remote() + ->sql(Url::of('mysql://user:pwd@host:3306/database?charset=utf8mb4')), +); +$_ = $manager + ->repository(YourAggregate::class) + ->all() + ->sort('someProperty', Sort::asc) + ->drop(150) + ->take(50) + ->foreach(static fn(YourAggregate $aggregate) => doStuff($aggregate)); +``` + +## Tips + +Since it focuses on usage and not _abstracting a persistence model_ this ORM allows 3 different persistence models: + +- SQL +- Filesystem +- Elasticsearch + +*[ORM]: Object Relational Mapping diff --git a/docs/getting-started/orm/production.md b/docs/getting-started/orm/production.md new file mode 100644 index 0000000..9f6d1b7 --- /dev/null +++ b/docs/getting-started/orm/production.md @@ -0,0 +1,168 @@ +# Production + +## Choose the right storage + +Now that you know how to use the main features of this ORM, it's time to really persist the data. + +As said in the introduction you have 3 options: + +- SQL +- Elasticsearch +- Filesystem + +If you need a reliable storage you should use SQL as it's battle proven. + +If you're trying to build a proof of concept then it's probable not necessary to use any third party storage and go with the filesystem. + +If you need efficiency when searching for your aggregates then you should go with Elasticsearch. + +!!! success "" + The 3 storages are tested against the same properties. This means that the behaviour between all of them will be the same. So you can switch between them. + +## SQL + +### Setup + +```php +use Formal\ORM\Manager; +use Innmind\Url\Url; + +$connection = $os + ->remote() + ->sql(Url::of('mysql://user:password@127.0.0.1:3306/database')); +$orm = Manager::sql($connection); +``` + +The rest of your code doesn't have to change. + +### Creating the tables + +In order to persist your data you first need to create the tables where they'll be stored. + +```php +$aggregates = Aggregates::of(Types::default()); #(1) +$show = ShowCreateTable::of($aggregates); + +$_ = $show(User::class)->foreach($connection); #(2) +``` + +1. Don't forget to also declare your own types here. +2. You don't need to specify the entities here, only the aggregates class. + +This code automatically execute the queries to create the tables. You could instead print them (1) and store them in a database migration tool. +{.annotate} + +1. `#!php $_ = $show(User::class)->foreach(var_dump(...));` + +??? warning + Unfortunately Innmind doesn't have a migration package yet. For now you could use [`doctrine/migrations`](https://packagist.org/packages/doctrine/migrations). + +## Elasticsearch + +You first need to run an Elasticsearch instance, head to their [documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html) to learn how to start one. + +Then change the adapter of the manager: + +```php +use Formal\ORM\Adapter\Elasticsearch; +use Innmind\Url\Url; + +$orm = Manager::of( + Elasticsearch::of( + $os->remote()->http(), + Url::of('http://localhost:9200/'), #(1) + ), +); +``` + +1. If you use a local instance you can omit this parameter. + +??? tip + If you want to run tests against a real instance of Elasticsearch you should decorate the HTTP client like this: + + ```php + use Formal\ORM\Adapter\Elasticsearch\Refresh; + + Elasticsearch::of(Refresh::of( + $os->remote()->http(), + )); + ``` + + This decorator makes sure each modification to the index are applied instantaneously. **DO NOT** use this decorator in production as it will overload your instance. + +Finally you need to create the index: + +```php +use Formal\ORM\{ + Definition\Aggregates, + Definition\Types, + Adapter\Elasticsearch\CreateIndex, +}; +use Innmind\Url\Url; + +$aggregates = Aggregates::of(Types::default()); #(1) +$createIndex = CreateIndex::of( + $os->remote()->http(), + $aggregates, + Url::of('http://localhost:9200/'), +); + +$_ = $createIndex(User::class)->match( + static fn() => null, + static fn() => throw new \RuntimeException('Failed to create index'), +); +``` + +1. Don't forget to also declare your own types here. + +??? warning + Unlike other storages Elasticsearch doesn't support transactions. + + Elasticsearch also doesn't allow to list more than 10k aggregates, this means that if you store more than that you won't be able to list them all in a single `Sequence`. You'll need to use explicit search queries to find them all back. + +## Filesystem + +### Local + +This is the best storage when starting to develop a new program as there's no schema to update. This allows for rapid prototyping. + +```php +use Innmind\Url\Path; + +$orm = Manager::filesystem( + $os + ->filesystem() + ->mount(Path::of('path/where/to/store/data')), +); +``` + +And... that's it. + +### S3 + +You should this storage for small programs without much concurrency that you need to synchronise for multiple clients. A good example is a CLI program that you want to work across multiple machines. + +First you need to require the S3 package: + +```sh +composer require innmind/s3:~4.1 +``` + +Then configure the ORM: + +```php +use Innmind\S3\{ + Factory, + Region, + Filesystem, +}; +use Innmind\Url\Url; + +$bucket = Factory::of($os)->build( + Url::of('https://acces_key:acces_secret@bucket-name.s3.region-name.scw.cloud/'), + Region::of('region-name'), +); +$orm = Manager::filesystem( + Filesystem\Adapter::of($bucket), +); +``` diff --git a/docs/getting-started/orm/testing.md b/docs/getting-started/orm/testing.md new file mode 100644 index 0000000..cef62f2 --- /dev/null +++ b/docs/getting-started/orm/testing.md @@ -0,0 +1,64 @@ +# Testing + +## Guarantees + +When it comes to testing your program a question arises: should you use the same kind of database than in production or use a faster implementation to speed up the test suite. + +When using ORMs that let the SQL bleed through their APIs this question becomes tricky because you may not end up having the same behaviour between your tests and in production. + +!!! success "" + This ORM doesn't have such problem. It uses [Property Based Testing](../../testing/property-based-testing.md) to make sure all storage implementations behave the same way. + +This means that you can safely use a faster storage for your tests and it will behave the same way as in production. + +## Setup + +### Filesystem + +You should use an in memory filesystem for your tests as it's the fastest since it never writes to the actual filesystem. And since the data is isolated to the process, you could run your tests in parallel thus speeding up even more your test suite. + +```php +use Formal\ORM\Manager; +use Innmind\Filesystem\Adapter\InMemory; + +$adapter = InMemory::emulateFilesystem(); +$orm = Manager::filesystem($adapter); +``` + +Your aggregates will be kept in memory as long as there is a reference to `$adapter`. This means that if your test looks something like this it won't work: + +```php +$orm = Manager::filesystem(InMemory::emulateFilesystem()); + +// do some work that creates aggregates + +$orm = Manager::filesystem(InMemory::emulateFilesystem()); + +// run expectations on your aggregates +``` + +The second instanciation of `$orm` will free the first one from memory and your aggregates will disappear. + +### Elasticsearch + +In case you want test a concrete instance of Elasticsearch to replicate the exact behaviour as in production, you should change one line when creating the orm: + +```php hl_lines="9" +use Formal\ORM\{ + Manager, + Adapter\Elasticsearch, + Adapter\Elasticsearch\Refresh, +}; + +$orm = Manager::of( + Elasticsearch::of( + Refresh::of( + $os->remote()->http(), + ), + ), +); +``` + +This decorator will make sure that every modification to an index is applied immediately. The default behaviour of Elasticsearch is that it will put the modification in an internal queue and there's at least a 1 second delay before seeing the change in the index. This is fine in production but it's difficult to do some assertions in a test. + +If you need to assert you can fetch an aggregate after persisting it, then this decorator is for you. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..367a688 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,56 @@ +--- +hide: + - navigation + - toc +--- + +# Welcome to Innmind + +Innmind bridges Object Oriented Programming and Functional Programming in a coherent ecosystem to bring high level abstraction to life. + +This documentation will show you how to move from simple scripts all the way to distributed systems (and all the steps in-between) by using a single way to code. + +If you've seen modern Java, C#, Rust, Swift and co you should find Innmind very familiar. + +??? example "Sneak peek" + The code below shows how the declarative nature of Innmind abstracts away the complexity. + + ```php + $os + ->filesystem() + ->mount(Path::of('somewhere/data/')) + ->get(Name::of('avatars')) + ->keep(Instance::of(Directory::class)) + ->map( + static fn(Directory $directory) => $directory->add(File::named( + 'users.csv', + Content::ofLines( + $orm + ->repository(User::class) + ->all() + ->map(static fn(User $user) => $user->toArray()) + ->map(static fn(array $user) => \implode(',', $user)) + ->map(Str::of(...)) + ->map(Line::of(...)), + ), + )), + ) + ->map(Tar::encode($os->clock())) + ->map(Gzip::encode()) + ->match( + static fn(File $tar) => Response::of( + StatusCode::ok, + ProtocolVersion::v11, + null, + $tar->content(), + ), + static fn() => Response::of( + StatusCode::noContent, + ProtocolVersion::v11, + ), + ); + ``` + + This example sends an HTTP response of a `.tar.gz` containing all files contained in an `avatars` directory and with a CSV of all users stored in a database. All this is done with the guarantee that you won't run in "out of memory" errors, and other advantages you'll learn throughout this documentation. + +By following the links at the bottom of each page you'll progressively learn your way through Innmind. While the [Philosophy](philosophy/index.md) chapter is an important part you can skip to the [Getting started](getting-started/index.md) one if you want to feel what it's like to code with Innmind. diff --git a/docs/packages.md b/docs/packages.md new file mode 100644 index 0000000..30572e7 --- /dev/null +++ b/docs/packages.md @@ -0,0 +1,478 @@ +--- +hide: + - navigation +--- + +# Other Packages + +## Access Control List + +Immutable object version to define unix permissions. + +```sh +composer require innmind/acl:~3.1 +``` + +```php +use Innmind\ACL\{ + ACL, + User, + Group, + Mode, +}; + +$acl = ACL::of('r---w---x user:group'); + +$acl->allows(User::of('foo'), Group::of('bar'), Mode::read); // false +$acl->allows(User::of('foo'), Group::of('bar'), Mode::execute); // true +``` + +[Repository](https://github.com/Innmind/ACL) + +## Coding standard + +Specifies the code style used throughout Innmind. + +```sh +composer require --dev innmind/coding-standard:~2.0 +``` + +```php title=".php-cs-fixer.dist.php" +toRGBA(); +``` + +[Repository](https://github.com/Innmind/Colour) + +## Cron + +Immutable objects to define cron jobs and install them on a machine (local or remote). + +```sh +composer require innmind/cron:~3.2 +``` + +```php +use Innmind\Cron\{ + Crontab, + Job, +}; +use Innmind\Server\Control\Server\Command; + +$install = Crontab::forUser( + 'admin', + new Job( + Job\Schedule::everyDayAt(10, 30), + Command::foreground('say hello'), + ), +); +$install($os->control()); +``` + +[Repository](https://github.com/Innmind/Cron) + +## Encoding + +Allows to `tar` [directories](getting-started/operating-system/filesystem.md) and `gzip` [files](getting-started/operating-system/filesystem.md) in a memory safe way. + +```sh +composer require innmind/encoding:~1.0 +``` + +```php +use Innmind\Filesystem\{ + File, + Name, +}; +use Innmind\Url\Path; +use Innmind\Encoding\{ + Gzip, + Tar, +}; + +$tar = $os + ->filesystem() + ->mount(Path::of('some/directory/')) + ->get(Name::of('data')) + ->map(Tar::encode($os->clock())) + ->map(Gzip::compress()) + ->match( + static fn(File $file) => $file, + static fn() => null, + ); +``` + +[Repository](https://github.com/Innmind/encoding) + +## Hash + +Allows to compute the hash of [files](getting-started/operating-system/filesystem.md) in a memory safe way. + +```sh +composer require innmind/hash:~1.5 +``` + +```php +use Innmind\Filesystem\Name; +use Innmind\Url\Path; +use Innmind\Hash\{ + Hash, + Value, +}; +use Innmind\Immutable\Set; + +$hash = $os + ->filesystem() + ->mount(Path::of('some-folder/')) + ->get(Name::of('some-file')) + ->map(Hash::sha512->ofFile(...)) + ->match( + static fn(Value $hash): string => $hash->hex(), + static fn() => null, + ); +``` + +[Repository](https://github.com/Innmind/hash) + +## Html + +Allows to parse HTML files to immutable objects (built on top of [XML](#xml)). + +```sh +composer require innmind/html:~6.3 +``` + +```php +use Innmind\Html\Reader\Reader; +use Innmind\HttpTransport\Success; +use Innmind\Http\{ + Request, + Method, + ProtocolVersion, +}; +use Innmind\Url\Url; +use Innmind\Xml\Node; +use Innmind\Immutable\Maybe; + +$read = Reader::default(); + +$html = $os + ->remote() + ->http()(Request::of( + Url::of('https://github.com/'), + Method::get, + ProtocolVersion::v11, + )) + ->maybe() + ->map(static fn(Success $success) => $success->response()->body()) + ->flatMap($read); +$html; // instance of Maybe +``` + +[Repository](https://github.com/Innmind/Html) + +## HTTP Authentication + +Simple structures to define the ways to extract the identity from a [`ServerRequest`](getting-started/app/http.md). + +[Repository](https://github.com/Innmind/HttpAuthentication) + +## HTTP Session + +Object approach to handle HTTP sessions without a global state. + +[Repository](https://github.com/Innmind/HttpSession) + +## Json + +Type safe functions to encode/decode JSON to prevent unseen errors. + +```sh +composer require innmind/json:~1.4 +``` + +```php +use Innmind\Json\Json; + +Json::encode(['foo' => 'bar']); // {"foo":"bar"} +Json::decode('{"foo":"bar"}'); // ['foo' => 'bar'] +Json::decode('{]'); // will throw an exception (instead of returning false) +``` + +[Repository](https://github.com/Innmind/Json) + +## Log reader + +Allows to read Apache access and Monolog logs into immutable objects in a memory safe way. + +```sh +composer require innmind/log-reader:~5.3 +``` + +```php +use Innmind\LogReader\{ + Reader, + LineParser\Monolog, + Log, +}; +use Innmind\Filesystem\{ + File, + Name, +}; +use Innmind\Url\Path; +use Innmind\Immutable\Predicate\Instance; +use Psr\Log\LogLevel; + +$read = Reader::of( + Monolog::of($os->clock()), +); +$os + ->filesystem() + ->mount(Path::of('var/logs/')) + ->get(Name::of('prod.log')) + ->keep(Instance::of(File::class)) + ->map(static fn($file) => $file->content()) + ->toSequence() + ->flatMap($read) + ->filter( + static fn(Log $log) => $log + ->attribute('level') + ->filter(static fn($level) => $level->value() === LogLevel::CRITICAL) + ->match( + static fn() => true, + static fn() => false, + ), + ) + ->foreach( + static fn($log) => $log + ->attribute('message') + ->match( + static fn($attribute) => print($attribute->value()), + static fn() => print('No message found'), + ), + ); +``` + +[Repository](https://github.com/Innmind/LogReader) + +## RabbitMQ management + +Object API on top of the `rabbitmqadmin` CLI command. + +[Repository](https://github.com/Innmind/RabbitMQManagement) + +## Robots.txt + +Allows to parse `robots.txt` files. + +```sh +composer require innmind/robots-txt:~6.2 +``` + +```php +use Innmind\RobotsTxt\{ + Parser, + RobotsTxt, +}; +use Innmind\Url\Url; + +$parse = Parser::of( + $os->remote()->http(), + 'My user agent', +); +$robots = $parse(Url::of('https://github.com/robots.txt'))->match( + static fn(RobotsTxt $robots) => $robots, + static fn() => throw new \RuntimeException('robots.txt not found'), +); +$robots->disallows('My user agent', Url::of('/humans.txt')); // false +$robots->disallows('My user agent', Url::of('/any/other/url')); // true +``` + +[Repository](https://github.com/Innmind/Robots.txt) + +## SSH key provider + +Allows to fetch a user ssh keys from different sources. + +```sh +composer require innmind/ssh-key-provider:~3.2 +``` + +```php +use Innmind\SshKeyProvider\{ + Cache, + Merge, + Local, + Github, + PublicKey, +}; +use Innmind\Url\Path; + +$provide = Cache::of( + Merge::of( + Local::of( + $os->filesystem()->mount(Path::of($_SERVER['USER'].'/.ssh/')), + ), + Github::of( + $os->remote()->http(), + 'GithubUsername', + ), + ), +); + +$provide()->foreach(static fn(PublicKey $key) => print($key->toString())); +``` + +[Repository](https://github.com/Innmind/SshKeyProvider) + +## URL resolver + +Allows to resolve a target url from a base one. This is useful for crawlers. + +```sh +composer require innmind/url-resolver:~5.1 +``` + +```php +use Innmind\UrlResolver\UrlResolver; +use Innmind\Url\Url; + +$resolve = UrlResolver::of('http', 'https'); + +$url = $resolve( + Url::of('http://example.com/foo/'), + Url::of('./bar/baz?query=string#fragment'), +); +// $url resolves to http://example.com/foo/bar/baz?query=string#fragment +``` + +[Repository](https://github.com/Innmind/url-resolver) + +## Validation + +This is a monadic approach to data validation. + +```sh +composer require innmind/validation:~1.4 +``` + +```php +use Innmind\Validation\{ + Shape, + Is, + Each, + Failure, +}; +use Innmind\Immutable\Sequence; + +$valid = [ + 'id' => 42, + 'username' => 'jdoe', + 'addresses' => [ + 'address 1', + 'address 2', + ], + 'submit' => true, +]; +$invalid = [ + 'id' => '42', + 'addresses' => [ + 'address 1', + null, + ], + 'submit' => true, +]; + +$validate = Shape::of('id', Is::int()) + ->with('username', Is::string()) + ->with( + 'addresses', + Is::list( + Is::string()->map( + static fn(string $address) => new YourModel($address), + ) + ) + ); +$result = $validate($valid)->match( + static fn(array $value) => $value, + static fn() => throw new \RuntimeException('invalid data'), +); +// Here $result looks like: +// [ +// 'id' => 42 +// 'username' => 'jdoe', +// 'addresses' [ +// new YourModel('address 1'), +// new YourModel('address 2'), +// ], +// (1) +// ] +$errors = $validate($invalid)->match( + static fn() => null, + static fn(Sequence $failures) => $failures + ->map(static fn(Failure $failure) => [ + $failure->path()->toString(), + $failure->message(), + ]) + ->toList(), +); +// Here $errors looks like: +// [ +// ['id', 'Value is not of type int'], +// ['$', 'The key username is missing'], +// ['addresses', 'Value is not of type string'] +// ] +``` + +1. See how the `submit` key disappeared. + +[Repository](https://github.com/Innmind/validation) + +## XML + +Allows to parse XML files to immutable objects. + +```sh +composer require innmind/xml:~7.5 +``` + +```php +use Innmind\Xml\{ + Reader\Reader, + Node, +}; +use Innmind\Filesystem\File\Content; +use Innmind\Immutable\Maybe; + +$read = Reader::of(); + +$tree = $read( + Content::ofString('') +); // Maybe +``` + +[Repository](https://github.com/Innmind/XML) diff --git a/docs/philosophy/abstractions.md b/docs/philosophy/abstractions.md new file mode 100644 index 0000000..f6fb86c --- /dev/null +++ b/docs/philosophy/abstractions.md @@ -0,0 +1,26 @@ +# Abstractions + +!!! abstract "" + All maps are wrong... but some are useful. + +For a map to be useful it has to be wrong. For a map to be right it has to represent with exactitude the world, meaning it has to be the size of the world it represent. Such map has no use as it doesn't simplify our task. + +A map's aim is to shrink the information to the minimum for us to accomplish our task. This means that the level of information **must** depend on our task. You won't take the same map for a road trip or a hike. + +Abstractions are the same. + +An abstraction that tries to represent all the information it tries to abstract fails in its mission. In the end you end up with the same information but expressed in a different way. + +This means that for an abstraction to be useful it **must** omit information. + +Then comes the need to choose which information to keep and the [appropriate semantic](semantic.md) for the task. + +This also means that an abstraction can't fit all tasks and you may not be able to use it. But this is ok, not everyone has to speak the same language; you just need to find the vocabulary that fits your need. + +Innmind doesn't try to fit everybody's need (1). +{.annotate} + +1. As you'll find with the [filesystem abstraction](../getting-started/operating-system/filesystem.md) or the [ORM](../getting-started/orm/index.md). + +??? note + But keep in mind that semantics change and vocabularies expand. diff --git a/docs/philosophy/capabilities.md b/docs/philosophy/capabilities.md new file mode 100644 index 0000000..baefe2e --- /dev/null +++ b/docs/philosophy/capabilities.md @@ -0,0 +1,16 @@ +# Capabilities + +[Ambient authority](https://en.wikipedia.org/wiki/Ambient_authority) is the ability to call the system (1) from anywhere in your program. +{.annotate} + +1. such as `fopen` + +On the other hand [Capabilities](https://en.wikipedia.org/wiki/Capability-based_security) is a way to represent a resource we have access to, and is given to us. We cannot access it directly. + +These designs are security models inside programs. + +Innmind focuses more on the system access side more than the security one. + +In essence the capabilities approach is about dependency injection on all things concerning the operating system. + +That's why the [Operating System](../getting-started/operating-system/index.md) abstraction is central in the ecosystem. diff --git a/docs/philosophy/development.md b/docs/philosophy/development.md new file mode 100644 index 0000000..e03e602 --- /dev/null +++ b/docs/philosophy/development.md @@ -0,0 +1,32 @@ +# Development Process + +## Type strictness + +All packages use [Psalm](https://psalm.dev) on the strictess level to make sure there won't be type errors. + +To make sure you use Innmind correctly you should use it as well. + +## Versioning + +All packages use [Semver](https://semver.org) to release new versions. + +All minor and bugfix versions are retro compatible and try as mush as possible to not change your program's behaviour. Most updates bring new code that you have to choose to use it (or not). + +Major updates break the API. + +Changelogs and the type system will help you through all changes. + +## Make it easy to use it right + +> and make it difficult to use it wrong. + +This summarizes all the previous chapters. + +To reach this all packages go through the same iteration loop: + +- make it work +- make it simple +- make it fast + +??? note + Though the last step has a lower priority than building new higher level [abstractions](abstractions.md). diff --git a/docs/philosophy/explicit.md b/docs/philosophy/explicit.md new file mode 100644 index 0000000..0d969ee --- /dev/null +++ b/docs/philosophy/explicit.md @@ -0,0 +1,74 @@ +# Explicit + +Implicit behaviours makes life easy for small programs. But as it grows and time passes it becomes more and more difficult to remember all of them and make sure they fit together. + +Innmind itself is a large project. That's why it tries to be as explicit as possible. + +By explicit ear the facts that: + +- no code will have unforeseen global behaviour +- no package installation will automatically change a behaviour of a program +- you call Innmind code, not the other way around + +This means that to understand your program you can always _go to the definition_ of the function you're using. You can traverse your whole program from entrypoint to low level calls to the system with this approach. + +Below you'll find some techniques that make Innmind explicit. + +## Parse, don't validate + +!!! info "" + This is a reference to [Alexis King's article](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/). + +Validation is the process to check if a value respect a set of rules before using it. Take this example that may feel familiar: + +```php +$email = 'foo@example.com'; +createUser($email); +sendWelcomeEmail($email); +``` + +Here both functions have to validate that the `string` passed as argument is indeed an email. The validation has to be done twice because the second function is unaware of the one done in the first function. + +Instead Parsing means to attach an information to the validated value. In other words encapsulate your data in an object. + +With parsing the example above becomes: + +```php +$email = new Email('foo@example.com'); +createUser($email); +sendWelcomeEmail($email); +``` + +Now the validation is done by the `Email` class and will throw if the `string` is not an email. The following functions no longer have to do validation as they're guaranteed to have a valid email as argument. + +This is why you'll find a lot of classes in Innmind that only hold data, the classes name are important. + +??? note "Exceptions to the rule" + Note that they're some exceptions to this rule such as the [`Sequence`](../getting-started/handling-data/sequence.md) not having a sister class `NonEmptySequence`. This class doesn't exist because it would make [composition](oop-fp.md#composition) harder. + +## Constraints liberate, liberties constrain + +!!! info "" + This is a reference to [Runar Bjarnason's talk](https://www.youtube.com/watch?v=GqmsQeSzMdw). + +A constraint prevents us from doing some thing. Liberty is our ability to do what we want. + +The saying _constraints liberate, liberties constrain_ then may seem contradictory. Yet us abiding by the law is just that. The law prevents us from harming another citizen, this constraint liberates us from having to worry about someone else trying to harm us and consequently free us to think about more productive activities. + +As developers we tend to want to do whatever we want in our programs. But this limits us in the level of abstractions we can use. + +Innmind chooses to apply constraints in order to build higher [abstractions](abstractions.md). + +### Closed by default + +This applies to 2 things: code and data. + +For a code to be closed usually means having `final` classes to prevent developer to use inheritance to modify the behaviour of a program, thus encouraging [composition](oop-fp.md#composition). The alternative is the use of functions that can only be composed. + +For data this means to be very restrictive when [parsing](#parse-dont-validate). + +### Maintainability + +By having enough constraints it simplifies the maintainability of this ecosystem. + +All possible usages and possible values are known thanks to these constraints, meaning any modification can be safely released. diff --git a/docs/philosophy/index.md b/docs/philosophy/index.md new file mode 100644 index 0000000..f6ad242 --- /dev/null +++ b/docs/philosophy/index.md @@ -0,0 +1,16 @@ +# Philosophy + +The ultimate goal of this organization is to verify [Antonio Damasio](https://en.wikipedia.org/wiki/Antonio_Damasio)'s theory of consciousness[^1]. + +While this objective doesn't directly matter for your projects it structures this ecosystem in a big way: + +!!! info "" + Innmind itself tries to solve a problem. + +This is why all the packages fits together to help solve bigger and bigger problems. It also means it won't go away while the goal is not reached. And since the goal may even not be reachable, Innmind is here to stay! + +This also enforces 2 things. Abstractions need to be extremely robust to safely build higher abstractions. And they need to be reusable outside this project (to avoid being wasted in case the goal is not reachable). + +All the following chapters will describe the principles behind the abstractions robustness. + +[^1]: You can learn more in this [french article](https://github.com/Innmind/Research-N-Development/blob/master/Papers/Sur%20la%20conscience.md). diff --git a/docs/philosophy/oop-fp.md b/docs/philosophy/oop-fp.md new file mode 100644 index 0000000..66fa75a --- /dev/null +++ b/docs/philosophy/oop-fp.md @@ -0,0 +1,142 @@ +# OOP & FP + +## OOP + +Originally this paradigm was intended to represent the behaviour of _living cells_ (objects) that interact with each other via message passing. Each cell/object is supposed to be a closed unit of compute. + +Innmind follows this principle by using closed (aka `final`) classes with no getter/setter method, each method express an action. + +There are 2 main kind of objects: the ones representing data with their associated behaviours (aka methods) and the ones expressing actions (usually with only the `__invoke` method). + +The _bigger idea_ of OOP mentioned by [Alan Kay](https://en.wikipedia.org/wiki/Alan_Kay) of message passing is fulfilled by the [Actor Model](../getting-started/concurrency/distributed.md#actor-model). + +## FP + +This is a more mathematical approach to building programs that has gained a lot of traction for the past few years. + +Innmind mainly use the principles described below, but FP as a whole has a lot more. + +### Immutability + +You already use immutable data without even realising it. For example if you do `#!php $result = str_replace($search, $replace, $subject)`, the call of the function doesn't modify the values inside `$search`, `$replace` or `$subject` but returns a new value `$result`. This is in essence immutability, you only return new values. + +This allows to have less things to think about when calling a method. You know for sure the data you pass in to a function won't change. And inside a function you know that manipulating the data passed in won't have side effects outside of your scope. + +The immutability of data applies to primitive values but also to objects. A call to a method will return a new object, and the initial object it kept as is. + +Innmind uses immutable data everywhere possible. + +### Purity + +This concept applies to functions; may it be anonymous functions, named functions or methods. + +A function is considered pure if it has no side effects. A side effect may be altering state or doing I/O. This means that a function can't use a global variable, print something to the screen or read something from the filesystem. + +In other words a pure function only interacts with the immutable arguments passed in and returns an immutable value. + +Just like Immutability this allows to have less things to think about. By modifying/calling a pure function you know you won't break an unforeseen part of your program. + +### Totality + +A function is considered _total_ if it can return a value for any combination of arguments it accepts. + +For example the function `divide(int, int): float` is not total because it will have to throw an exception for a division by `0`. On the other hand `divide(int, int): ?float` is total because it can return `null` in case of a division by `0`. + +The advantage of using total functions is that a [static analysis tool](development.md#type-strictness) can automatically check all the combinations to make sure your programm won't crash. It eliminates the need to write tests for the exceptions or the surprises of the runtime. + +Innmind heavily relies on this design to reduce the mental load of making sure every unhappy path is covered. + +??? info "Exceptions" + Innmind also relies on `Exception`s for cases where the program can't be recovered. But since it can't be recovered you don't have to think about them, that's also why they're not documented. + + It somewhat follows the `Let it crash` approach of [Erlang](https://www.erlang.org). + +### Composition + +This allows to extend the behaviour of a function without knowing its implementation. And this is completely transparent for the caller of such functions. + +An example is an HTTP client (1) that provides a base implementation to do calls via `curl` and the `logger`, `followRedirections` and `circuitBreaker` decorators. You can compose them any way you wish: +{.annotate} + +1. such as [`innmind/http-transport`](https://github.com/Innmind/HttpTransport) + +
+- `logger(followRedirections(curl))` will only log the user calls and is unaware if the redirections are followed +- `followRedirections(logger(curl))` will log the user calls and every redirections +- `circuitBreaker(logger(curl))` will not log calls to a domain that has previously failed +- etc... +
+ +The big advantage is that you can compose them _locally_ depending on your needs (1). +{.annotate} + +1. As opposed to the inheritance approach where you're limited by the statically defined combinations exposed. + +Innmind heavily uses composition to adapt behaviours locally and allows you to compose the base implementations the way you wish. + +??? info + The interesting discovery after many years of using composition with Innmind is that the higher the abstractions the more possibilities it offers. + +### _Type detonation_ + +This means computing a concrete value. But _detonating_ too early exposes some problems. + +For example, the math operation of the square of the square root of a number should return the same number. But if the type is _detonated_ at each function `square(squareRoot(2))` won't return `2`(1). +{.annotate} + +1. `sqrt(2)**2` will return `2.0000000000000004` + +By not _detonating_ too early the abstractions can do some optimisations on your behalf without even realising it (1). This is done by returning intermediate representations of the operations that needs to be done. +{.annotate} + +1. For example [`innmind/math`](https://github.com/Innmind/Math) uses objects to represents numbers that optimise the `square(squareRoot(2))` in order to compute `2`. Or [`innmind/http-transport`](https://github.com/Innmind/HttpTransport) that can [run requests concurrently](../getting-started/concurrency/http.md). + +In essence this _lazyness_ allows Innmind to optimise some operations. + +### Monads + +Monads are the culmination of the designs described above and are the cornerstone of this ecosystem. + +They're data structures classes with at least 2 methods: `map` and `flatMap`. + +- `map` will return a new monad of the same class with the data contained in it modified via the function passed to `map`. +- `flatMap` is similar to `map` except that the function passed to it must return a monad. + +Said like this, monads are very abstract concepts. So here's an example of the simplest monad, the `Identity`: + +=== "Identity monad" + ```php + $value = Identity::of(1) + ->map(fn(int $value) => $value + 1) + ->flatMap(fn(int $value) => Identity::of($value * 2)) + ->unwrap(); + $value === 4; // true + ``` + +=== "Plain old PHP" + ```php + $value = 1; + $value = $value + 1; + $value = $value * 2; + $value === 4; // true + ``` + +While this example may not seem to provide much value, these structures are extremly powerful as it will be shown in [Getting started](../getting-started/index.md). + +??? tip "Type detonation" + [Detonating](#type-detonation) a monad means calling a method that returns something else than a monad. + +## False dichotomy + +When choosing our tools we are usually presented 2 choices: Hype vs Boring technologies, OOP vs FP, etc... with the impression that we can only choose one. + +Innmind uses both OOP and FP to take advantages from both worlds. + +The easiness of combining data with associated methods and handling of mutable data (1) of OOP. The ease of mind of FP to only have to deal with the code in front of you (2). +{.annotate} + +1. such as socket servers +2. There's little chance to break the code on the other side of a program. + +*[OOP]: Object-Oriented Programming +*[FP]: Functional Programming diff --git a/docs/philosophy/semantic.md b/docs/philosophy/semantic.md new file mode 100644 index 0000000..7fcf62a --- /dev/null +++ b/docs/philosophy/semantic.md @@ -0,0 +1,42 @@ +# Semantic + +## The aim + +A good semantic allows to communicate an idea quickly by _compressing the information_. For example, the word _cat_ caries a lot more information and in a more comprehensive way than _a set of atoms forming a small 4 legged animal with whiskers_. + +By establishing a good vocabulary it's possible to convey more and more complex information in a relatively constant space. + +## This is not code + +```php +$os + ->filesystem() + ->mount(Path::of('folder/')) + ->get(Name::of('file')) + ->keep(Instance::of(File::class)) + ->match( + static fn(File $file) => $file + ->content() + ->lines() + ->foreach(static fn(Line $line) => echo $line->toString()), + static fn() => echo 'unknown file', + ); +``` + +With this example we see that it's possible to understand the result of a program without knowing how it is executed. + +Code was initially a way to tell the machine the steps to follow to reach a result. But the more we move up through the abstractions (languages included) the farther away we get to tell the machine the exact steps. + +The more the abstractions the more we need to communicate with other developers in order to build programs. + +Code is now a formalised language between humans that has the side effect of being runnable by machines. + +## Declarative + +Imperative code is telling the machine **how** to do things. While Declarative is telling **what** to do. + +A concrete example of this is the difference between an `array` and the [`Sequence` monad](../getting-started/handling-data/sequence.md). With an array it's easy to handle data as it's assigning values for indices, but it's not possible to handle an infinite stream of values (as it requires to use generators). On the other hand the `Sequence` only allows to describe the transitions the values must follow, you have no say on the way the values are assigned internally. + +This allows the `Sequence` to have multiple internal representations. It can work just like an `array` with the same assignment logic, or it can work with an infinite stream of values. The choice lies with the one creating the `Sequence`, any use of it is the same afterward. + +It's by this mechanism that this ecosystem can grow while keeping the complexity under control. diff --git a/docs/philosophy/simplicity.md b/docs/philosophy/simplicity.md new file mode 100644 index 0000000..5ad9095 --- /dev/null +++ b/docs/philosophy/simplicity.md @@ -0,0 +1,41 @@ +# Simplicity + +## Complexity vs Difficulty + +We tend to use simple and easy or difficult and complex interchangeably. But they're very much different. + +Complexity is an objective scale (1) of a number of parts of a system and the number of interactions between each part. +{.annotate} + +1. with Simplicity being on one end of this scale + +Difficulty is a subjective scale related to your familiarity with a subject. By familiarity ear the number of times you've done some task. + +A general example of this difference is an electric circuit to light a bulb: + +- it is _simple_ to use, you only need to be aware of the switch to light the bulb (1) + {.annotate} + + 1. even with many switches to light the same bulb the complexity is the same + +- it is _complex_ to build such circuit (1) + {.annotate} + + 1. especially with mutliple switches + +- it is _difficult_ for a child to build such circuit +- it is _easy_ for an electrician to build such circuit + +Innmind heavily leans toward simplicity. Even if at times it doesn't feel easy. + +## In practice + +The [Filesystem package](../getting-started/operating-system/filesystem.md) was bitten (in an early version) for mistaking easyness by simplicity. + +The `Adapter` interface has a `get` method to return a file. Initially the argument passed to it was a `string` to represent the file name. But months later when building an [S3](https://github.com/Innmind/S3) abstraction for this interface it wasn't clear if a path could be passed in the string. + +The easiness of using a `string` brought complexity to the implementation to make sure all adapters behave the same way. And also brought difficulty to the user when switching an adapter for another, having to deal with the inconsistencies. + +The `string` was later replaced by the a class named `Name`. Any `Adapter` implementation has to check its behaviour to understand what's possible, no need to be aware of other implementations anymore. + +You'll find all kind of classes in this ecosystem that encapsulate values to reach this kind of simplicity. diff --git a/docs/testing/blackbox.md b/docs/testing/blackbox.md new file mode 100644 index 0000000..7ce997c --- /dev/null +++ b/docs/testing/blackbox.md @@ -0,0 +1,129 @@ +# BlackBox + +This is Innmind's own testing framework. + +It follows [Innmind's philosophy](../philosophy/index.md) meaning it can be integrated in other tools. It is self contained and do not rely on global state. + +## Installation + +```sh +composer require --dev innmind/black-box:~5.6 +``` + +## Setup + +```php title="blackbox.php" +tryToProve(function() { + yield test( + 'More on tests in the next chapter', + static fn($assert) => $assert->true(true), + ); + }) + ->exit(); +``` + +1. More on the usage of `$argv` [below](#tags). + +This is the simplest setup of BlackBox. A PHP file (1) that bootstraps an `Application` to which is passed a function that will return a generator of tests and then exits. +{.annotate} + +1. In this case the file is named `blackbox.php` but you can call it the way you want. + +And you simply run your tests via `php blackbox.php`. + +## Organization + +In the example above the tests are provided inside an inline generator. This is fine when you only have a few of them. When it's no longer convenient you should split your tests in multiple files. + +=== "File 1" + ```php title="proofs/file1.php" + $assert->true(true), + ); + } + ``` + +=== "File 2" + ```php title="proofs/file2.php" + $assert->true(true), + ); + } + ``` + +If you want to load these 2 files you can do: + +```php title="blackbox.php" hl_lines="10-11" +tryToProve(function() { + yield from (require 'proofs/file1.php'); + yield from (require 'proofs/file2.php'); + }) + ->exit(); +``` + +This is good because you can control the way your files are loaded. But adding new files becomes tedious, especially when multiple persons work on the project. + +Instead you can simplify it with: + +```php title="blackbox.php" hl_lines="8 12" +tryToProve(Load::everythingIn('proofs/')) + ->exit(); +``` + +In the end you have full control over the order your tests are loaded. + +## Tags + +After a while you may end up with a lot of tests and running them all all the time can be time consuming. You can categorize your tests via tags. + +You declare them this way: + +```php +use Innmind\BlackBox\Tag; + +yield test( #(1) + 'Test name', + static fn($assert) => $assert->true(true), +)->tag(Tag::positive, Tag::wip); +``` + +1. Refer to example above to know where to place a test. + +Then to only run a test with a given tag: `php blackbox.php wip`. + +The list of arguments you pass in the CLI command is passed to BlackBox via the code `#!php Application::new($argv)`. Each argument must correspond to the name of a case on the `Innmind\BlackBox\Tag` enum. diff --git a/docs/testing/index.md b/docs/testing/index.md new file mode 100644 index 0000000..799a5f1 --- /dev/null +++ b/docs/testing/index.md @@ -0,0 +1,14 @@ +# Testing + +Testing is an important part of any project. In PHP the popular frameworks helps you test at different levels of abstraction with a variety of syntaxes. But they all work pretty much the same way: they help you test one particular scenario. + +!!! success "" + Innmind uses the [Property Based Testing](property-based-testing.md) technique. + +This helps bring higher and higher stable abstractions without the burden to write more scenarii manually. + +[`innmind/black-box`](blackbox.md) can help you bring this technique to your projects. You can start writing tests like you're used to and gradually move toward PBT. + +Innmind packages initially were tested with PHPUnit and now gradually move to BlackBox. + +*[PBT]: Property Based Testing diff --git a/docs/testing/proofs.md b/docs/testing/proofs.md new file mode 100644 index 0000000..e080909 --- /dev/null +++ b/docs/testing/proofs.md @@ -0,0 +1,122 @@ +# Proofs + +A proof[^1] is a way to declare a behaviour for a range of values. + +Let's refactor the [previous test](tests.md): + +```php hl_lines="4 9 11-16" +use Innmind\BlackBox\{ + Application, + Runner\Assert, + Set, +}; + +Application::new([]) + ->tryToProve(function() { + yield proof( + 'add', + given( + Set\Elements::of(1), + Set\Elements::of(2), + Set\Elements::of(3), + ), + static fn(Assert $assert, int $a, int $b, int $expected) => $assert + ->expected($expected) + ->same(add($a, $b)), + ); + }) + ->exit(); +``` + +The most important part here is the `Set`s passed to `given`. A `Set` defines a range of values to generate a scenario. In this case we use 3 sets each containing a single value. Each `Set` parameter you add to `given` adds an argument to the test function. + +In essence this proof does **exactly** the same thing as the previous test. + +Now if we want to generate mutiple scenario: + +```php +use Innmind\BlackBox\{ + Application, + Runner\Assert, + Set, +}; + +Application::new([]) + ->tryToProve(function() { + yield proof( + 'add', + given( + Set\Elements::of( + [1, 2, 3], + [2, 3, 5], + ), + ), + static function(Assert $assert, array $case) { + [$a, $b, $expected] = $case; + $assert + ->expected($expected) + ->same(add($a, $b)); + } + ); + }) + ->exit(); +``` + +This proof will run both `add(1, 2) === 3` and `add(2, 3) === 5`. But you don't want to specify all scenarii possible to prove the behaviour of this function. + +Instead you'd code the `add` properties like this: + +```php +use Innmind\BlackBox\{ + Application, + Runner\Assert, + Set, +}; + +Application::new([]) + ->tryToProve(function() { + yield proof( + 'add is commutative', + given( + Set\Integers::any(), + Set\Integers::any(), + ), + static fn(Assert $assert, int $a, int $b) => $assert->same( + add($a, $b), + add($b, $a), + ), + ); + yield proof( + 'add is cumulative', + given( + Set\Integers::any(), + Set\Integers::any(), + Set\Integers::any(), + ), + static fn(Assert $assert, int $a, int $b, int $c) => $assert->same( + add($a, add($b, $c)), + add(add($a, $b), $c), + ), + ); + yield proof( + '0 is an identity value', + given(Set\Integers::any()), + static fn(Assert $assert, int $a) => $assert->same( + $a, + add($a, 0), #(1) + ), + ); + }) + ->exit(); +``` + +1. Where `0` is placed doesn't matter thanks to the commutative proof above. + +Each time you'll run these proofs BlackBox will run `100` scenarii per proof with different values each time. This way the more you run these proofs the more BlackBox explores the _values space_ to try to find a specific combination that makes the `add` function fail. + +You should explore the `Innmind\BlackBox\Set\` namespace to see all the values you can generate. `Set`s can be composed so you're not limited to primitive values, you can build pretty much any data structure. + +??? tip + Other packages can also expose `Set`s, you can find them on Packagist via the [`innmind/black-box-sets` virtual package](https://packagist.org/providers/innmind/black-box-sets). + +[^1]: BlackBox use the term _proof_ to emphasize that you are testing behaviours not specific scenarii, but these are **NOT** [formal proofs](https://en.wikipedia.org/wiki/Formal_proof). diff --git a/docs/testing/properties.md b/docs/testing/properties.md new file mode 100644 index 0000000..c880ce4 --- /dev/null +++ b/docs/testing/properties.md @@ -0,0 +1,11 @@ +# Properties + +Properties are [proofs](proofs.md) extracted as classes so that they can be run in a different context. More specifically they verify the behaviour of objects. + +The main goal of these properties is to make sure multiple implementations of a given system all behave the same way. + +The most prominent example in the Innmind ecosystem are the [filesystem](../getting-started/operating-system/filesystem.md) properties. [`innmind/filesystem`](https://github.com/Innmind/Filesystem) has 2 implementations of its `Adapter` interface, a real implementation and an in memory one, and both implementations are tested against the same properties. [`innmind/s3`](https://github.com/Innmind/S3) also has implementation of `Adapter` and simply uses the filesystem properties to make sure implementations can be swapped by a user without a change in behaviour. + +The [ORM](../getting-started/orm/index.md) also uses properties to make sure its 3 storage implementations behave exactly the same way. + +To learn more about them head to the [BlackBox package documentation](https://github.com/Innmind/BlackBox/). diff --git a/docs/testing/property-based-testing.md b/docs/testing/property-based-testing.md new file mode 100644 index 0000000..47e4f71 --- /dev/null +++ b/docs/testing/property-based-testing.md @@ -0,0 +1,86 @@ +# Property Based Testing + +## Description + +Instead of writing tests like: + +> When I run this code with this value X then I should get back the value Y. + +You'd do: + +
+> When I run this code with any value of type X then this property (1) is true. +
+ +1. Here _property_ is used in the general sense, not as an _object property_. + +The advantage of writing tests this way is that we define the _range of values_ that should make the test pass. It's then up to the framework to generate the values within this range and run your test multiple times to make sure the test pass. + +This means that the testing framework constantly run new scenarii for your tests. The more you run your tests the more confidence you have that your code is correct. + +!!! success "" + The goal is to make sure your implementation is correct in all possible scenarii. And it also has the side effect to making your tests less brittle. + +## Examples + +### Math `add` function + +This is the _go to_ example to introduce this technique as this function is easy to understand and only have 3 properties: + +=== "Commutative" + This means that you can change the order of arguments and still get the same result. + + In pseudo code a test would look like: + ```php + function testCommutative(int $a, int $b) { + assertSame( + add($a, $b), + add($b, $a), + ); + } + ``` + +=== "Cumulative" + This means that no matter the order of operations you still get the same result. + + In pseudo code a test would look like: + ```php + function testCumulative(int $a, int $b, int $c) { + assertSame( + add($a, add($b, $c)), + add(add($a, $b), $c), + ); + } + ``` + +=== "`0` is an identity value" + This means that if you add `0` to any other value it returns the same value. + + In pseudo code a test would look like: + ```php + function testIdentityValue(int $a) { + assertSame( + $a, + add($a, 0), + ); + } + ``` + +With this 3 properties you virtually cover all possible operations that could be run. And this makes the implementation of `add` very difficult to get wrong. + +### More realistic example + +This kind of example is rarely the kind of things you'll have to test in a real program. + +A more concrete example that exists in many programs is user registration/login. Let's say that the registration workflow is a new user comes in and register, a confirmation email is sent, and once confirmed the user can login. + +The properties that you would have to verify are: + +- Any new user (pair of identifier and password strings) can register +- Any new user can't login by default +- Any new user can't register twice +- A user can login after clicking the email confirmation link + +The first one verifies you can accept any pair of strings (no crash of your program). The second verifies the registration is partial and an extra step is required in order to login. The third one prevents duplicates in your database and sending multiple emails that would confuse the user. And the last one makes sure that it's the email extra step that allows the user to login. + +To find the properties of a system look for business rules that are **always** true. Do not try to put the whole business rules inside a single property. It's the combination of them that makes sure the system is correct. diff --git a/docs/testing/tests.md b/docs/testing/tests.md new file mode 100644 index 0000000..be0eb7d --- /dev/null +++ b/docs/testing/tests.md @@ -0,0 +1,60 @@ +# Tests + +Before learning to write proofs and properties, you should familiarizes with BlackBox API by writing simple tests. + +## Specific scenario + +If we reuse the [`add` function](property-based-testing.md#examples): + +```php +use Innmind\BlackBox\{ + Application, + Runner\Assert, +}; + +Application::new([]) + ->tryToProve(function() { + yield test( + 'add(1, 2)', + static fn(Assert $assert) => $assert + ->expected(3) + ->same(add(1, 2)), + ); + }) + ->exit(); +``` + +Here we use a short function with only one assertion but you can run any number of assertions in a function. + +!!! info "" + You should explore the `Assert`ion API with your code editor to discover all its capabilities. + +## Same scenario for multiple values + +Sometime you may want to run the same test but with a different set of values. Since declaring the tests is a generator you can do: + +```php +use Innmind\BlackBox\{ + Application, + Runner\Assert, +}; + +Application::new([]) + ->tryToProve(function() { + $cases = [ + [1, 2, 3], + [2, 3, 5], + // etc... + ]; + + foreach ($cases as [$a, $b, $expected]) { + yield test( + "add($a, $b)", + static fn(Assert $assert) => $assert + ->expected($expected) + ->same(add($a, $b)), + ); + } + }) + ->exit(); +``` diff --git a/docs/tools.md b/docs/tools.md new file mode 100644 index 0000000..997bbfb --- /dev/null +++ b/docs/tools.md @@ -0,0 +1,99 @@ +--- +hide: + - navigation +--- + +# Tools + +All the examples below assume that the folder `~/.composer/vendor/bin` is part of your `$PATH` environment variable. + +## Dependency graph + +This tool helps to visualize all the dependencies of a package or an organization. The main feature is that it highlight the dependencies that are out of date. + +This ease the development on Innmind to understand in which order to update packages. + +### Installation + +```sh +composer global require innmind/dependency-graph:~3.5 +``` + +### Usage + +=== "Organization" + ```sh + dependency-graph vendor innmind + ``` + + ![](assets/dependency-graph/innmind.svg) + + !!! info "" + You can see a lot of red because the refactoring to the monadic style is still under way. + +=== "From lock" + ```sh + cd to/your/project && dependency-graph from-lock + ``` + + ![](assets/dependency-graph/dependencies.svg) + +=== "Package dependencies" + ```sh + dependency-graph of innmind/cli + ``` + + ![](assets/dependency-graph/innmind_cli_dependencies.svg) + +=== "Package dependents" + ```sh + dependency-graph depends-on innmind/cli innmind + ``` + + ![](assets/dependency-graph/innmind_cli_dependents.svg) + +### macOS App + +Instead of manually executing the CLI commands you can use this [macOS app](https://github.com/Innmind/macOS-tooling). You only need to specify the organization you want to visualize and the app will fetch everything necessary. + +![](assets/dependency-graph/macOS-app.png) + +## Lab station + +This tool automatically runs your tests, psalm and code style checker everytime you save a file. + +### Installation + +```sh +composer global require innmind/lab-station:~4.1 +``` + +### Usage + +```sh +cd to/package/ && lab-station +``` + + + +## Git release + +This is a tool to create [SemVer](https://semver.org) tags. + +### Installation + +```sh +composer global require innmind/git-release:~3.1 +``` + +### Usage + +```sh +git-release major +``` + +If the last major version is `1.2.3` this will create the tag `2.0.0` and push it to the remote. + +There's also the `minor` and `bugfix` commands. diff --git a/use_cases/copy_local_directory_to_s3.md b/docs/use-cases/copy-local-directory-to-s3.md similarity index 63% rename from use_cases/copy_local_directory_to_s3.md rename to docs/use-cases/copy-local-directory-to-s3.md index 2482a19..562193e 100644 --- a/use_cases/copy_local_directory_to_s3.md +++ b/docs/use-cases/copy-local-directory-to-s3.md @@ -1,15 +1,16 @@ # Copy a local directory to S3 +```sh +composer require innmind/s3:~4.1 +``` + ```php -use Innmind\OperatingSystem\Factory; use Innmind\S3; use Innmind\Url\{ Url, Path, }; -$os = Factory::build(); - $bucket = S3\Factory::of($os)->build( Url::of('https://acces_key:acces_secret@bucket-name.s3.region-name.scw.cloud/'), S3\Region::of('region-name'), @@ -22,5 +23,3 @@ $directory = $os ->root(); $s3->add($directory); ``` - -> **Note** This example requires [`innmind/operating-system`](https://packagist.org/packages/innmind/operating-system) and [`innmind/s3`](https://packagist.org/packages/innmind/s3). diff --git a/docs/use-cases/creating-archive-directory.md b/docs/use-cases/creating-archive-directory.md new file mode 100644 index 0000000..dba8610 --- /dev/null +++ b/docs/use-cases/creating-archive-directory.md @@ -0,0 +1,30 @@ +# Creating an archive of a directory + +```sh +composer require innmind/encoding:~1.0 +``` + +```php +use Innmind\Filesystem\Name; +use Innmind\Url\Path; +use Innmind\Encoding\{ + Gzip, + Tar, +}; + +$tar = $os + ->filesystem() + ->mount(Path::of('some/directory/')) + ->get(Name::of('data')) + ->map(Tar::encode($os->clock())) + ->map(Gzip::compress()) + ->match( + static fn($file) => $file, + static fn() => throw new \RuntimeException('Data not found'), + ); +``` + +Here `$tar` represents a `.tar.gz` file containing all the files and directories from `some/directory/data/`. + +!!! info + The content of the `$tar` file is lazily computed meaning you can create an archive larger than the allowed PHP memory. diff --git a/docs/use-cases/index.md b/docs/use-cases/index.md new file mode 100644 index 0000000..9b1172e --- /dev/null +++ b/docs/use-cases/index.md @@ -0,0 +1,3 @@ +# Use cases + +In this chapter you'll find a set of use cases using packages already shown in previous chapters and [a few other new ones](../packages.md). diff --git a/use_cases/persist_crawled_links_to_database.md b/docs/use-cases/persist-crawled-links-to-database.md similarity index 64% rename from use_cases/persist_crawled_links_to_database.md rename to docs/use-cases/persist-crawled-links-to-database.md index f5ee3be..c94df94 100644 --- a/use_cases/persist_crawled_links_to_database.md +++ b/docs/use-cases/persist-crawled-links-to-database.md @@ -1,10 +1,13 @@ # Persist crawled links to a database +```sh +composer require innmind/html:~6.3 +``` + ```php -use Innmind\OperatingSystem\Factory; use Innmind\Http\{ - Message\Request\Request, - Message\Method, + Request, + Method, ProtocolVersion, }; use Innmind\Html\{ @@ -20,15 +23,14 @@ use Formal\AccessLayer\{ Row, }; -$os = Factory::build(); -$reader = Reader::default(); +$read = Reader::default(); $sql = $os ->remote() ->sql(Url::of('mysql://127.0.0.1:3306/database_name')); $_ = $os ->remote() - ->http()(new Request( + ->http()(Request::of( Url::of('https://some-server.com/page.html') Method::get, ProtocolVersion::v11, @@ -40,11 +42,9 @@ $_ = $os ->toSet() ->flatMap(Elements::of('a')) ->keep(Instance::of(A::class)) - ->map(static fn($a) => $a->href()->toString()) - ->foreach(static fn($href) => $sql(Insert::into( + ->map(static fn(A $a) => $a->href()->toString()) + ->foreach(static fn(string $href) => $sql(Insert::into( Name::of('table_name'), Row::of(['column_name' => $href]), ))); ``` - -> **Note** This example requires [`innmind/operating-system`](https://packagist.org/packages/innmind/operating-system) and [`innmind/html`](https://packagist.org/packages/innmind/html). diff --git a/use_cases/persist_sql_result_to_file.md b/docs/use-cases/persist-sql-result-to-file.md similarity index 76% rename from use_cases/persist_sql_result_to_file.md rename to docs/use-cases/persist-sql-result-to-file.md index efbc0a7..b55d790 100644 --- a/use_cases/persist_sql_result_to_file.md +++ b/docs/use-cases/persist-sql-result-to-file.md @@ -1,11 +1,10 @@ # Persist a SQL result to a file ```php -use Innmind\OperatingSystem\Factory; -use Innmind\Filesystem\File\{ +use Innmind\Filesystem\{ File, - Content\Lines, - Content\Line, + File\Content, + File\Content\Line, }; use Innmind\Url\{ Url, @@ -17,8 +16,6 @@ use Formal\AccessLayer\{ Table\Name, }; -$os = Factory::build(); - $sql = $os ->remote() ->sql(Url::of('mysql://127.0.0.1:3306/database_name')); @@ -28,7 +25,7 @@ $_ = $os ->mount(Path::of('some directory/')) ->add(File::named( 'results.csv', - Lines::of( + Content::ofLines( $sql(Select::onDemand(Name::of('table_name'))) ->map( static fn($row) => $row @@ -42,5 +39,3 @@ $_ = $os ``` Since the sql query is lazy (thanks to `::onDemand()`) you can persist a very long result without loading everything in memory. - -> **Note** This example requires [`innmind/operating-system`](https://packagist.org/packages/innmind/operating-system). diff --git a/use_cases/serve_s3_file.md b/docs/use-cases/serve-s3-file.md similarity index 85% rename from use_cases/serve_s3_file.md rename to docs/use-cases/serve-s3-file.md index c531922..1674647 100644 --- a/use_cases/serve_s3_file.md +++ b/docs/use-cases/serve-s3-file.md @@ -1,5 +1,9 @@ # Serve a S3 file via an HTTP server +```sh +composer require innmind/s3:~4.1 +``` + ```php use Innmind\Framework\{ Application, @@ -12,9 +16,9 @@ use Innmind\Filesystem\{ Name, }; use Innmind\Http\{ - Message\ServerRequest, - Message\Response\Response, - Message\StatusCode, + ServerRequest, + Response, + Response\StatusCode, Headers, Header\ContentType, }; @@ -39,7 +43,7 @@ new class extends Http { ->s3 ->get(Name::of('some file.txt')) ->match( - static fn($file) => new Response( + static fn($file) => Response::of( StatusCode::ok, $request->protocolVersion(), Headers::of(ContentType::of( @@ -60,4 +64,5 @@ new class extends Http { }; ``` -> **Note** This example requires [`innmind/framework`](https://packagist.org/packages/innmind/framework) and [`innmind/s3`](https://packagist.org/packages/innmind/s3). +!!! tip + Head to the [framework chapter](../getting-started/framework/index.md) to learn how to call this server. diff --git a/use_cases/upload_local_file.md b/docs/use-cases/upload-local-file.md similarity index 74% rename from use_cases/upload_local_file.md rename to docs/use-cases/upload-local-file.md index b991975..3b814ec 100644 --- a/use_cases/upload_local_file.md +++ b/docs/use-cases/upload-local-file.md @@ -1,7 +1,6 @@ # Upload a local file via HTTP ```php -use Innmind\OperatingSystem\Factory; use Innmind\Filesystem\Name; use Innmind\Http\{ Message\Request\Request, @@ -17,8 +16,6 @@ use Innmind\Url\{ Path, }; -$os = Factory::build(); - $boundary = Boundary::uuid(); $_ = $os ->filesystem() @@ -27,12 +24,14 @@ $_ = $os ->flatMap( static fn($file) => $os ->remote() - ->http()(new Request( + ->http()(Request::of( Url::of('https://some-server.com/api/upload'), Method::post, ProtocolVersion::v11, Headers::of(ContentType::of('multipart', 'form-data', $boundary)), - Multipart::boundary($boundary)->withFile('some[file]', $file), + Multipart::boundary($boundary) + ->withFile('some[file]', $file) + ->asContent(), )) ->maybe(), ) @@ -41,5 +40,3 @@ $_ = $os static fn() => throw new \Exception('No file or failed to upload'), ); ``` - -> **Note** This example requires [`innmind/operating-system`](https://packagist.org/packages/innmind/operating-system). diff --git a/docs/use-cases/wait-server-start.md b/docs/use-cases/wait-server-start.md new file mode 100644 index 0000000..6c45ba6 --- /dev/null +++ b/docs/use-cases/wait-server-start.md @@ -0,0 +1,29 @@ +# Wait for a server to start + +Let's say you want to start the PHP HTTP server to starting sending requests to it. Before sending requests you need to make sure it's up. + +You can do so with: + +```php +use Innmind\Server\Control\Server\Command; +use Innmind\Immutable\Str; + +$process = $os + ->control() + ->processes() + ->execute( + Command::foreground('php') + ->withShortOption('S') + ->withArgument('localhost:8080'), + ); +$process + ->output() + ->chunks() + ->map(static fn(array $chunk) => $chunk[0]) + ->takeWhile(static fn(Str $chunk) => !$chunk->contains('started')) + ->memoize(); + +// you can send requests here +``` + +The `memoize` call is important because it's at this point that it will wait for an output chunk to contain the `started` text. Since by default the output use a [deferred `Sequence`](../getting-started/handling-data/sequence.md#deferred) without the `memoize` it would do nothing (as if the code wasn't there at all). diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..bdf6e3f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,157 @@ +site_name: Innmind +site_description: The functional ecosystem +repo_name: innmind/documentation + +nav: + - Home: index.md + - Philosophy: + - philosophy/index.md + - OOP & FP: philosophy/oop-fp.md + - Semantic: philosophy/semantic.md + - Simplicity: philosophy/simplicity.md + - Explicit: philosophy/explicit.md + - Capabilities: philosophy/capabilities.md + - Abstractions: philosophy/abstractions.md + - Development Process: philosophy/development.md + - Getting started: + - getting-started/index.md + - Handling data: + - getting-started/handling-data/index.md + - Sequence: getting-started/handling-data/sequence.md + - Maybe: getting-started/handling-data/maybe.md + - Either: getting-started/handling-data/either.md + - Operating System: + - getting-started/operating-system/index.md + - Clock: getting-started/operating-system/clock.md + - HTTP: getting-started/operating-system/http.md + - Filesystem: getting-started/operating-system/filesystem.md + - SQL: getting-started/operating-system/sql.md + - PHP Process: getting-started/operating-system/php-process.md + - Processes: getting-started/operating-system/processes.md + - Monitoring: getting-started/operating-system/monitoring.md + - Network: getting-started/operating-system/network.md + - App: + - CLI: getting-started/app/cli.md + - HTTP: getting-started/app/http.md + - Framework: + - getting-started/framework/index.md + - CLI: getting-started/framework/cli.md + - HTTP: getting-started/framework/http.md + - Middlewares: getting-started/framework/middlewares.md + - Profiler: getting-started/framework/profiler.md + - Extensions: getting-started/framework/extensions.md + - ORM: + - getting-started/orm/index.md + - Development: getting-started/orm/development.md + - Production: getting-started/orm/production.md + - Testing: getting-started/orm/testing.md + - Concurrency: + - getting-started/concurrency/index.md + - HTTP calls: getting-started/concurrency/http.md + - Asynchronous code: getting-started/concurrency/async.md + - Queues: getting-started/concurrency/queues.md + - Inter Process Communication: getting-started/concurrency/ipc.md + - Distributed: getting-started/concurrency/distributed.md + - Use cases: + - use-cases/index.md + - Upload a local file via HTTP: use-cases/upload-local-file.md + - Copy a local directory to S3: use-cases/copy-local-directory-to-s3.md + - Serve a S3 file via an HTTP server: use-cases/serve-s3-file.md + - Persist a SQL result to a file: use-cases/persist-sql-result-to-file.md + - Persist crawled links to a database: use-cases/persist-crawled-links-to-database.md + - Creating an archive of a directory: use-cases/creating-archive-directory.md + - Wait for a server to start: use-cases/wait-server-start.md + - Testing: + - testing/index.md + - Property Based testing: testing/property-based-testing.md + - BlackBox: testing/blackbox.md + - Tests: testing/tests.md + - Proofs: testing/proofs.md + - Properties: testing/properties.md + - Tools: tools.md + - Other Packages: packages.md + +theme: + name: material + logo: assets/logo.svg + favicon: assets/favicon.png + font: false + features: + - content.code.copy + - content.code.annotate + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.expand + - navigation.indexes + - navigation.top + - navigation.footer + - search.suggest + - search.highlight + - content.action.edit + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + primary: blue + accent: deep orange + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + primary: blue + accent: deep orange + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + primary: blue + accent: deep orange + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + extend_pygments_lang: + - name: php + lang: php + options: + startinline: true + - pymdownx.inlinehilite + - pymdownx.snippets + - attr_list + - md_in_html + - pymdownx.superfences + - abbr + - admonition + - pymdownx.details: + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + - footnotes + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +extra_css: + - assets/stylesheets/extra.css + +plugins: + - search + - privacy + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/Innmind + - icon: fontawesome/brands/x-twitter + link: https://twitter.com/Baptouuuu diff --git a/packages.md b/packages.md deleted file mode 100644 index 7b9b426..0000000 --- a/packages.md +++ /dev/null @@ -1,88 +0,0 @@ -# Packages - -| Name | CI | Type coverage | Code coverage | | -|-|-|-|-|-| -| [`acl`](https://packagist.org/packages/innmind/acl) | [![Build Status](https://github.com/innmind/acl/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/acl/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/acl/coverage.svg)](https://shepherd.dev/github/innmind/acl) | [![codecov](https://codecov.io/gh/innmind/acl/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/acl) | [Documentation](https://github.com/innmind/acl/#usage) | -| [`amqp`](https://packagist.org/packages/innmind/amqp) | [![Build Status](https://github.com/innmind/amqp/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/amqp/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/amqp/coverage.svg)](https://shepherd.dev/github/innmind/amqp) | [![codecov](https://codecov.io/gh/innmind/amqp/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/amqp) | [Documentation](https://github.com/innmind/amqp/#usage) | -| [`ark`](https://packagist.org/packages/innmind/ark) | [![Build Status](https://github.com/innmind/ark/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/ark/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/ark/coverage.svg)](https://shepherd.dev/github/innmind/ark) | [![codecov](https://codecov.io/gh/innmind/ark/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/ark) | [Documentation](https://github.com/innmind/ark/#usage) | -| [`async-http-server`](https://packagist.org/packages/innmind/async-http-server) | [![Build Status](https://github.com/innmind/async-http-server/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/async-http-server/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/async-http-server/coverage.svg)](https://shepherd.dev/github/innmind/async-http-server) | | [Documentation](https://github.com/Innmind/async-http-server#usage) | -| [`async-operating-system`](https://packagist.org/packages/innmind/async-operating-system) | [![Build Status](https://github.com/innmind/async-operating-system/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/async-operating-system/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/async-operating-system/coverage.svg)](https://shepherd.dev/github/innmind/async-operating-system) | [![codecov](https://codecov.io/gh/innmind/async-operating-system/branch/main/graph/badge.svg)](https://codecov.io/gh/innmind/async-operating-system) | [Documentation](https://github.com/Innmind/async-operating-system#usage) | -| [`async-socket`](https://packagist.org/packages/innmind/async-socket) | [![Build Status](https://github.com/innmind/async-socket/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/async-socket/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/async-socket/coverage.svg)](https://shepherd.dev/github/innmind/async-socket) | | [Documentation](https://github.com/Innmind/async-socket) | -| [`async-stream`](https://packagist.org/packages/innmind/async-stream) | [![Build Status](https://github.com/innmind/async-stream/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/async-stream/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/async-stream/coverage.svg)](https://shepherd.dev/github/innmind/async-stream) | [![codecov](https://codecov.io/gh/innmind/async-stream/branch/main/graph/badge.svg)](https://codecov.io/gh/innmind/async-stream) | [Documentation](https://github.com/Innmind/async-stream#usage) | -| [`async-time-warp`](https://packagist.org/packages/innmind/async-time-warp) | [![Build Status](https://github.com/innmind/async-time-warp/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/async-time-warp/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/async-time-warp/coverage.svg)](https://shepherd.dev/github/innmind/async-time-warp) | [![codecov](https://codecov.io/gh/innmind/async-time-warp/branch/main/graph/badge.svg)](https://codecov.io/gh/innmind/async-time-warp) | [Documentation](https://github.com/Innmind/async-time-warp#usage) | -| [`black-box`](https://packagist.org/packages/innmind/black-box) | [![Build Status](https://github.com/innmind/blackbox/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/blackbox/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/blackbox/coverage.svg)](https://shepherd.dev/github/innmind/blackbox) | [![codecov](https://codecov.io/gh/innmind/blackbox/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/blackbox) | [Documentation](https://github.com/Innmind/BlackBox/blob/master/documentation/readme.md) | -| [`cli`](https://packagist.org/packages/innmind/cli) | [![Build Status](https://github.com/innmind/cli/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/cli/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/cli/coverage.svg)](https://shepherd.dev/github/innmind/cli) | [![codecov](https://codecov.io/gh/innmind/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/cli) | [Documentation](https://github.com/innmind/cli/#usage) | -| [`coding-standard`](https://packagist.org/packages/innmind/coding-standard) | [![Build Status](https://github.com/innmind/coding-standard/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/coding-standard/actions?query=workflow%3ACI) | | | [Documentation](https://github.com/innmind/coding-standard/#usage) | -| [`colour`](https://packagist.org/packages/innmind/colour) | [![Build Status](https://github.com/innmind/colour/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/colour/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/colour/coverage.svg)](https://shepherd.dev/github/innmind/colour) | [![codecov](https://codecov.io/gh/innmind/colour/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/colour) | [Documentation](https://github.com/innmind/colour/#usage) | -| [`crawler`](https://packagist.org/packages/innmind/crawler) | [![Build Status](https://github.com/innmind/crawler/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/crawler/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/crawler/coverage.svg)](https://shepherd.dev/github/innmind/crawler) | [![codecov](https://codecov.io/gh/innmind/crawler/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/crawler) | [Documentation](https://github.com/innmind/crawler/#usage) | -| [`crawler-app`](https://packagist.org/packages/innmind/crawler-app) | [![Build Status](https://github.com/innmind/crawlerapp/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/crawlerapp/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/crawlerapp/coverage.svg)](https://shepherd.dev/github/innmind/crawlerapp) | [![codecov](https://codecov.io/gh/innmind/crawlerapp/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/crawlerapp) | [Documentation](https://github.com/innmind/crawlerapp/#usage) | -| [`cron`](https://packagist.org/packages/innmind/cron) | [![Build Status](https://github.com/innmind/cron/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/cron/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/cron/coverage.svg)](https://shepherd.dev/github/innmind/cron) | [![codecov](https://codecov.io/gh/innmind/cron/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/cron) | [Documentation](https://github.com/innmind/cron/#usage) | -| [`debug`](https://packagist.org/packages/innmind/debug) | [![Build Status](https://github.com/innmind/debug/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/debug/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/debug/coverage.svg)](https://shepherd.dev/github/innmind/debug) | [![codecov](https://codecov.io/gh/innmind/debug/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/debug) | [Documentation](https://github.com/innmind/debug/#usage) | -| [`dependency-graph`](https://packagist.org/packages/innmind/dependency-graph) | [![Build Status](https://github.com/innmind/dependencygraph/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/dependencygraph/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/dependencygraph/coverage.svg)](https://shepherd.dev/github/innmind/dependencygraph) | [![codecov](https://codecov.io/gh/innmind/dependencygraph/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/dependencygraph) | [Documentation](https://github.com/innmind/dependencygraph/#usage) | -| [`di`](https://packagist.org/packages/innmind/di) | [![Build Status](https://github.com/innmind/di/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/di/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/di/coverage.svg)](https://shepherd.dev/github/innmind/di) | [![codecov](https://codecov.io/gh/innmind/di/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/di) | [Documentation](https://github.com/innmind/di/#usage) | -| [`doctrine`](https://packagist.org/packages/innmind/doctrine) | [![Build Status](https://github.com/innmind/doctrine/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/doctrine/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/doctrine/coverage.svg)](https://shepherd.dev/github/innmind/doctrine) | [![codecov](https://codecov.io/gh/innmind/doctrine/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/doctrine) | [Documentation](https://github.com/innmind/doctrine/#usage) | -| [`file-watch`](https://packagist.org/packages/innmind/file-watch) | [![Build Status](https://github.com/innmind/filewatch/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/filewatch/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/filewatch/coverage.svg)](https://shepherd.dev/github/innmind/filewatch) | [![codecov](https://codecov.io/gh/innmind/filewatch/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/filewatch) | [Documentation](https://github.com/innmind/filewatch/#usage) | -| [`filesystem`](https://packagist.org/packages/innmind/filesystem) | [![Build Status](https://github.com/innmind/filesystem/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/filesystem/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/filesystem/coverage.svg)](https://shepherd.dev/github/innmind/filesystem) | [![codecov](https://codecov.io/gh/innmind/filesystem/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/filesystem) | [Documentation](https://github.com/innmind/filesystem/#usage) | -| [`genome`](https://packagist.org/packages/innmind/genome) | [![Build Status](https://github.com/innmind/genome/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/genome/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/genome/coverage.svg)](https://shepherd.dev/github/innmind/genome) | [![codecov](https://codecov.io/gh/innmind/genome/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/genome) | [Documentation](https://github.com/innmind/genome/#usage) | -| [`git`](https://packagist.org/packages/innmind/git) | [![Build Status](https://github.com/innmind/git/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/git/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/git/coverage.svg)](https://shepherd.dev/github/innmind/git) | [![codecov](https://codecov.io/gh/innmind/git/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/git) | [Documentation](https://github.com/innmind/git/#usage) | -| [`git-release`](https://packagist.org/packages/innmind/git-release) | [![Build Status](https://github.com/innmind/gitrelease/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/gitrelease/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/gitrelease/coverage.svg)](https://shepherd.dev/github/innmind/gitrelease) | [![codecov](https://codecov.io/gh/innmind/gitrelease/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/gitrelease) | [Documentation](https://github.com/innmind/gitrelease/#usage) | -| [`graphviz`](https://packagist.org/packages/innmind/graphviz) | [![Build Status](https://github.com/innmind/graphviz/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/graphviz/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/graphviz/coverage.svg)](https://shepherd.dev/github/innmind/graphviz) | [![codecov](https://codecov.io/gh/innmind/graphviz/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/graphviz) | [Documentation](https://github.com/innmind/graphviz/#usage) | -| [`hash`](https://packagist.org/packages/innmind/hash) | [![Build Status](https://github.com/innmind/hash/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/hash/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/hash/coverage.svg)](https://shepherd.dev/github/innmind/hash) | [![codecov](https://codecov.io/gh/innmind/hash/branch/main/graph/badge.svg)](https://codecov.io/gh/innmind/hash) | [Documentation](https://github.com/innmind/hash/#usage) | -| [`homeostasis`](https://packagist.org/packages/innmind/homeostasis) | [![Build Status](https://github.com/innmind/homeostasis/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/homeostasis/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/homeostasis/coverage.svg)](https://shepherd.dev/github/innmind/homeostasis) | [![codecov](https://codecov.io/gh/innmind/homeostasis/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/homeostasis) | [Documentation](https://github.com/innmind/homeostasis/#usage) | -| [`html`](https://packagist.org/packages/innmind/html) | [![Build Status](https://github.com/innmind/html/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/html/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/html/coverage.svg)](https://shepherd.dev/github/innmind/html) | [![codecov](https://codecov.io/gh/innmind/html/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/html) | [Documentation](https://github.com/innmind/html/#usage) | -| [`http`](https://packagist.org/packages/innmind/http) | [![Build Status](https://github.com/innmind/http/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/http/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/http/coverage.svg)](https://shepherd.dev/github/innmind/http) | [![codecov](https://codecov.io/gh/innmind/http/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/http) | [Documentation](https://github.com/innmind/http/#build-a-serverrequest) | -| [`http-authentication`](https://packagist.org/packages/innmind/http-authentication) | [![Build Status](https://github.com/innmind/httpauthentication/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/httpauthentication/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/httpauthentication/coverage.svg)](https://shepherd.dev/github/innmind/httpauthentication) | [![codecov](https://codecov.io/gh/innmind/httpauthentication/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/httpauthentication) | [Documentation](https://github.com/innmind/httpauthentication/#usage) | -| [`http-parser`](https://packagist.org/packages/innmind/http-parser) | [![Build Status](https://github.com/innmind/http-parser/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/http-parser/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/http-parser/coverage.svg)](https://shepherd.dev/github/innmind/http-parser) | [![codecov](https://codecov.io/gh/innmind/http-parser/branch/main/graph/badge.svg)](https://codecov.io/gh/innmind/http-parser) | [Documentation](https://github.com/innmind/http-parser/#usage) | -| [`http-server`](https://packagist.org/packages/innmind/http-server) | [![Build Status](https://github.com/innmind/httpserver/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/httpserver/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/httpserver/coverage.svg)](https://shepherd.dev/github/innmind/httpserver) | | [Documentation](https://github.com/innmind/httpserver/#usage) | -| [`http-session`](https://packagist.org/packages/innmind/http-session) | [![Build Status](https://github.com/innmind/httpsession/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/httpsession/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/httpsession/coverage.svg)](https://shepherd.dev/github/innmind/httpsession) | [![codecov](https://codecov.io/gh/innmind/httpsession/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/httpsession) | [Documentation](https://github.com/innmind/httpsession/#usage) | -| [`http-transport`](https://packagist.org/packages/innmind/http-transport) | [![Build Status](https://github.com/innmind/httptransport/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/httptransport/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/httptransport/coverage.svg)](https://shepherd.dev/github/innmind/httptransport) | [![codecov](https://codecov.io/gh/innmind/httptransport/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/httptransport) | [Documentation](https://github.com/innmind/httptransport/#usage) | -| [`immutable`](https://packagist.org/packages/innmind/immutable) | [![Build Status](https://github.com/innmind/immutable/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/immutable/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/immutable/coverage.svg)](https://shepherd.dev/github/innmind/immutable) | [![codecov](https://codecov.io/gh/innmind/immutable/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/immutable) | [Documentation](https://github.com/Innmind/Immutable/blob/master/docs/README.md) | -| [`infrastructure`](https://packagist.org/packages/innmind/infrastructure) | [![Build Status](https://github.com/innmind/infrastructure/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/infrastructure/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/infrastructure/coverage.svg)](https://shepherd.dev/github/innmind/infrastructure) | | | -| [`infrastructure-amqp`](https://packagist.org/packages/innmind/infrastructure-amqp) | [![Build Status](https://github.com/innmind/infrastructureamqp/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/infrastructureamqp/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/infrastructureamqp/coverage.svg)](https://shepherd.dev/github/innmind/infrastructureamqp) | | | -| [`infrastructure-neo4j`](https://packagist.org/packages/innmind/infrastructure-neo4j) | [![Build Status](https://github.com/innmind/infrastructureneo4j/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/infrastructureneo4j/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/infrastructureneo4j/coverage.svg)](https://shepherd.dev/github/innmind/infrastructureneo4j) | | | -| [`infrastructure-nginx`](https://packagist.org/packages/innmind/infrastructure-nginx) | [![Build Status](https://github.com/innmind/infrastructurenginx/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/infrastructurenginx/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/infrastructurenginx/coverage.svg)](https://shepherd.dev/github/innmind/infrastructurenginx) | | | -| [`installation-monitor`](https://packagist.org/packages/innmind/installation-monitor) | [![Build Status](https://github.com/innmind/installationmonitor/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/installationmonitor/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/installationmonitor/coverage.svg)](https://shepherd.dev/github/innmind/installationmonitor) | [![codecov](https://codecov.io/gh/innmind/installationmonitor/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/installationmonitor) | [Documentation](https://github.com/innmind/installationmonitor/#usage) | -| [`io`](https://packagist.org/packages/innmind/io) | [![Build Status](https://github.com/innmind/io/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/io/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/io/coverage.svg)](https://shepherd.dev/github/innmind/io) | [![codecov](https://codecov.io/gh/innmind/io/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/io) | [Documentation](https://github.com/innmind/io/#usage) | -| [`ip`](https://packagist.org/packages/innmind/ip) | [![Build Status](https://github.com/innmind/ip/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/ip/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/ip/coverage.svg)](https://shepherd.dev/github/innmind/ip) | [![codecov](https://codecov.io/gh/innmind/ip/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/ip) | [Documentation](https://github.com/innmind/ip/#usage) | -| [`ipc`](https://packagist.org/packages/innmind/ipc) | [![Build Status](https://github.com/innmind/ipc/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/ipc/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/ipc/coverage.svg)](https://shepherd.dev/github/innmind/ipc) | [![codecov](https://codecov.io/gh/innmind/ipc/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/ipc) | [Documentation](https://github.com/innmind/ipc/#usage) | -| [`json`](https://packagist.org/packages/innmind/json) | [![Build Status](https://github.com/innmind/json/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/json/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/json/coverage.svg)](https://shepherd.dev/github/innmind/json) | [![codecov](https://codecov.io/gh/innmind/json/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/json) | [Documentation](https://github.com/innmind/json/#usage) | -| [`kalmiya`](https://packagist.org/packages/innmind/kalmiya) | [![Build Status](https://github.com/innmind/kalmiya/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/kalmiya/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/kalmiya/coverage.svg)](https://shepherd.dev/github/innmind/kalmiya) | | [Documentation](https://github.com/innmind/kalmiya/#usage) | -| [`lab-station`](https://packagist.org/packages/innmind/lab-station) | [![Build Status](https://github.com/innmind/labstation/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/labstation/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/labstation/coverage.svg)](https://shepherd.dev/github/innmind/labstation) | [![codecov](https://codecov.io/gh/innmind/labstation/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/labstation) | [Documentation](https://github.com/innmind/labstation/#lab-station) | -| [`library`](https://packagist.org/packages/innmind/library) | [![Build Status](https://github.com/innmind/library/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/library/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/library/coverage.svg)](https://shepherd.dev/github/innmind/library) | [![codecov](https://codecov.io/gh/innmind/library/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/library) | [Documentation](https://github.com/innmind/library/#usage) | -| [`log-reader`](https://packagist.org/packages/innmind/log-reader) | [![Build Status](https://github.com/innmind/logreader/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/logreader/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/logreader/coverage.svg)](https://shepherd.dev/github/innmind/logreader) | [![codecov](https://codecov.io/gh/innmind/logreader/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/logreader) | [Documentation](https://github.com/innmind/logreader/#usage) | -| [`logger`](https://packagist.org/packages/innmind/logger) | [![Build Status](https://github.com/innmind/logger/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/logger/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/logger/coverage.svg)](https://shepherd.dev/github/innmind/logger) | [![codecov](https://codecov.io/gh/innmind/logger/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/logger) | [Documentation](https://github.com/innmind/logger/#usage) | -| [`mantle`](https://packagist.org/packages/innmind/mantle) | [![Build Status](https://github.com/innmind/mantle/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/mantle/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/mantle/coverage.svg)](https://shepherd.dev/github/innmind/mantle) | [![codecov](https://codecov.io/gh/innmind/mantle/branch/main/graph/badge.svg)](https://codecov.io/gh/innmind/mantle) | [Documentation](https://github.com/innmind/mantle/) | -| [`math`](https://packagist.org/packages/innmind/math) | [![Build Status](https://github.com/innmind/math/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/math/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/math/coverage.svg)](https://shepherd.dev/github/innmind/math) | [![codecov](https://codecov.io/gh/innmind/math/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/math) | [Documentation](https://github.com/innmind/math/#algebra) | -| [`media-type`](https://packagist.org/packages/innmind/media-type) | [![Build Status](https://github.com/innmind/mediatype/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/mediatype/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/mediatype/coverage.svg)](https://shepherd.dev/github/innmind/mediatype) | [![codecov](https://codecov.io/gh/innmind/mediatype/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/mediatype) | [Documentation](https://github.com/innmind/mediatype/#usage) | -| [`neo4j-dbal`](https://packagist.org/packages/innmind/neo4j-dbal) | [![Build Status](https://github.com/innmind/neo4j-dbal/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/neo4j-dbal/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/neo4j-dbal/coverage.svg)](https://shepherd.dev/github/innmind/neo4j-dbal) | [![codecov](https://codecov.io/gh/innmind/neo4j-dbal/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/neo4j-dbal) | [Documentation](https://github.com/innmind/neo4j-dbal/#documentation) | -| [`neo4j-onm`](https://packagist.org/packages/innmind/neo4j-onm) | [![Build Status](https://github.com/innmind/neo4j-onm/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/neo4j-onm/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/neo4j-onm/coverage.svg)](https://shepherd.dev/github/innmind/neo4j-onm) | [![codecov](https://codecov.io/gh/innmind/neo4j-onm/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/neo4j-onm) | [Documentation](https://github.com/innmind/neo4j-onm/#documentation) | -| [`object-graph`](https://packagist.org/packages/innmind/object-graph) | [![Build Status](https://github.com/innmind/objectgraph/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/objectgraph/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/objectgraph/coverage.svg)](https://shepherd.dev/github/innmind/objectgraph) | [![codecov](https://codecov.io/gh/innmind/objectgraph/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/objectgraph) | [Documentation](https://github.com/innmind/objectgraph/#usage) | -| [`operating-system`](https://packagist.org/packages/innmind/operating-system) | [![Build Status](https://github.com/innmind/operatingsystem/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/operatingsystem/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/operatingsystem/coverage.svg)](https://shepherd.dev/github/innmind/operatingsystem) | [![codecov](https://codecov.io/gh/innmind/operatingsystem/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/operatingsystem) | [Documentation](https://github.com/Innmind/OperatingSystem/blob/master/documentation/readme.md) | -| [`process-manager`](https://packagist.org/packages/innmind/process-manager) | [![Build Status](https://github.com/innmind/processmanager/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/processmanager/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/processmanager/coverage.svg)](https://shepherd.dev/github/innmind/processmanager) | [![codecov](https://codecov.io/gh/innmind/processmanager/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/processmanager) | [Documentation](https://github.com/innmind/processmanager/#usage) | -| [`profiler`](https://packagist.org/packages/innmind/profiler) | [![Build Status](https://github.com/innmind/profiler/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/profiler/actions?query=workflow%3ACI) | | [![codecov](https://codecov.io/gh/innmind/profiler/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/profiler) | [Documentation](https://github.com/innmind/profiler/#overview) | -| [`rabbitmq-management`](https://packagist.org/packages/innmind/rabbitmq-management) | [![Build Status](https://github.com/innmind/rabbitmqmanagement/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/rabbitmqmanagement/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/rabbitmqmanagement/coverage.svg)](https://shepherd.dev/github/innmind/rabbitmqmanagement) | [![codecov](https://codecov.io/gh/innmind/rabbitmqmanagement/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/rabbitmqmanagement) | [Documentation](https://github.com/innmind/rabbitmqmanagement/#usage) | -| [`reflection`](https://packagist.org/packages/innmind/reflection) | [![Build Status](https://github.com/innmind/reflection/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/reflection/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/reflection/coverage.svg)](https://shepherd.dev/github/innmind/reflection) | [![codecov](https://codecov.io/gh/innmind/reflection/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/reflection) | [Documentation](https://github.com/innmind/reflection/#build-and-inject-data-into-an-object) | -| [`robots-txt`](https://packagist.org/packages/innmind/robots-txt) | [![Build Status](https://github.com/innmind/robots.txt/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/robots.txt/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/robots.txt/coverage.svg)](https://shepherd.dev/github/innmind/robots.txt) | [![codecov](https://codecov.io/gh/innmind/robots.txt/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/robots.txt) | [Documentation](https://github.com/innmind/robots.txt/#usage) | -| [`router`](https://packagist.org/packages/innmind/router) | [![Build Status](https://github.com/innmind/router/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/router/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/router/coverage.svg)](https://shepherd.dev/github/innmind/router) | [![codecov](https://codecov.io/gh/innmind/router/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/router) | [Documentation](https://github.com/innmind/router/#usage) | -| [`s3`](https://packagist.org/packages/innmind/s3) | [![Build Status](https://github.com/innmind/s3/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/s3/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/s3/coverage.svg)](https://shepherd.dev/github/innmind/s3) | [![codecov](https://codecov.io/gh/innmind/s3/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/s3) | [Documentation](https://github.com/innmind/s3/#usage) | -| [`scaleway-sdk`](https://packagist.org/packages/innmind/scaleway-sdk) | [![Build Status](https://github.com/innmind/scalewaysdk/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/scalewaysdk/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/scalewaysdk/coverage.svg)](https://shepherd.dev/github/innmind/scalewaysdk) | [![codecov](https://codecov.io/gh/innmind/scalewaysdk/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/scalewaysdk) | [Documentation](https://github.com/innmind/scalewaysdk/#usage) | -| [`server-control`](https://packagist.org/packages/innmind/server-control) | [![Build Status](https://github.com/innmind/servercontrol/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/servercontrol/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/servercontrol/coverage.svg)](https://shepherd.dev/github/innmind/servercontrol) | [![codecov](https://codecov.io/gh/innmind/servercontrol/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/servercontrol) | [Documentation](https://github.com/innmind/servercontrol/#usage) | -| [`server-status`](https://packagist.org/packages/innmind/server-status) | [![Build Status](https://github.com/innmind/serverstatus/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/serverstatus/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/serverstatus/coverage.svg)](https://shepherd.dev/github/innmind/serverstatus) | [![codecov](https://codecov.io/gh/innmind/serverstatus/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/serverstatus) | [Documentation](https://github.com/innmind/serverstatus/#usage) | -| [`signals`](https://packagist.org/packages/innmind/signals) | [![Build Status](https://github.com/innmind/signals/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/signals/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/signals/coverage.svg)](https://shepherd.dev/github/innmind/signals) | [![codecov](https://codecov.io/gh/innmind/signals/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/signals) | [Documentation](https://github.com/innmind/signals/#usage) | -| [`silent-cartographer`](https://packagist.org/packages/innmind/silent-cartographer) | [![Build Status](https://github.com/innmind/silentcartographer/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/silentcartographer/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/silentcartographer/coverage.svg)](https://shepherd.dev/github/innmind/silentcartographer) | [![codecov](https://codecov.io/gh/innmind/silentcartographer/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/silentcartographer) | [Documentation](https://github.com/innmind/silentcartographer/#usage) | -| [`socket`](https://packagist.org/packages/innmind/socket) | [![Build Status](https://github.com/innmind/socket/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/socket/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/socket/coverage.svg)](https://shepherd.dev/github/innmind/socket) | [![codecov](https://codecov.io/gh/innmind/socket/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/socket) | [Documentation](https://github.com/innmind/socket/#usage) | -| [`specification`](https://packagist.org/packages/innmind/specification) | [![Build Status](https://github.com/innmind/specification/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/specification/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/specification/coverage.svg)](https://shepherd.dev/github/innmind/specification) | [![codecov](https://codecov.io/gh/innmind/specification/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/specification) | [Documentation](https://github.com/innmind/specification/#implementation-example) | -| [`ssh-key-provider`](https://packagist.org/packages/innmind/ssh-key-provider) | [![Build Status](https://github.com/innmind/sshkeyprovider/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/sshkeyprovider/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/sshkeyprovider/coverage.svg)](https://shepherd.dev/github/innmind/sshkeyprovider) | [![codecov](https://codecov.io/gh/innmind/sshkeyprovider/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/sshkeyprovider) | [Documentation](https://github.com/innmind/sshkeyprovider/#usage) | -| [`stack`](https://packagist.org/packages/innmind/stack) | [![Build Status](https://github.com/innmind/stack/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/stack/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/stack/coverage.svg)](https://shepherd.dev/github/innmind/stack) | [![codecov](https://codecov.io/gh/innmind/stack/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/stack) | [Documentation](https://github.com/innmind/stack/#usage) | -| [`stack-trace`](https://packagist.org/packages/innmind/stack-trace) | [![Build Status](https://github.com/innmind/stacktrace/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/stacktrace/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/stacktrace/coverage.svg)](https://shepherd.dev/github/innmind/stacktrace) | [![codecov](https://codecov.io/gh/innmind/stacktrace/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/stacktrace) | [Documentation](https://github.com/innmind/stacktrace/#usage) | -| [`stream`](https://packagist.org/packages/innmind/stream) | [![Build Status](https://github.com/innmind/stream/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/stream/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/stream/coverage.svg)](https://shepherd.dev/github/innmind/stream) | [![codecov](https://codecov.io/gh/innmind/stream/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/stream) | [Documentation](https://github.com/innmind/stream/#usage) | -| [`templating`](https://packagist.org/packages/innmind/templating) | [![Build Status](https://github.com/innmind/templating/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/templating/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/templating/coverage.svg)](https://shepherd.dev/github/innmind/templating) | [![codecov](https://codecov.io/gh/innmind/templating/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/templating) | [Documentation](https://github.com/innmind/templating/#usage) | -| [`time-continuum`](https://packagist.org/packages/innmind/time-continuum) | [![Build Status](https://github.com/innmind/timecontinuum/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/timecontinuum/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/timecontinuum/coverage.svg)](https://shepherd.dev/github/innmind/timecontinuum) | [![codecov](https://codecov.io/gh/innmind/timecontinuum/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/timecontinuum) | [Documentation](https://github.com/innmind/timecontinuum/#usage) | -| [`time-warp`](https://packagist.org/packages/innmind/time-warp) | [![Build Status](https://github.com/innmind/timewarp/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/timewarp/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/timewarp/coverage.svg)](https://shepherd.dev/github/innmind/timewarp) | [![codecov](https://codecov.io/gh/innmind/timewarp/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/timewarp) | [Documentation](https://github.com/innmind/timewarp/#usage) | -| [`tower`](https://packagist.org/packages/innmind/tower) | [![Build Status](https://github.com/innmind/tower/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/tower/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/tower/coverage.svg)](https://shepherd.dev/github/innmind/tower) | [![codecov](https://codecov.io/gh/innmind/tower/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/tower) | [Documentation](https://github.com/innmind/tower/#tower) | -| [`url`](https://packagist.org/packages/innmind/url) | [![Build Status](https://github.com/innmind/url/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/url/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/url/coverage.svg)](https://shepherd.dev/github/innmind/url) | [![codecov](https://codecov.io/gh/innmind/url/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/url) | [Documentation](https://github.com/innmind/url/#usage) | -| [`url-resolver`](https://packagist.org/packages/innmind/url-resolver) | [![Build Status](https://github.com/innmind/url-resolver/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/url-resolver/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/url-resolver/coverage.svg)](https://shepherd.dev/github/innmind/url-resolver) | [![codecov](https://codecov.io/gh/innmind/url-resolver/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/url-resolver) | [Documentation](https://github.com/innmind/url-resolver/#urlresolver) | -| [`url-template`](https://packagist.org/packages/innmind/url-template) | [![Build Status](https://github.com/innmind/urltemplate/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/urltemplate/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/urltemplate/coverage.svg)](https://shepherd.dev/github/innmind/urltemplate) | [![codecov](https://codecov.io/gh/innmind/urltemplate/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/urltemplate) | [Documentation](https://github.com/innmind/urltemplate/#usage) | -| [`virtual-machine`](https://packagist.org/packages/innmind/virtual-machine) | [![Build Status](https://github.com/innmind/virtual-machine/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/virtual-machine/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/virtual-machine/coverage.svg)](https://shepherd.dev/github/innmind/virtual-machine) | [![codecov](https://codecov.io/gh/innmind/virtual-machine/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/virtual-machine) | [Documentation](https://github.com/innmind/virtual-machine/#usage) | -| [`warden`](https://packagist.org/packages/innmind/warden) | [![Build Status](https://github.com/innmind/warden/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/warden/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/warden/coverage.svg)](https://shepherd.dev/github/innmind/warden) | [![codecov](https://codecov.io/gh/innmind/warden/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/warden) | [Documentation](https://github.com/innmind/warden/#usage) | -| [`xml`](https://packagist.org/packages/innmind/xml) | [![Build Status](https://github.com/innmind/xml/workflows/CI/badge.svg?branch=master)](https://github.com/innmind/xml/actions?query=workflow%3ACI) | [![Type Coverage](https://shepherd.dev/github/innmind/xml/coverage.svg)](https://shepherd.dev/github/innmind/xml) | [![codecov](https://codecov.io/gh/innmind/xml/branch/master/graph/badge.svg)](https://codecov.io/gh/innmind/xml) | [Documentation](https://github.com/innmind/xml/#usage) | diff --git a/vision.md b/vision.md deleted file mode 100644 index 61587b2..0000000 --- a/vision.md +++ /dev/null @@ -1,9 +0,0 @@ -# Vision - -The ultimate goal of this organization is to verify [Antonio Damasio](https://en.wikipedia.org/wiki/Antonio_Damasio)'s theory of consciousness. - -Building each level of abstraction will become more and more complex. To keep the level of complexity manageable every [package](packages.md) follows the same [principles](design_choices.md), this means _reinventing the wheel_ even though a similar package already exists in the PHP ecosystem. - -Since this goal may not be reachable, packages are designed in a way so that they can be used in any project. - -> **Note** you can read more about [this (in french)](https://github.com/Innmind/Research-N-Development/blob/master/Papers/Sur%20la%20conscience.md)