diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50b321e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor +composer.lock +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b45bb64 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,36 @@ +dist: focal + +arch: + - amd64 + +os: linux + +language: shell + +notifications: + email: + - team@appwrite.io + +services: + - docker + +before_install: + - curl -fsSL https://get.docker.com | sh + - echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json + - mkdir -p $HOME/.docker + - echo '{"experimental":"enabled"}' | sudo tee $HOME/.docker/config.json + - sudo service docker start + - > + if [ ! -z "${DOCKERHUB_PULL_USERNAME:-}" ]; then + echo "${DOCKERHUB_PULL_PASSWORD}" | docker login --username "${DOCKERHUB_PULL_USERNAME}" --password-stdin + fi + - docker --version + +install: + - docker-compose up -d + - sleep 10 + +script: + - docker ps -a + - docker-compose exec web vendor/bin/phpunit --configuration phpunit.xml + - docker-compose exec web vendor/bin/phpcs -p \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..707c377 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +team@appwrite.io.. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ca1aeae --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,119 @@ +# Contributing + +We would ❤️ for you to contribute to Platform and help make it better! We want contributing to Platform to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, new docs as well as updates and tweaks, blog posts, workshops, and more. + +## How to Start? + +If you are worried or don’t know where to start, check out our next section explaining what kind of help we could use and where can you get involved. You can reach out with questions to [Eldad Fux (@eldadfux)](https://twitter.com/eldadfux) or [@appwrite_io](https://twitter.com/appwrite_io) on Twitter, and anyone from the [Appwrite team on Discord](https://discord.gg/GSeTUeA). You can also submit an issue, and a maintainer can guide you! + +## Code of Conduct + +Help us keep framework open and inclusive. Please read and follow our [Code of Conduct](/CODE_OF_CONDUCT.md). + +## Submit a Pull Request 🚀 + +Branch naming convention is as following + +`TYPE-ISSUE_ID-DESCRIPTION` + +example: + +``` +doc-548-submit-a-pull-request-section-to-contribution-guide +``` + +When `TYPE` can be: + +- **feat** - is a new feature +- **doc** - documentation only changes +- **cicd** - changes related to CI/CD system +- **fix** - a bug fix +- **refactor** - code change that neither fixes a bug nor adds a feature + +**All PRs must include a commit message with the changes description!** + +For the initial start, fork the project and use git clone command to download the repository to your computer. A standard procedure for working on an issue would be to: + +1. `git pull`, before creating a new branch, pull the changes from upstream. Your master needs to be up to date. + +``` +$ git pull +``` + +2. Create new branch from `master` like: `doc-548-submit-a-pull-request-section-to-contribution-guide`
+ +``` +$ git checkout -b [name_of_your_new_branch] +``` + +3. Work - commit - repeat ( be sure to be in your branch ) + +4. Before you push your changes, make sure your code follows the `PSR12` coding standards , which is the standard Appwrite follows currently. You can easily do this by running the formatter. + +```bash +composer format +``` + +Now, go a step further by running the linter by the following command to manually fix the issues the formatter wasn't able to fix. + +```bash +composer lint +``` + +This will give you a list of errors for you to rectify , if there is an instance you need more information on the errors being displayed you can pass in additional command line arguments. More list of available arguments can be found [here](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage). A very useful command line argument is `--report=diff`. This will give you the expected changes by the linter for easy fixing of formatting issues. + +```bash +composer lint --report=diff +``` + +5. Push changes to GitHub + +``` +$ git push origin [name_of_your_new_branch] +``` + +6. Submit your changes for review + If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button. +7. Start a Pull Request + Now submit the pull request and click on `Create pull request`. +8. Get a code review approval/reject +9. After approval, merge your PR +10. GitHub will automatically delete the branch after the merge is done. (they can still be restored). + +## Introducing New Features + +We would 💖 you to contribute to Framework, but we would also like to make sure Framework is as great as possible and loyal to its vision and mission statement 🙏. + +For us to find the right balance, please open an issue explaining your ideas before introducing a new pull request. + +This will allow the Framework community to have sufficient discussion about the new feature value and how it fits in the product roadmap and vision. + +This is also important for the Framework lead developers to be able to give technical input and different emphasis regarding the feature design and architecture. Some bigger features might need to go through our [RFC process](https://github.com/appwrite/rfc). + +## Other Ways to Help + +Pull requests are great, but there are many other areas where you can help Framework + +### Blogging & Speaking + +Blogging, speaking about, or creating tutorials about one of Framework's many features is great way to contribute and help our project grow. + +### Presenting at Meetups + +Presenting at meetups and conferences about your Framework projects. Your unique challenges and successes in building things with Framework can provide great speaking material. We’d love to review your talk abstract/CFP, so get in touch with us if you’d like some help! + +### Sending Feedbacks & Reporting Bugs + +Sending feedback is a great way for us to understand your different use cases of Framework better. If you had any issues, bugs, or want to share about your experience, feel free to do so on our GitHub issues page or at our [Discord channel](https://discord.gg/GSeTUeA). + +### Submitting New Ideas + +If you think Framework could use a new feature, please open an issue on our GitHub repository, stating as much information as you can think about your new idea and it's implications. We would also use this issue to gather more information, get more feedback from the community, and have a proper discussion about the new feature. + +### Improving Documentation + +Submitting documentation updates, enhancements, designs, or bug fixes. Spelling or grammar fixes will be very much appreciated. + +### Helping Someone + +Searching for Framework on Discord, GitHub, or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Framework's repo! \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a0d6381 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +FROM composer:2.0 AS step0 + + +ARG TESTING=true + +ENV TESTING=$TESTING + +WORKDIR /usr/local/src/ + +COPY composer.* /usr/local/src/ + +RUN composer update --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist \ + `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` + +FROM php:8.0-cli-alpine as final +LABEL maintainer="team@appwrite.io" + +ENV DEBIAN_FRONTEND=noninteractive \ + PHP_VERSION=8 + +RUN \ + apk add --no-cache --virtual .deps \ + supervisor php$PHP_VERSION php$PHP_VERSION-fpm nginx bash + + +# Nginx Configuration (with self-signed ssl certificates) +COPY ./tests/docker/nginx.conf /etc/nginx/nginx.conf + +# PHP Configuration +RUN mkdir -p /var/run/php +COPY ./tests/docker/www.conf /etc/php/$PHP_VERSION/fpm/pool.d/www.conf + +# Script +COPY ./tests/docker/start /usr/local/bin/start + +# Add PHP Source Code +COPY ./src /usr/share/nginx/html/src +COPY ./tests /usr/share/nginx/html/tests +COPY ./phpunit.xml /usr/share/nginx/html/phpunit.xml +COPY ./phpcs.xml /usr/share/nginx/html/phpcs.xml +COPY --from=step0 /usr/local/src/vendor /usr/share/nginx/html/vendor + +# Supervisord Conf +COPY ./tests/docker/supervisord.conf /etc/supervisord.conf + +# Executables +RUN chmod +x /usr/local/bin/start + +EXPOSE 80 + +WORKDIR /usr/share/nginx/html + +CMD ["/bin/bash", "/usr/local/bin/start"] \ No newline at end of file diff --git a/README.md b/README.md index 02ddf20..3253e9e 100644 --- a/README.md +++ b/README.md @@ -1 +1,97 @@ -# platform \ No newline at end of file +# Utopia Platform + +[![Build Status](https://travis-ci.org/utopia-php/platform.svg?branch=master)](https://travis-ci.com/utopia-php/platform) +![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/platform.svg) +[![Discord](https://img.shields.io/discord/564160730845151244?label=discord)](https://appwrite.io/discord) + +An object oriented way of writing Applications using Utopia libraries + +## Getting Started + +This library contains abstract classes that assist in implementing services and actions for Utopia http framework and CLI. You must implement `Platform`, `Service` and `Action` classes to build your application. + +## Example + +Install using composer + +``` +composer require utopia-php/config +``` + +Implementing a HTTP services using platform. + +```php +// Action + +httpPath = '/hello'; + $this->httpMethod = 'GET'; + $this->inject('response'); + $this->callback(fn ($response) => $this->action($response)); + } + + public function action($response) + { + $response->send('Hello World!'); + } +} + +// service + +use Utopia\Platform\Service; + +class HelloWorldService extends Service +{ + public function __construct() + { + $this->type = Service::TYPE_HTTP; + $this->addAction('hello', new HelloWorldAction()); + } +} + +// Platform + +use Utopia\Platform\Platform; + +class HelloWorldPlatform extends Platform +{ + public function __construct() + { + $this->addService('helloService', new HelloWorldService()); + } +} + +// Using platform to initialize http service + +$platform = new HelloWorldPlatform(); +$platform->init('http'); + +``` + +## System Requirements + +Utopia Framework requires PHP 8.0 or later. We recommend using the latest PHP version whenever possible. + +## Contributing + +All code contributions - including those of people having commit access - must go through a pull request and be approved by a core developer before being merged. This is to ensure a proper review of all the code. + +We truly ❤️ pull requests! If you wish to help, you can learn more about how you can contribute to this project in the [contribution guide](CONTRIBUTING.md). + +## Authors + +**Damodar Lohani** + ++ [https://twitter.com/lohanidamodar](https://twitter.com/lohanidamodar) ++ [https://github.com/lohanidamodar](https://github.com/lohanidamodar) + +## Copyright and license + +The MIT License (MIT) [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d0d031a --- /dev/null +++ b/composer.json @@ -0,0 +1,38 @@ +{ + "name": "utopia-php/platform", + "description": "Light and Fast Platform Library", + "type": "library", + "keywords": ["php","framework", "upf", "utopia", "cache"], + "license": "MIT", + "minimum-stability": "stable", + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + } + ], + "autoload": { + "psr-4": {"Utopia\\Platform\\": "src/Platform"} + }, + "autoload-dev": { + "psr-4": { + "Utopia\\Tests\\": "tests/Platform" + } + }, + "require": { + "php": ">=8.0", + "ext-json": "*", + "ext-redis": "*", + "utopia-php/framework": "0.20.*", + "utopia-php/cli": "0.13.*" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.6" + }, + "scripts": { + "format": "vendor/bin/phpcbf", + "lint": "vendor/bin/phpcs", + "test": "docker-compose up -d && sleep 10 && docker-compose exec web vendor/bin/phpunit --configuration phpunit.xml" + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f7624f2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3' + +services: + web: + build: . + ports: + - "9020:80" + volumes: + - ./tests:/usr/share/nginx/html/tests + - ./src:/usr/share/nginx/html/src \ No newline at end of file diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..1efccde --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,15 @@ + + + + ./src + ./tests + + + + * + + + + * + + \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..de6deb0 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + ./tests/e2e/Client.php + ./tests/ + + + \ No newline at end of file diff --git a/src/Platform/Action.php b/src/Platform/Action.php new file mode 100644 index 0000000..adb398d --- /dev/null +++ b/src/Platform/Action.php @@ -0,0 +1,235 @@ +type = $type; + return $this; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get the value of description + * + * @return string + */ + public function getDesc(): ?string + { + return $this->desc; + } + + /** + * Set the value of description + * + * @param string $description + * + * @return self + */ + public function desc(string $description): self + { + $this->desc = $description; + + return $this; + } + + /** + * Get the value of groups + * + * @return array + */ + public function getGroups(): array + { + return $this->groups; + } + + /** + * Set Groups + * + * @param array $groups + * @return self + */ + public function groups(array $groups): self + { + $this->groups = $groups; + + return $this; + } + + /** + * Get the value of callback + * + * @return mixed + */ + public function getCallback(): mixed + { + return $this->callback; + } + + /** + * Set Callback + * + * @param mixed $callback + * @return self + */ + public function callback(mixed $callback): self + { + $this->callback = $callback; + + return $this; + } + + /** + * Get the value of params + * + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + /** + * Set Param + * + * @param string $key + * @param mixed $default + * @param Validator|callable $validator + * @param string $description + * @param boolean $optional + * @param array $injections + * @return self + */ + public function param(string $key, mixed $default, Validator|callable $validator, string $description = '', bool $optional = false, array $injections = []): self + { + $param = [ + 'default' => $default, + 'validator' => $validator, + 'description' => $description, + 'optional' => $optional, + 'injections' => $injections + ]; + $this->options['param:' . $key] = array_merge($param, ['type' => 'param']); + $this->params[$key] = $param; + + return $this; + } + + /** + * Get the value of injections + * + * @return array + */ + public function getInjections(): array + { + return $this->injections; + } + + /** + * Inject + * + * @param string $injection + * + * @throws Exception + * + * @return self + */ + public function inject(string $injection): self + { + if (array_key_exists($injection, $this->injections)) { + throw new Exception('Injection already declared for ' . $injection); + } + + $this->options['injection:' . $injection] = [ + 'name' => $injection, + 'type' => 'injection' + ]; + $this->injections[] = $injection; + + return $this; + } + + /** + * Get the value of labels + * + * @return array + */ + public function getLabels(): array + { + return $this->labels; + } + + /** + * Add Label + * + * @param string $key + * @param mixed $value + * + * @return self + */ + public function label(string $key, mixed $value): self + { + $this->labels[$key] = $value; + + return $this; + } + + /** + * Get Http Options + * + * @return array + */ + public function getOptions(): array + { + return $this->options; + } +} diff --git a/src/Platform/Platform.php b/src/Platform/Platform.php new file mode 100644 index 0000000..b9ad11f --- /dev/null +++ b/src/Platform/Platform.php @@ -0,0 +1,210 @@ + [], + Service::TYPE_CLI => [], + Service::TYPE_HTTP => [], + Service::TYPE_GRAPHQL => [] + ]; + + protected CLI $cli; + + /** + * Initialize Application + * + * @return void + */ + public function init(string $type): void + { + switch ($type) { + case Service::TYPE_HTTP: + $this->initHttp(); + break; + case Service::TYPE_CLI: + $this->initCLI(); + break; + case Service::TYPE_GRAPHQL: + $this->initGraphQL(); + break; + default: + throw new Exception("Please provide which type of initialization you want to carry out."); + } + } + + /** + * Init HTTP service + * + * @param Service $service + * @return void + */ + protected function initHttp(): void + { + foreach ($this->services[Service::TYPE_HTTP] as $service) { + foreach ($service->getActions() as $action) { + /** @var Action $action */ + switch ($action->getType()) { + case Action::TYPE_INIT: + $hook = App::init(); + break; + case Action::TYPE_ERROR: + $hook = App::error(); + break; + case Action::TYPE_OPTIONS: + $hook = App::options(); + break; + case Action::TYPE_SHUTDOWN: + $hook = App::shutdown(); + break; + case Action::TYPE_DEFAULT: + default: + $hook = App::addRoute($action->getHttpMethod(), $action->getHttpPath()); + break; + } + + $hook + ->groups($action->getGroups()) + ->desc($action->getDesc() ?? ''); + + if ($hook instanceof Route) { + if (!empty($action->getHttpAliasPath())) { + $hook->alias($action->getHttpAliasPath(), $action->getHttpAliasParams()); + } + } + + foreach ($action->getOptions() as $key => $option) { + switch ($option['type']) { + case 'param': + $key = substr($key, stripos($key, ':') + 1); + $hook->param($key, $option['default'], $option['validator'], $option['description'], $option['optional'], $option['injections']); + break; + case 'injection': + $hook->inject($option['name']); + break; + } + } + + if ($hook instanceof Route) { + foreach ($action->getLabels() as $key => $label) { + $hook->label($key, $label); + } + } + + $hook->action($action->getCallback()); + } + } + } + + /** + * Init CLI Services + * + * @return void + */ + protected function initCLI(): void + { + $this->cli ??= new CLI(); + foreach ($this->services[Service::TYPE_CLI] as $service) { + foreach ($service->getActions() as $key => $action) { + $task = $this->cli->task($key); + $task + ->desc($action->getDesc() ?? '') + ->action($action->getCallback()); + + foreach ($action->getParams() as $key => $param) { + $task->param($key, $param['default'], $param['validator'], $param['description'], $param['optional']); + } + + foreach ($action->getLabels() as $key => $label) { + $task->label($key, $label); + } + } + } + } + + /** + * Initialize GraphQL Services + * + * @return void + */ + protected function initGraphQL(): void + { + } + + /** + * Add Service + * + * @param string $key + * @param Service $service + * @return Platform + */ + public function addService(string $key, Service $service): Platform + { + $this->services['all'][$key] = $service; + $this->services[$service->getType()][$key] = $service; + return $this; + } + + /** + * Remove Service + * + * @param string $key + * @return Platform + */ + public function removeService(string $key): Platform + { + unset($this->services[$key]); + return $this; + } + + + /** + * Get Service + * + * @param string $key + * @return Service + */ + public function getService(string $key): Service + { + if (empty($this->services['all'][$key])) { + throw new Exception('Service ' . $key . ' not found'); + } + return $this->services['all'][$key] ?? null; + } + + + /** + * Get Services + * + * @return array + */ + public function getServices(): array + { + return $this->services['all']; + } + + /** + * Get the value of cli + */ + public function getCli(): CLI + { + return $this->cli; + } + + /** + * Set the value of cli + */ + public function setCli(CLI $cli): self + { + $this->cli = $cli; + + return $this; + } +} diff --git a/src/Platform/Scope/HTTP.php b/src/Platform/Scope/HTTP.php new file mode 100644 index 0000000..1f2f669 --- /dev/null +++ b/src/Platform/Scope/HTTP.php @@ -0,0 +1,91 @@ +httpPath = $path; + return $this; + } + + /** + * Set Http Method + * + * @param string $method + * @return self + */ + public function setHttpMethod(string $method): self + { + $this->httpMethod = $method; + return $this; + } + + + /** + * Get httpPath + * + * @return string + */ + public function getHttpPath(): string + { + return $this->httpPath; + } + + /** + * Get the value of httpAliasPath + * + * @return string + */ + public function getHttpAliasPath(): ?string + { + return $this->httpAliasPath; + } + + /** + * Get the value of httpAliasParams + * + * @return array + */ + public function getHttpAliasParams(): array + { + return $this->httpAliasParams; + } + + /** + * Get the value of httpMethod + * + * @return string + */ + public function getHttpMethod(): string + { + return $this->httpMethod; + } + + /** + * Set httpAlias path and params + * + * @param string $path + * @param array $params + * @return self + */ + public function httpAlias(string $path, array $params = []): self + { + $this->httpAliasPath = $path; + $this->httpAliasParams = $params; + + return $this; + } +} diff --git a/src/Platform/Service.php b/src/Platform/Service.php new file mode 100644 index 0000000..91948b6 --- /dev/null +++ b/src/Platform/Service.php @@ -0,0 +1,83 @@ +type = $type; + return $this; + } + + /** + * Get Type + * + * @return string|null + */ + public function getType(): ?string + { + return $this->type; + } + + /** + * Add + * + * @param string $key + * @param Action $action + * @return Service + */ + public function addAction(string $key, Action $action): Service + { + $this->actions[$key] = $action; + return $this; + } + + /** + * Remove Action + * + * @param string $key + * @return Service + */ + public function removeAction(string $key): Service + { + unset($this->actions[$key]); + return $this; + } + + /** + * Get Action + * + * @param string $key + * @return Action|null + */ + public function getAction(string $key): ?Action + { + return $this->actions[$key] ?? null; + } + + /** + * Get Actions + * + * @return array + */ + public function getActions(): array + { + return $this->actions; + } +} diff --git a/tests/Platform/TestActionCLI.php b/tests/Platform/TestActionCLI.php new file mode 100644 index 0000000..42eb564 --- /dev/null +++ b/tests/Platform/TestActionCLI.php @@ -0,0 +1,25 @@ +param('email', null, new Text(0), '') + ->param('list', null, new ArrayList(new Text(256)), 'List of strings') + ->callback(function ($email, $list) { + $this->action($email, $list); + }); + } + + public function action($email, $list) + { + echo $email . '-' . implode('-', $list); + } +} diff --git a/tests/Platform/TestActionChunked.php b/tests/Platform/TestActionChunked.php new file mode 100644 index 0000000..575def4 --- /dev/null +++ b/tests/Platform/TestActionChunked.php @@ -0,0 +1,25 @@ +httpPath = '/chunked'; + $this->httpMethod = 'GET'; + $this->inject('response'); + $this->callback(function ($response) { + $this->action($response); + }); + } + + public function action($response) + { + foreach (["Hello ", "World!"] as $key => $word) { + $response->chunk($word, $key == 1); + } + } +} diff --git a/tests/Platform/TestActionInit.php b/tests/Platform/TestActionInit.php new file mode 100644 index 0000000..4ad61d0 --- /dev/null +++ b/tests/Platform/TestActionInit.php @@ -0,0 +1,25 @@ +type = Action::TYPE_INIT; + $this->groups(['test']); + $this->inject('response'); + $this->callback(function ($response) { + $this->action($response); + }); + } + + public function action(Response $response) + { + + $response->addHeader('x-init', 'init-called'); + } +} diff --git a/tests/Platform/TestActionRedirect.php b/tests/Platform/TestActionRedirect.php new file mode 100644 index 0000000..3066ce9 --- /dev/null +++ b/tests/Platform/TestActionRedirect.php @@ -0,0 +1,23 @@ +httpPath = '/redirect'; + $this->httpMethod = 'GET'; + $this->inject('response'); + $this->callback(function ($response) { + $this->action($response); + }); + } + + public function action($response) + { + $response->redirect('/'); + } +} diff --git a/tests/Platform/TestActionRoot.php b/tests/Platform/TestActionRoot.php new file mode 100644 index 0000000..d50a6cf --- /dev/null +++ b/tests/Platform/TestActionRoot.php @@ -0,0 +1,24 @@ +httpPath = '/'; + $this->groups(['test']); + $this->httpMethod = 'GET'; + $this->inject('response'); + $this->callback(function ($response) { + $this->action($response); + }); + } + + public function action($response) + { + $response->send('Hello World!'); + } +} diff --git a/tests/Platform/TestPlatform.php b/tests/Platform/TestPlatform.php new file mode 100644 index 0000000..259feea --- /dev/null +++ b/tests/Platform/TestPlatform.php @@ -0,0 +1,14 @@ +addService('testService', new TestService()); + $this->addService('testCli', new TestServiceCLI()); + } +} diff --git a/tests/Platform/TestService.php b/tests/Platform/TestService.php new file mode 100644 index 0000000..edb5970 --- /dev/null +++ b/tests/Platform/TestService.php @@ -0,0 +1,17 @@ +type = Service::TYPE_HTTP; + $this->addAction('root', new TestActionRoot()); + $this->addAction('chunked', new TestActionChunked()); + $this->addAction('redirect', new TestActionRedirect()); + $this->addAction('initHook', new TestActionInit()); + } +} diff --git a/tests/Platform/TestServiceCLI.php b/tests/Platform/TestServiceCLI.php new file mode 100644 index 0000000..697260a --- /dev/null +++ b/tests/Platform/TestServiceCLI.php @@ -0,0 +1,15 @@ +type = Service::TYPE_CLI; + $this->addAction('build', new TestActionCLI()); + $this->addAction('build2', new TestActionCLI()); + } +} diff --git a/tests/docker/nginx.conf b/tests/docker/nginx.conf new file mode 100644 index 0000000..32dac62 --- /dev/null +++ b/tests/docker/nginx.conf @@ -0,0 +1,87 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +daemon off; + +events { + worker_connections 2048; + + # multi_accept on; +} + +http { + # Basic Settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 10M; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging Settings + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Virtual Host Configs + server { + listen 80; ## listen for ipv4; this line is default and implied + listen [::]:80 ipv6only=on; ## listen for ipv6 + + root /usr/share/nginx/html/tests/e2e; + index index.php server.php index.html index.htm; + + server_tokens off; + + # Make site accessible from http://localhost/ + #server_name localhost; + + # Disable sendfile as per https://docs.vagrantup.com/v2/synced-folders/virtualbox.html + sendfile off; + + # Add stdout logging + + #error_log /dev/stdout info; + #access_log /dev/stdout; + + access_log off; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to index.html + try_files $uri $uri/ /server.php?q=$uri&$args; + + } + + # redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + #fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_pass 127.0.0.1:9000; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param HTTP_IF_NONE_MATCH $http_if_none_match; + fastcgi_param HTTP_IF_MODIFIED_SINCE $http_if_modified_since; + fastcgi_read_timeout 600; + fastcgi_index server.php; + include fastcgi_params; + } + + # deny access to . files, for security + location ~ /\.(?!well-known).* { + #log_not_found off; + deny all; + } + } +} \ No newline at end of file diff --git a/tests/docker/start b/tests/docker/start new file mode 100644 index 0000000..253121f --- /dev/null +++ b/tests/docker/start @@ -0,0 +1,25 @@ +#!/bin/bash + +export PHP_VERSION=$PHP_VERSION + +chown -Rf www-data.www-data /usr/share/nginx/html/ + +# Function to update the fpm configuration to make the service environment variables available +function setEnvironmentVariable() { + if [ -z "$2" ]; then + echo "Environment variable '$1' not set." + return + fi + + # Check whether variable already exists + if ! grep -q "\[$1\]" /etc/php/$PHP_VERSION/fpm/pool.d/www.conf; then + # Add variable + echo "env[$1] = $2" >> /etc/php/$PHP_VERSION/fpm/pool.d/www.conf + fi + + # Reset variable + # sed -i "s/^env\[$1.*/env[$1] = $2/g" /etc/php/$PHP_VERSION/fpm/pool.d/www.conf +} + +# Start supervisord and services +/usr/bin/supervisord -n -c /etc/supervisord.conf \ No newline at end of file diff --git a/tests/docker/supervisord.conf b/tests/docker/supervisord.conf new file mode 100644 index 0000000..6715ffc --- /dev/null +++ b/tests/docker/supervisord.conf @@ -0,0 +1,45 @@ +[unix_http_server] +file=/tmp/supervisor.sock ; (the path to the socket file) + +[supervisord] +;logfile=/tmp/supervisord.log ; (main log file;default.conf $CWD/supervisord.log) +logfile=/dev/null +logfile_maxbytes=0 ; (max main logfile bytes b4 rotation;default.conf 50MB) +logfile_backups=10 ; (num of main logfile rotation backups;default.conf 10) +loglevel=info ; (log level;default.conf info; others: debug,warn,trace) +pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default.conf supervisord.pid) +nodaemon=false ; (start in foreground if true;default.conf false) +minfds=1024 ; (min. avail startup file descriptors;default.conf 1024) +minprocs=200 ; (min. avail process descriptors;default.conf 200) +user=root ; + +; the below section must remain in the config file for RPC +; (supervisorctl/web interface) to work, additional interfaces may be +; added by defining them in separate rpcinterface: sections +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket + +[program:php8-fpm] +command=php-fpm%(ENV_PHP_VERSION)s -F +autostart=true +autorestart=true +priority=5 +startretries=10 +redirect_stderr=true +redirect_stdout=true + +[program:nginx] +command=nginx +autostart=true +autorestart=true +priority=10 +stdout_events_enabled=true +stderr_events_enabled=true +startretries=10 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes = 0 \ No newline at end of file diff --git a/tests/docker/www.conf b/tests/docker/www.conf new file mode 100644 index 0000000..30dc765 --- /dev/null +++ b/tests/docker/www.conf @@ -0,0 +1,412 @@ +; Start a new pool named 'www'. +; the variable $pool can we used in any directive and will be replaced by the +; pool name ('www' here) +[www] + +; Per pool prefix +; It only applies on the following directives: +; - 'access.log' +; - 'slowlog' +; - 'listen' (unixsocket) +; - 'chroot' +; - 'chdir' +; - 'php_values' +; - 'php_admin_values' +; When not set, the global prefix (or /usr) applies instead. +; Note: This directive can also be relative to the global prefix. +; Default Value: none +;prefix = /path/to/pools/$pool + +; Unix user/group of processes +; Note: The user is mandatory. If the group is not set, the default user's group +; will be used. +user = www-data +group = www-data + +; The address on which to accept FastCGI requests. +; Valid syntaxes are: +; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on +; a specific port; +; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on +; a specific port; +; 'port' - to listen on a TCP socket to all IPv4 addresses on a +; specific port; +; '[::]:port' - to listen on a TCP socket to all addresses +; (IPv6 and IPv4-mapped) on a specific port; +; '/path/to/unix/socket' - to listen on a unix socket. +; Note: This value is mandatory. +; listen = /var/run/php5-fpm.sock +listen = 127.0.0.1:9000 + +; Set listen(2) backlog. +; Default Value: 65535 (-1 on FreeBSD and OpenBSD) +;listen.backlog = 65535 + +; Set permissions for unix socket, if one is used. In Linux, read/write +; permissions must be set in order to allow connections from a web server. Many +; BSD-derived systems allow connections regardless of permissions. +; Default Values: user and group are set as the running user +; mode is set to 0660 +listen.owner = www-data +listen.group = www-data +;listen.mode = 0660 +; When POSIX Access Control Lists are supported you can set them using +; these options, value is a comma separated list of user/group names. +; When set, listen.owner and listen.group are ignored +;listen.acl_users = +;listen.acl_groups = + +; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect. +; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original +; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address +; must be separated by a comma. If this value is left blank, connections will be +; accepted from any ip address. +; Default Value: any +;listen.allowed_clients = 127.0.0.1 + +; Specify the nice(2) priority to apply to the pool processes (only if set) +; The value can vary from -19 (highest priority) to 20 (lower priority) +; Note: - It will only work if the FPM master process is launched as root +; - The pool processes will inherit the master process priority +; unless it specified otherwise +; Default Value: no set +; process.priority = -19 + +; Choose how the process manager will control the number of child processes. +; Possible Values: +; static - a fixed number (pm.max_children) of child processes; +; dynamic - the number of child processes are set dynamically based on the +; following directives. With this process management, there will be +; always at least 1 children. +; pm.max_children - the maximum number of children that can +; be alive at the same time. +; pm.start_servers - the number of children created on startup. +; pm.min_spare_servers - the minimum number of children in 'idle' +; state (waiting to process). If the number +; of 'idle' processes is less than this +; number then some children will be created. +; pm.max_spare_servers - the maximum number of children in 'idle' +; state (waiting to process). If the number +; of 'idle' processes is greater than this +; number then some children will be killed. +; ondemand - no children are created at startup. Children will be forked when +; new requests will connect. The following parameter are used: +; pm.max_children - the maximum number of children that +; can be alive at the same time. +; pm.process_idle_timeout - The number of seconds after which +; an idle process will be killed. +; Note: This value is mandatory. +pm = dynamic + +; The number of child processes to be created when pm is set to 'static' and the +; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. +; This value sets the limit on the number of simultaneous requests that will be +; served. Equivalent to the ApacheMaxClients directive with mpm_prefork. +; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP +; CGI. The below defaults are based on a server without much resources. Don't +; forget to tweak pm.* to fit your needs. +; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' +; Note: This value is mandatory. +pm.max_children = 5 + +; The number of child processes created on startup. +; Note: Used only when pm is set to 'dynamic' +; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2 +pm.start_servers = 2 + +; The desired minimum number of idle server processes. +; Note: Used only when pm is set to 'dynamic' +; Note: Mandatory when pm is set to 'dynamic' +pm.min_spare_servers = 1 + +; The desired maximum number of idle server processes. +; Note: Used only when pm is set to 'dynamic' +; Note: Mandatory when pm is set to 'dynamic' +pm.max_spare_servers = 3 + +; The number of seconds after which an idle process will be killed. +; Note: Used only when pm is set to 'ondemand' +; Default Value: 10s +;pm.process_idle_timeout = 10s; + +; The number of requests each child process should execute before respawning. +; This can be useful to work around memory leaks in 3rd party libraries. For +; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. +; Default Value: 0 +;pm.max_requests = 500 + +; The URI to view the FPM status page. If this value is not set, no URI will be +; recognized as a status page. It shows the following informations: +; pool - the name of the pool; +; process manager - static, dynamic or ondemand; +; start time - the date and time FPM has started; +; start since - number of seconds since FPM has started; +; accepted conn - the number of request accepted by the pool; +; listen queue - the number of request in the queue of pending +; connections (see backlog in listen(2)); +; max listen queue - the maximum number of requests in the queue +; of pending connections since FPM has started; +; listen queue len - the size of the socket queue of pending connections; +; idle processes - the number of idle processes; +; active processes - the number of active processes; +; total processes - the number of idle + active processes; +; max active processes - the maximum number of active processes since FPM +; has started; +; max children reached - number of times, the process limit has been reached, +; when pm tries to start more children (works only for +; pm 'dynamic' and 'ondemand'); +; Value are updated in real time. +; Example output: +; pool: www +; process manager: static +; start time: 01/Jul/2011:17:53:49 +0200 +; start since: 62636 +; accepted conn: 190460 +; listen queue: 0 +; max listen queue: 1 +; listen queue len: 42 +; idle processes: 4 +; active processes: 11 +; total processes: 15 +; max active processes: 12 +; max children reached: 0 +; +; By default the status page output is formatted as text/plain. Passing either +; 'html', 'xml' or 'json' in the query string will return the corresponding +; output syntax. Example: +; http://www.foo.bar/status +; http://www.foo.bar/status?json +; http://www.foo.bar/status?html +; http://www.foo.bar/status?xml +; +; By default the status page only outputs short status. Passing 'full' in the +; query string will also return status for each pool process. +; Example: +; http://www.foo.bar/status?full +; http://www.foo.bar/status?json&full +; http://www.foo.bar/status?html&full +; http://www.foo.bar/status?xml&full +; The Full status returns for each process: +; pid - the PID of the process; +; state - the state of the process (Idle, Running, ...); +; start time - the date and time the process has started; +; start since - the number of seconds since the process has started; +; requests - the number of requests the process has served; +; request duration - the duration in µs of the requests; +; request method - the request method (GET, POST, ...); +; request URI - the request URI with the query string; +; content length - the content length of the request (only with POST); +; user - the user (PHP_AUTH_USER) (or '-' if not set); +; script - the main script called (or '-' if not set); +; last request cpu - the %cpu the last request consumed +; it's always 0 if the process is not in Idle state +; because CPU calculation is done when the request +; processing has terminated; +; last request memory - the max amount of memory the last request consumed +; it's always 0 if the process is not in Idle state +; because memory calculation is done when the request +; processing has terminated; +; If the process is in Idle state, then informations are related to the +; last request the process has served. Otherwise informations are related to +; the current request being served. +; Example output: +; ************************ +; pid: 31330 +; state: Running +; start time: 01/Jul/2011:17:53:49 +0200 +; start since: 63087 +; requests: 12808 +; request duration: 1250261 +; request method: GET +; request URI: /test_mem.php?N=10000 +; content length: 0 +; user: - +; script: /home/fat/web/docs/php/test_mem.php +; last request cpu: 0.00 +; last request memory: 0 +; +; Note: There is a real-time FPM status monitoring sample web page available +; It's available in: /usr/share/php5/fpm/status.html +; +; Note: The value must start with a leading slash (/). The value can be +; anything, but it may not be a good idea to use the .php extension or it +; may conflict with a real PHP file. +; Default Value: not set +;pm.status_path = /status + +; The ping URI to call the monitoring page of FPM. If this value is not set, no +; URI will be recognized as a ping page. This could be used to test from outside +; that FPM is alive and responding, or to +; - create a graph of FPM availability (rrd or such); +; - remove a server from a group if it is not responding (load balancing); +; - trigger alerts for the operating team (24/7). +; Note: The value must start with a leading slash (/). The value can be +; anything, but it may not be a good idea to use the .php extension or it +; may conflict with a real PHP file. +; Default Value: not set +;ping.path = /ping + +; This directive may be used to customize the response of a ping request. The +; response is formatted as text/plain with a 200 response code. +; Default Value: pong +;ping.response = pong + +; The access log file +; Default: not set +;access.log = log/$pool.access.log + +; The access log format. +; The following syntax is allowed +; %%: the '%' character +; %C: %CPU used by the request +; it can accept the following format: +; - %{user}C for user CPU only +; - %{system}C for system CPU only +; - %{total}C for user + system CPU (default) +; %d: time taken to serve the request +; it can accept the following format: +; - %{seconds}d (default) +; - %{miliseconds}d +; - %{mili}d +; - %{microseconds}d +; - %{micro}d +; %e: an environment variable (same as $_ENV or $_SERVER) +; it must be associated with embraces to specify the name of the env +; variable. Some exemples: +; - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e +; - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e +; %f: script filename +; %l: content-length of the request (for POST request only) +; %m: request method +; %M: peak of memory allocated by PHP +; it can accept the following format: +; - %{bytes}M (default) +; - %{kilobytes}M +; - %{kilo}M +; - %{megabytes}M +; - %{mega}M +; %n: pool name +; %o: output header +; it must be associated with embraces to specify the name of the header: +; - %{Content-Type}o +; - %{X-Powered-By}o +; - %{Transfert-Encoding}o +; - .... +; %p: PID of the child that serviced the request +; %P: PID of the parent of the child that serviced the request +; %q: the query string +; %Q: the '?' character if query string exists +; %r: the request URI (without the query string, see %q and %Q) +; %R: remote IP address +; %s: status (response code) +; %t: server time the request was received +; it can accept a strftime(3) format: +; %d/%b/%Y:%H:%M:%S %z (default) +; %T: time the log has been written (the request has finished) +; it can accept a strftime(3) format: +; %d/%b/%Y:%H:%M:%S %z (default) +; %u: remote user +; +; Default: "%R - %u %t \"%m %r\" %s" +;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%" + +; The log file for slow requests +; Default Value: not set +; Note: slowlog is mandatory if request_slowlog_timeout is set +;slowlog = log/$pool.log.slow + +; The timeout for serving a single request after which a PHP backtrace will be +; dumped to the 'slowlog' file. A value of '0s' means 'off'. +; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) +; Default Value: 0 +;request_slowlog_timeout = 0 + +; The timeout for serving a single request after which the worker process will +; be killed. This option should be used when the 'max_execution_time' ini option +; does not stop script execution for some reason. A value of '0' means 'off'. +; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) +; Default Value: 0 +;request_terminate_timeout = 0 + +; Set open file descriptor rlimit. +; Default Value: system defined value +;rlimit_files = 1024 + +; Set max core size rlimit. +; Possible Values: 'unlimited' or an integer greater or equal to 0 +; Default Value: system defined value +;rlimit_core = 0 + +; Chroot to this directory at the start. This value must be defined as an +; absolute path. When this value is not set, chroot is not used. +; Note: you can prefix with '$prefix' to chroot to the pool prefix or one +; of its subdirectories. If the pool prefix is not set, the global prefix +; will be used instead. +; Note: chrooting is a great security feature and should be used whenever +; possible. However, all PHP paths will be relative to the chroot +; (error_log, sessions.save_path, ...). +; Default Value: not set +;chroot = + +; Chdir to this directory at the start. +; Note: relative path can be used. +; Default Value: current directory or / when chroot +chdir = / + +; Redirect worker stdout and stderr into main error log. If not set, stdout and +; stderr will be redirected to /dev/null according to FastCGI specs. +; Note: on highloaded environement, this can cause some delay in the page +; process time (several ms). +; Default Value: no +;catch_workers_output = yes + +; Clear environment in FPM workers +; Prevents arbitrary environment variables from reaching FPM worker processes +; by clearing the environment in workers before env vars specified in this +; pool configuration are added. +; Setting to "no" will make all environment variables available to PHP code +; via getenv(), $_ENV and $_SERVER. +; Default Value: yes +;clear_env = no + +; Limits the extensions of the main script FPM will allow to parse. This can +; prevent configuration mistakes on the web server side. You should only limit +; FPM to .php extensions to prevent malicious users to use other extensions to +; exectute php code. +; Note: set an empty value to allow all extensions. +; Default Value: .php +;security.limit_extensions = .php .php3 .php4 .php5 + +; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from +; the current environment. +; Default Value: clean env +;env[HOSTNAME] = $HOSTNAME +;env[PATH] = /usr/local/bin:/usr/bin:/bin +;env[TMP] = /tmp +;env[TMPDIR] = /tmp +;env[TEMP] = /tmp + +; Additional php.ini defines, specific to this pool of workers. These settings +; overwrite the values previously defined in the php.ini. The directives are the +; same as the PHP SAPI: +; php_value/php_flag - you can set classic ini defines which can +; be overwritten from PHP call 'ini_set'. +; php_admin_value/php_admin_flag - these directives won't be overwritten by +; PHP call 'ini_set' +; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no. + +; Defining 'extension' will load the corresponding shared extension from +; extension_dir. Defining 'disable_functions' or 'disable_classes' will not +; overwrite previously defined php.ini values, but will append the new value +; instead. + +; Note: path INI options can be relative and will be expanded with the prefix +; (pool, global or /usr) + +; Default Value: nothing is defined by default except the values in php.ini and +; specified at startup with the -d argument +;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com +;php_flag[display_errors] = off +;php_admin_value[error_log] = /var/log/fpm-php.www.log +;php_admin_flag[log_errors] = on +;php_admin_value[memory_limit] = 32M \ No newline at end of file diff --git a/tests/e2e/CLITest.php b/tests/e2e/CLITest.php new file mode 100644 index 0000000..ccbcca8 --- /dev/null +++ b/tests/e2e/CLITest.php @@ -0,0 +1,37 @@ +setCli($cli); + $platform->init(Service::TYPE_CLI); + + $cli = $platform->getCli(); + $cli->run(); + + $result = ob_get_clean(); + + $this->assertEquals('me@example.com-item1-item2', $result); + $this->assertCount(2, $cli->getTasks()); + } +} diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php new file mode 100644 index 0000000..e71b926 --- /dev/null +++ b/tests/e2e/Client.php @@ -0,0 +1,95 @@ +baseUrl . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); + $responseHeaders = []; + $responseStatus = -1; + $responseType = ''; + $responseBody = ''; + + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $header = explode(':', $header, 2); + + if (count($header) < 2) { // ignore invalid headers + return $len; + } + + $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); + + return $len; + }); + + $responseBody = curl_exec($ch); + $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { + throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus); + } + + curl_close($ch); + + $responseHeaders['status-code'] = $responseStatus; + + if ($responseStatus === 500) { + echo 'Server error(' . $method . ': ' . $path . '. Params: ' . json_encode($params) . '): ' . json_encode($responseBody) . "\n"; + } + + return [ + 'headers' => $responseHeaders, + 'body' => $responseBody + ]; + } +} diff --git a/tests/e2e/HTTPServicesTest.php b/tests/e2e/HTTPServicesTest.php new file mode 100644 index 0000000..fd6ccfe --- /dev/null +++ b/tests/e2e/HTTPServicesTest.php @@ -0,0 +1,52 @@ +client = new Client(); + } + + public function tearDown(): void + { + $this->client = null; + } + + /** + * @var Client $client + */ + protected $client; + + public function testRootAction() + { + $response = $this->client->call(Client::METHOD_GET, '/'); + $this->assertEquals('Hello World!', $response['body']); + } + + public function testChunkedAction() + { + $response = $this->client->call(Client::METHOD_GET, '/chunked'); + $this->assertEquals('Hello World!', $response['body']); + } + + public function testRedirectAction() + { + $response = $this->client->call(Client::METHOD_GET, '/redirect'); + $this->assertEquals('Hello World!', $response['body']); + } + + public function testHook() + { + $response = $this->client->call(Client::METHOD_GET, '/'); + $this->assertEquals('Hello World!', $response['body']); + $this->assertEquals('init-called', $response['headers']['x-init']); + + $response = $this->client->call(Client::METHOD_GET, '/chunked'); + $this->assertEquals('Hello World!', $response['body']); + $this->assertEquals('', ($response['headers']['x-init'] ?? '')); + } +} diff --git a/tests/e2e/server.php b/tests/e2e/server.php new file mode 100644 index 0000000..ab29826 --- /dev/null +++ b/tests/e2e/server.php @@ -0,0 +1,23 @@ +init('http'); + +$request = new Request(); +$response = new Response(); + +$app = new App('UTC'); +$app->run($request, $response);