diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..6b9135c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,144 @@ + +# This file is part of Phalcon. +# +# (c) Phalcon Team +# +# For the full copyright and license information, please view +# the LICENSE file that was distributed with this source code. + +name: REST API v6 CI +on: + push: + paths-ignore: + - '**.md' + - '**.txt' + pull_request: + workflow_dispatch: + +env: + fail-fast: true + + # PHP extensions required by Composer + EXTENSIONS: json, mbstring, pcov, pdo, pdo_mysql + +permissions: { } +jobs: + + # PHP CodeSniffer inspection + phpcs: + name: "Quality gate" + if: "!contains(github.event.head_commit.message, 'ci skip')" + + permissions: + contents: read + + runs-on: ubuntu-22.04 + + strategy: + fail-fast: true + matrix: + php: + - '8.2' + - '8.3' + - '8.4' + steps: + - uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@2.35.4 + with: + php-version: ${{ matrix.php }} + extensions: ${{ env.EXTENSIONS }} + tools: pecl + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: "Install development dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + composer-options: "--prefer-dist" + + - name: "PHPCS" + run: | + composer cs + + - name: "PHPStan" + run: | + composer analyze + + unit-tests: + needs: phpcs + + permissions: + contents: read # to fetch code (actions/checkout) + + name: Unit tests / PHP-${{ matrix.php }} + runs-on: ubuntu-22.04 + + strategy: + matrix: + php: + - '8.2' + - '8.3' + - '8.4' + + services: + mariadb: + image: mariadb:10.6 + ports: + - "3306:3306" + env: + MYSQL_ROOT_PASSWORD: secret + MYSQL_USER: phalcon + MYSQL_DATABASE: phalcon + MYSQL_PASSWORD: secret + + steps: + - uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@2.35.4 + with: + php-version: ${{ matrix.php }} + extensions: ${{ env.EXTENSIONS }} + tools: pecl + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: "Install development dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + composer-options: "--prefer-dist" + + - name: "Setup Tests" + shell: bash + run: | + cp config/.env.ci .env + mkdir -p tests/_output/coverage/ + + - name: "Run Migrations" + if: always() + run: | + composer migrate + + - name: "Run Unit Tests" + if: always() + run: | + composer test-unit-coverage + + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + projectBaseDir: ./ + args: > + -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }} + -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }} + -Dsonar.sources=src/ + -Dsonar.exclusions=vendor/**,cv/**,tests/** + -Dsonar.sourceEncoding=UTF-8 + -Dsonar.language=php + -Dsonar.tests=tests/ + -Dsonar.php.coverage.reportPaths=tests/_output/cov.xml diff --git a/.gitignore b/.gitignore index 6ffe3aa..958d87e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ -/cache/ -/config/development/ +.cache +.config +.local +.composer +composer.lock +vendor/ +.bash_history +tests/_output +.env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2e4c4a2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.0 + +Under development diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..42cceab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to Phalcon + +Phalcon is an open source project and a volunteer effort. Phalcon welcomes contribution from everyone. Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. + +Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. + +## Contributions + +Contributions to Phalcon should be made in the form of GitHub pull requests. Each pull request will be reviewed by a core contributor (someone with permission to merge patches). Feedback can be provided and potentially changes requested or the pull request will be merged. All contributions should +follow this format, even those from core contributors. + +## Questions & Support + +We use the GitHub issues for tracking bugs and feature requests and have limited bandwidth to address all of them. Thus we only accept bug reports, new feature requests and pull requests in GitHub. Our great community and contributors are happy to help you though! Please use these community resources for obtaining help. + +_Please use the [Documentation](https://phalcon.io/docs) before anything else. You can also use the search feature in our documents to find what you are looking for. If your question is still not answered, there are more options below._ + +* Questions should go to [GitHub Discussions](https://phalcon.io/discussions) +* Come join the Phalcon [Discord](https://phalcon.io/discord) +* Our social network accounts are: + * [Telegram](https://phalcon.io/telegram) + * [Gab](https://phalcon.io/gab) + * [MeWe](https://phalcon.io/mewe) + * [Twitter](https://phalcon.io/t) + * [Facebook](https://phalcon.io/fb) +* If you still believe that what you found is a bug, please + [open an issue](https://github.com/phalcon/phalcon/issues/new) + +Please report bugs when you've exhausted all of the above options. + +## Bug Report Checklist + +* Make sure you are using the latest released version of the composer packages. +* If you have found a bug it is important to add relevant reproducibility information to your issue to allow us to reproduce the bug and fix it quicker. Add a script, small program or repository providing the necessary code to make everyone reproduce the issue reported easily. +* Be sure that information such as OS, Phalcon version and PHP version are part of the bug report + +## Pull Request Checklist + +* Don't submit your pull requests to the `master` branch. Branch from the required branch and, if needed, rebase to the proper branch before submitting your pull request. If it doesn't merge cleanly with master you may be asked to rebase your changes +* Don't put submodule updates in your pull request unless they are to landed commits +* Add tests relevant to the fixed bug or new feature. Test classes should follow the [PSR-12 coding style guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-coding-style-guide.md). + +## Requesting Features + +If you have a change or new feature in mind, please fill out an NFR on GitHub. + + +Thanks! +Phalcon Team diff --git a/README.md b/README.md index 9e62ae1..65bea64 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# rest-api-v6 +# REST API with Phalcon v6 + A REST API developed with Phalcon v6 diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e1380a5 --- /dev/null +++ b/composer.json @@ -0,0 +1,65 @@ +{ + "name": "phalcon/rest-api-v6", + "type": "library", + "description": "Sample REST API application implemented with Phalcon v6", + "keywords": [ + "phalcon", + "framework", + "sample app", + "rest-api", + "rest", + "api" + ], + "homepage": "https://phalcon.io", + "license": "MIT", + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/phalcon/rest-api-v6/graphs/contributors" + } + ], + "require": { + "ext-mbstring": "*", + "ext-pdo": "*", + "phalcon/phalcon": "^6.0.x-dev", + "robmorgan/phinx": "^0.16.10", + "vlucas/phpdotenv": "^5.6" + }, + "require-dev": { + "pds/composer-script-names": "^1.0", + "pds/skeleton": "^1.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.5", + "squizlabs/php_codesniffer": "^3.13" + }, + "autoload": { + "psr-4": { + "Phalcon\\Api\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Phalcon\\Api\\Tests\\": "tests/" + } + }, + "config": { + "preferred-install": "dist", + "sort-packages": true, + "optimize-autoloader": true, + "cache-dir": ".composer" + }, + "replace": { + "symfony/polyfill-php80": "*", + "symfony/polyfill-ctype": "*", + "symfony/polyfill-mbstring": "*" + }, + "scripts": { + "analyze": "vendor/bin/phpstan analyze -c phpstan.neon", + "cs": "vendor/bin/phpcs --standard=phpcs.xml", + "cs-fix": "vendor/bin/phpcbf --standard=phpcs.xml", + "migrate": "vendor/bin/phinx migrate", + "test-unit": "vendor/bin/phpunit -c phpunit.xml.dist --display-all-issues", + "test-unit-coverage": "vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover tests/_output/cov.xml --display-all-issues", + "test-unit-coverage-html": "vendor/bin/phpunit -c phpunit.xml.dist --coverage-html tests/_output/coverage --display-all-issues" + } +} diff --git a/config/.env.ci b/config/.env.ci new file mode 100644 index 0000000..09b4fab --- /dev/null +++ b/config/.env.ci @@ -0,0 +1,16 @@ +PROJECT_NAME="rest" + +# Mariadb +DB_HOST="127.0.0.1" +DB_PORT=3306 +DB_USER="root" +DB_PASS="secret" +DB_NAME="phalcon" +DB_CHARSET="utf8" + +# Redis +DATA_REDIS_HOST="app-cache" +DATA_REDIS_PORT=6379 +DATA_REDIS_NAME="0" + +XDEBUG_MODE=coverage diff --git a/config/.env.example b/config/.env.example new file mode 100644 index 0000000..37fccc8 --- /dev/null +++ b/config/.env.example @@ -0,0 +1,16 @@ +PROJECT_NAME="rest" + +# Mariadb +DB_HOST="app-db" +DB_PORT=3306 +DB_USER="root" +DB_PASS="secret" +DB_NAME="phalcon" +DB_CHARSET= "utf8" + +# Redis +DATA_REDIS_HOST="app-cache" +DATA_REDIS_PORT=6379 +DATA_REDIS_NAME="0" + +XDEBUG_MODE=coverage diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b96a807 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,103 @@ +services: + + app-8.2: + build: + dockerfile: ./resources/docker/Dockerfile + args: + PHP_VERSION: 8.2 + hostname: rest-api-app-82 + container_name: "${PROJECT_NAME}-api-8.2" + tty: true + working_dir: /app + volumes: + - ./:/app + depends_on: + - app-db + - app-cache + networks: + - app-network + environment: + - PHP_IDE_CONFIG=serverName=rest-api-app-82 + - APP_ENV=development + - APP_ENV_ADAPTER=dotenv + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/fpm-ping"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + + app-8.3: + build: + dockerfile: ./resources/docker/Dockerfile + args: + PHP_VERSION: 8.3 + hostname: rest-api-app-83 + container_name: "${PROJECT_NAME}-api-8.3" + tty: true + working_dir: /app + volumes: + - ./:/app + depends_on: + - app-db + - app-cache + networks: + - app-network + environment: + - PHP_IDE_CONFIG=serverName=rest-api-app-83 + - APP_ENV=development + - APP_ENV_ADAPTER=dotenv + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/fpm-ping"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + + app-8.4: + build: + dockerfile: ./resources/docker/Dockerfile + args: + PHP_VERSION: 8.4 + hostname: rest-api-app-84 + container_name: "${PROJECT_NAME}-api-8.4" + tty: true + working_dir: /app + volumes: + - ./:/app + depends_on: + - app-db + - app-cache + networks: + - app-network + environment: + - PHP_IDE_CONFIG=serverName=rest-api-app-83 + - APP_ENV=development + - APP_ENV_ADAPTER=dotenv + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/fpm-ping"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + + app-db: + container_name: "${PROJECT_NAME}-db" + image: mariadb:10.6 + environment: + - MYSQL_ROOT_PASSWORD=secret + - MYSQL_USER=phalcon + - MYSQL_DATABASE=phalcon + - MYSQL_PASSWORD=secret + networks: + - app-network + + app-cache: + container_name: "${PROJECT_NAME}-cache" + image: redis:8-alpine + networks: + - app-network + +networks: + app-network: + driver: bridge diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/phinx.php b/phinx.php new file mode 100644 index 0000000..3c1b2e3 --- /dev/null +++ b/phinx.php @@ -0,0 +1,35 @@ +load(); + +$_ENV = array_merge($envs, $_ENV); + +return [ + 'paths' => [ + 'migrations' => './resources/db/migrations', + 'seeds' => './resources/db/seeds', + ], + 'environments' => [ + 'default_migration_table' => "ut_migrations", + 'default_environment' => 'development', + 'development' => [ + 'adapter' => $_ENV['DB_ADAPTER'] ?? 'mysql', + 'host' => $_ENV['DB_HOST'] ?? '127.0.0.1', + 'name' => $_ENV['DB_NAME'] ?? 'phalcon', + 'user' => $_ENV['DB_USER'] ?? 'root', + 'pass' => $_ENV['DB_PASS'] ?? 'secret', + 'port' => $_ENV['DB_PORT'] ?? 3306, + 'charset' => $_ENV['DB_CHARSET'] ?? 'utf8', + ], + ], + 'version_order' => 'creation', +]; diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..0af51b5 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,12 @@ + + + Phalcon REST API v6 Coding Standards + + + + + + + src + tests/Unit + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..776ccd8 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: max + paths: + - src diff --git a/phpunit.php b/phpunit.php new file mode 100644 index 0000000..9158029 --- /dev/null +++ b/phpunit.php @@ -0,0 +1,25 @@ + +# +# For the full copyright and license information, please view +# the LICENSE file that was distributed with this source code. + + +ini_set('xdebug.mode', 'coverage'); + +error_reporting(E_ALL); + +$autoloader = __DIR__ . '/vendor/autoload.php'; + +if (! file_exists($autoloader)) { + echo "Composer autoloader not found: $autoloader" . PHP_EOL; + echo "Please issue 'composer install' and try again." . PHP_EOL; + exit(1); +} + +require_once $autoloader; diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..174c263 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + ./tests/Unit/ + + + + + ./src + + + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..d20791f --- /dev/null +++ b/public/index.php @@ -0,0 +1,68 @@ + 'get', + 'pattern' => '/', + 'service' => Container::HELLO_SERVICE, + 'responder' => Container::HELLO_RESPONDER_JSON, + ], +]; + +foreach ($routes as $route) { + $method = $route['method']; + $pattern = $route['pattern']; + $serviceName = $route['service']; + $responderName = $route['responder']; + + $application->$method( + $pattern, + function () use ($container, $serviceName, $responderName) { + /** @var DomainInterface $service */ + $service = $container->get($serviceName); + /** @var ResponderInterface $responder */ + $responder = $container->get($responderName); + + $action = new ActionHandler($service, $responder); + $action->__invoke(); + } + ); +} + +$application->finish( + function () use ($container) { + $response = $container->getShared(Container::RESPONSE); + $sender = new ResponseSender(); + + $sender->__invoke($response); + } +); + +$application->notFound( + function () { + echo "404 - Not Found - " . date("Y-m-d H:i:s"); + } +); + + +/** @var string $uri */ +$uri = $_SERVER['REQUEST_URI'] ?? ''; + +$application->handle($uri); diff --git a/resources/db/migrations/.gitkeep b/resources/db/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/db/migrations/20250908190433_add_users_table.php b/resources/db/migrations/20250908190433_add_users_table.php new file mode 100644 index 0000000..3d9c66d --- /dev/null +++ b/resources/db/migrations/20250908190433_add_users_table.php @@ -0,0 +1,57 @@ +table( + 'co_users', + [ + 'id' => 'usr_id', + 'signed' => false, + ] + ); + + $table + ->addColumn( + 'usr_status_flag', + 'boolean', + [ + 'signed' => false, + 'null' => false, + 'default' => 0, + ] + ) + ->addColumn( + 'usr_username', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'usr_password', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addIndex('usr_status_flag') + ->addIndex('usr_username') + ->save() + ; + } + + public function down(): void + { + $this->table('co_users')->drop()->save(); + } +} diff --git a/resources/db/seeds/.gitkeep b/resources/db/seeds/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/docker/Dockerfile b/resources/docker/Dockerfile new file mode 100644 index 0000000..9c1ec1f --- /dev/null +++ b/resources/docker/Dockerfile @@ -0,0 +1,95 @@ +ARG PHP_VERSION=8.4 + +FROM php:${PHP_VERSION}-fpm + +ARG UID=1000 +ARG GID=1000 +ARG USER=phalcon +ARG GROUP=phalcon + +# hadolint ignore=DL3022 +COPY --from=ghcr.io/mlocati/php-extension-installer \ + /usr/bin/install-php-extensions \ + /usr/local/bin/ + +# This is the folder structure from where compose is run from (root) +COPY resources/docker/config/ /config/ + +SHELL [ "/bin/bash", "-o", "pipefail", "-c" ] + +RUN set -eux \ +# Add user and group \ + && getent group "${GROUP}" || groupadd -g "${GID}" "${GROUP}" \ + && id -u "${USER}" &>/dev/null || useradd -l -u "${UID}" -g "${GID}" -d /app "${USER}" \ + && usermod -s /bin/bash "${USER}" \ + && mkdir -p /app /app/public /app/storage /run/nginx /run/supervisor \ + && mv /config/public/index.php /app/public/index.php \ + && chown "${USER}":"${GROUP}" /app \ + && chmod 0770 /app \ + && apt update \ +# Install applications \ + && apt install --no-install-recommends --no-install-suggests -q -y \ + apt-utils \ + git \ + nano \ + nginx \ + ssh \ + supervisor \ + unzip \ + zip \ +# Install base extensions \ + && install-php-extensions \ + igbinary \ + pcov \ + pdo_mysql \ + redis \ + xdebug \ + xsl \ + zip \ +# Configure PHP-FPM and PHP \ + && mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \ + && mv /config/php/fpm-pool.conf /usr/local/etc/php-fpm.d/www.conf \ + && mv /config/php/php.ini /usr/local/etc/php/conf.d/80-custom.ini \ + && sed -i -e "s/group = www-data/group = ${GROUP}/" /usr/local/etc/php-fpm.d/www.conf \ + && sed -i -e "s/listen.group = www-data/listen.group = ${GROUP}/" /usr/local/etc/php-fpm.d/www.conf \ +# Configure nginx \ + && rm -f /etc/nginx/sites-enabled/default \ + && mv /config/nginx/nginx.conf /etc/nginx/nginx.conf \ + && mv /config/nginx/default.conf /etc/nginx/conf.d/default.conf \ + && sed -i -e "s/user nobody nobody;/user www-data ${GROUP};/" /etc/nginx/nginx.conf \ +# Configure supervisord \ + && mv /config/supervisor/supervisor.conf /etc/supervisor/supervisord.conf \ +# Configure bashrc and permissions \ + && echo "" >> /etc/bash.bashrc \ + && cat /config/bashrc >> /etc/bash.bashrc \ + && rm -fR /config \ + && chown -R ${USER}:${GROUP} /app \ + && chgrp -R ${GROUP} /var/log/nginx /run/nginx /run/supervisor \ + && chmod -R 0775 /app /var/log/nginx /run/nginx /run/supervisor \ +# Cleanup \ + && apt autoremove --purge -y \ + && apt autoclean -y \ + && apt clean -y \ + && rm -rf /tmp/* /var/tmp/* \ + && find /var/cache/apt/archives /var/lib/apt/lists -not -name lock -type f -delete \ + && find /var/cache -type f -delete \ + && find /var/log -type f -delete + +# hadolint ignore=DL3022 +COPY --from=composer/composer:2 \ + --chown=${USER}:${GROUP} \ + --chmod=0770 \ + /usr/bin/composer \ + /usr/local/bin/composer + +# Configure a healthcheck to validate that everything is up&running +HEALTHCHECK --timeout=10s CMD curl --silent --fail http://127.0.0.1:80/fpm-ping || exit 1 + +WORKDIR /app + +USER ${USER} + +EXPOSE 80 + +# Let supervisord start nginx & php-fpm +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/resources/docker/config/bashrc b/resources/docker/config/bashrc new file mode 100644 index 0000000..6f95e2e --- /dev/null +++ b/resources/docker/config/bashrc @@ -0,0 +1,75 @@ +#!/bin/bash + +# Easier navigation: .., ..., ...., ....., ~ and - +alias ..="cd .." +alias ...="cd ../.." +alias ....="cd ../../.." +alias .....="cd ../../../.." +alias ~="cd ~" # `cd` is probably faster to type though +alias -- -="cd -" + +# Shortcuts +alias g="git" +alias h="history" + +# Detect which `ls` flavor is in use +if ls --color > /dev/null 2>&1; then # GNU `ls` + colorflag="--color" +else # OS X `ls` + colorflag="-G" +fi + +# List all files colorized in long format +# shellcheck disable=SC2139 +alias l="ls -lF ${colorflag}" + +# List all files colorized in long format, including dot files +# shellcheck disable=SC2139 +alias la="ls -laF ${colorflag}" + +# List only directories +# shellcheck disable=SC2139 +alias lsd="ls -lF ${colorflag} | grep --color=never '^d'" + +# See: https://superuser.com/a/656746/280737 +alias ll='LC_ALL="C.UTF-8" ls -alF' + +# Always use color output for `ls` +# shellcheck disable=SC2139 +alias ls="command ls ${colorflag}" +export LS_COLORS='no=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.bz2=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.avi=01;35:*.fli=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.ogg=01;35:*.mp3=01;35:*.wav=01;35:' + +# Always enable colored `grep` output +alias grep='grep --color=auto ' + +# Enable aliases to be sudo’ed +alias sudo='sudo ' + +# Get week number +alias week='date +%V' + +# Stopwatch +alias timer='echo "Timer started. Stop with Ctrl-D." && date && time cat && date' + +# Canonical hex dump; some systems have this symlinked +command -v hd > /dev/null || alias hd="hexdump -C" + +# vhosts +alias hosts='sudo nano /etc/hosts' + +# copy working directory +alias cwd='pwd | tr -d "\r\n" | xclip -selection clipboard' + +# copy file interactive +alias cp='cp -i' + +# move file interactive +alias mv='mv -i' + +# untar +alias untar='tar xvf' + +PATH=$PATH:./vendor/bin/ + +PHP_VERSION=`php -r 'echo PHP_VERSION;'` +PS1='${debian_chroot:+($debian_chroot)}\u [$PHP_VERSION]@\w\$ ' diff --git a/resources/docker/config/nginx/default.conf b/resources/docker/config/nginx/default.conf new file mode 100644 index 0000000..ee90bc2 --- /dev/null +++ b/resources/docker/config/nginx/default.conf @@ -0,0 +1,56 @@ +# Default server definition +server { + listen [::]:80 default_server; + listen 80 default_server; + server_name _; + + sendfile off; + tcp_nodelay on; + absolute_redirect off; + + root /app/public; + index index.php index.html; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to index.php + try_files $uri $uri/ /index.php$is_args$args; + } + + # Redirect server error pages to the static page /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /var/lib/nginx/html; + } + + # Pass the PHP scripts to PHP-FPM listening on php-fpm.sock + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass 127.0.0.1:9000; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_index index.php; + include fastcgi_params; + } + + # Set the cache-control headers on assets to cache for 5 days + location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml)$ { + expires 5d; + } + + # Deny access to . files, for security + location ~ /\. { + log_not_found off; + deny all; + } + + # Allow fpm ping and status from localhost + location ~ ^/(fpm-status|fpm-ping)$ { + access_log off; +# allow 127.0.0.1; +# deny all; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_pass 127.0.0.1:9000; + } +} diff --git a/resources/docker/config/nginx/nginx.conf b/resources/docker/config/nginx/nginx.conf new file mode 100644 index 0000000..e3952d5 --- /dev/null +++ b/resources/docker/config/nginx/nginx.conf @@ -0,0 +1,53 @@ +worker_processes auto; +user nobody nobody; +pid /run/nginx/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; +} + +http { + sendfile on; + tcp_nopush on; + types_hash_max_size 2048; + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Define custom log format to include response times + log_format main_timed '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + '$request_time $upstream_response_time $pipe $upstream_cache_status'; + + ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, TLSv1, and TLSv1.1 for security + ssl_prefer_server_ciphers on; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + keepalive_timeout 65; + + # Write temporary files to /tmp so they can be created as a non-privileged user + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + # Hide headers that identify the server to prevent information leakage + proxy_hide_header X-Powered-By; + fastcgi_hide_header X-Powered-By; + server_tokens off; + + # Enable gzip compression by default + gzip on; + gzip_proxied any; + # Based on CloudFlare's recommended settings + gzip_types text/richtext text/plain text/css text/x-script text/x-component text/x-java-source text/x-markdown application/javascript application/x-javascript text/javascript text/js image/x-icon image/vnd.microsoft.icon application/x-perl application/x-httpd-cgi text/xml application/xml application/rss+xml application/vnd.api+json application/x-protobuf application/json multipart/bag multipart/mixed application/xhtml+xml font/ttf font/otf font/x-woff image/svg+xml application/vnd.ms-fontobject application/ttf application/x-ttf application/otf application/x-otf application/truetype application/opentype application/x-opentype application/font-woff application/eot application/font application/font-sfnt application/wasm application/javascript-binast application/manifest+json application/ld+json application/graphql+json application/geo+json; + gzip_vary on; + gzip_disable "msie6"; + + # Include server configs + include /etc/nginx/conf.d/*.conf; +} diff --git a/resources/docker/config/php/fpm-pool.conf b/resources/docker/config/php/fpm-pool.conf new file mode 100644 index 0000000..38c5e4b --- /dev/null +++ b/resources/docker/config/php/fpm-pool.conf @@ -0,0 +1,20 @@ +[global] +; Log to stderr +error_log = /dev/stderr + +[www] +catch_workers_output = yes +clear_env = no +decorate_workers_output = no +group = www-data +listen = 127.0.0.1:9000 +listen.group = www-data +listen.owner = www-data +listen.mode = 0660 +ping.path = /fpm-ping +pm = ondemand +pm.max_children = 100 +pm.max_requests = 1000 +pm.process_idle_timeout = 10s; +pm.status_path = /fpm-status +user = www-data diff --git a/resources/docker/config/php/php.ini b/resources/docker/config/php/php.ini new file mode 100644 index 0000000..3be8e2d --- /dev/null +++ b/resources/docker/config/php/php.ini @@ -0,0 +1,13 @@ +[PHP] +date.timezone = UTC +memory_limit = 512M +max_execution_time = 120 +max_input_time = 120 +error_reporting = E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED +xmlrpc_errors = Off +report_memleaks = On +display_errors = On +display_startup_errors = On +log_errors = On +html_errors = Off +xdebug.mode = debug,develop,trace diff --git a/resources/docker/config/public/index.php b/resources/docker/config/public/index.php new file mode 100644 index 0000000..83f1549 --- /dev/null +++ b/resources/docker/config/public/index.php @@ -0,0 +1,3 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Exceptions; + +class InvalidConfigurationArgumentException extends \InvalidArgumentException +{ +} diff --git a/src/Domain/Hello/HelloService.php b/src/Domain/Hello/HelloService.php new file mode 100644 index 0000000..289ddc4 --- /dev/null +++ b/src/Domain/Hello/HelloService.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Hello; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Interfaces\DomainInterface; +use Phalcon\Domain\Payload; + +use function date; + +final class HelloService implements DomainInterface +{ + public function __invoke(): Payload + { + return new Payload( + DomainStatus::SUCCESS, + [ + 'results' => "Hello World!!! - " . date("Y-m-d H:i:s") + ] + ); + } +} diff --git a/src/Domain/Interfaces/ActionInterface.php b/src/Domain/Interfaces/ActionInterface.php new file mode 100644 index 0000000..bb74052 --- /dev/null +++ b/src/Domain/Interfaces/ActionInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Interfaces; + +interface ActionInterface +{ + public function __invoke(): void; +} diff --git a/src/Domain/Interfaces/DomainInterface.php b/src/Domain/Interfaces/DomainInterface.php new file mode 100644 index 0000000..ef9d164 --- /dev/null +++ b/src/Domain/Interfaces/DomainInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Interfaces; + +use Phalcon\Domain\Payload; + +interface DomainInterface +{ + public function __invoke(): Payload; +} diff --git a/src/Domain/Interfaces/ResponderInterface.php b/src/Domain/Interfaces/ResponderInterface.php new file mode 100644 index 0000000..d15b29f --- /dev/null +++ b/src/Domain/Interfaces/ResponderInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Interfaces; + +use Phalcon\Domain\Payload; +use Phalcon\Http\ResponseInterface; + +interface ResponderInterface +{ + public function __invoke(Payload $payload): ResponseInterface; +} diff --git a/src/Domain/Middleware/ResponseSender.php b/src/Domain/Middleware/ResponseSender.php new file mode 100644 index 0000000..616d1c0 --- /dev/null +++ b/src/Domain/Middleware/ResponseSender.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Middleware; + +use Phalcon\Api\Domain\Interfaces\ActionInterface; +use Phalcon\Api\Domain\Interfaces\DomainInterface; +use Phalcon\Api\Domain\Interfaces\ResponderInterface; +use Phalcon\Http\ResponseInterface; + +final readonly class ResponseSender +{ + public function __invoke(ResponseInterface $response): ResponseInterface + { + return $response->send(); + } +} diff --git a/src/Domain/Services/ActionHandler.php b/src/Domain/Services/ActionHandler.php new file mode 100644 index 0000000..6577ec4 --- /dev/null +++ b/src/Domain/Services/ActionHandler.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Services; + +use Phalcon\Api\Domain\Interfaces\ActionInterface; +use Phalcon\Api\Domain\Interfaces\DomainInterface; +use Phalcon\Api\Domain\Interfaces\ResponderInterface; + +final readonly class ActionHandler implements ActionInterface +{ + public function __construct( + private DomainInterface $service, + private ResponderInterface $responder + ) { + } + + public function __invoke(): void + { + $this->responder->__invoke( + $this->service->__invoke() + ); + } +} diff --git a/src/Domain/Services/Container.php b/src/Domain/Services/Container.php new file mode 100644 index 0000000..82d6870 --- /dev/null +++ b/src/Domain/Services/Container.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Services; + +use Phalcon\Api\Action\Hello\GetAction; +use Phalcon\Api\Domain\Hello\HelloService; +use Phalcon\Api\Responder\Hello\HelloTextResponder; +use Phalcon\Api\Responder\HelloJsonResponder; +use Phalcon\Api\Responder\JsonResponder; +use Phalcon\Di\Di; +use Phalcon\Di\Service; +use Phalcon\Filter\FilterFactory; +use Phalcon\Http\Request; +use Phalcon\Http\Response; +use Phalcon\Mvc\Router; + +class Container extends Di +{ + /** @var string */ + public const CACHE = 'cache'; + /** @var string */ + public const CONNECTION = 'connection'; + /** @var string */ + public const FILTER = 'filter'; + /** @var string */ + public const LOGGER = 'logger'; + /** @var string */ + public const REQUEST = 'request'; + /** @var string */ + public const RESPONSE = 'response'; + /** @var string */ + public const ROUTER = 'router'; + + /** + * Hello + */ + public const HELLO_SERVICE = 'hello.service'; + public const HELLO_RESPONDER_JSON = 'hello.responder.json'; + + public function __construct() + { + $this->services = [ + self::FILTER => $this->getServiceFilter(), + self::REQUEST => $this->getServiceSimple(Request::class, true), + self::RESPONSE => $this->getServiceSimple(Response::class, true), + self::ROUTER => $this->getServiceRouter(), + + self::HELLO_SERVICE => $this->getServiceSimple(HelloService::class), + self::HELLO_RESPONDER_JSON => $this->getServiceResponderJson(), + ]; + + parent::__construct(); + } + + /** + * @return Service + */ + private function getServiceFilter(): Service + { + return new Service( + function () { + return (new FilterFactory())->newInstance(); + }, + true + ); + } + + /** + * @return Service + */ + private function getServiceRouter(): Service + { + return new Service( + [ + 'className' => Router::class, + 'arguments' => [ + [ + 'type' => 'parameter', + 'value' => false, + ] + ] + ] + ); + } + + private function getServiceResponderJson(): Service + { + return new Service( + [ + 'className' => JsonResponder::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::RESPONSE, + ] + ] + ] + ); + } + + /** + * @param string $className + * @param bool $isShared + * + * @return Service + */ + private function getServiceSimple( + string $className, + bool $isShared = false + ): Service { + return new Service($className, $isShared); + } +} diff --git a/src/Domain/Services/Env/Adapters/AdapterInterface.php b/src/Domain/Services/Env/Adapters/AdapterInterface.php new file mode 100644 index 0000000..ae377ad --- /dev/null +++ b/src/Domain/Services/Env/Adapters/AdapterInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Services\Env\Adapters; + +interface AdapterInterface +{ + public function load(array $options): array; +} diff --git a/src/Domain/Services/Env/Adapters/DotEnv.php b/src/Domain/Services/Env/Adapters/DotEnv.php new file mode 100644 index 0000000..1d0b64b --- /dev/null +++ b/src/Domain/Services/Env/Adapters/DotEnv.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Services\Env\Adapters; + +use Dotenv\Dotenv as ParentDotEnv; +use Exception; +use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; + +class DotEnv implements AdapterInterface +{ + /** + * @param array $options + * + * @return array + * @throws Exception + */ + public function load(array $options): array + { + /** @var string|null $filePath */ + $filePath = $options['filePath'] ?? null; + if (true === empty($filePath) || true !== file_exists($filePath)) { + throw new InvalidConfigurationArgumentException( + 'The .env file does not exist at the specified path: ' + . (string)$filePath + ); + } + + $dotenv = ParentDotEnv::createImmutable($filePath); + $dotenv->load(); + + return $_ENV; + } +} diff --git a/src/Domain/Services/Env/EnvFactory.php b/src/Domain/Services/Env/EnvFactory.php new file mode 100644 index 0000000..5125ce2 --- /dev/null +++ b/src/Domain/Services/Env/EnvFactory.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Services\Env; + +use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Services\Env\Adapters\AdapterInterface; +use Phalcon\Api\Domain\Services\Env\Adapters\DotEnv; + +class EnvFactory +{ + protected array $instances = []; + + public function newInstance(string $name, mixed ...$parameters): AdapterInterface + { + $adapters = $this->getAdapters(); + if (true !== isset($this->instances[$name])) { + if (true !== isset($adapters[$name])) { + throw new InvalidConfigurationArgumentException( + 'Service ' . $name . ' is not registered' + ); + } + + $definition = $adapters[$name]; + /** @var AdapterInterface $instance */ + $instance = new $definition(...$parameters); + $this->instances[$name] = $instance; + } + + return $this->instances[$name]; + } + + protected function getAdapters(): array + { + return [ + 'dotenv' => DotEnv::class, + ]; + } +} diff --git a/src/Domain/Services/Env/EnvManager.php b/src/Domain/Services/Env/EnvManager.php new file mode 100644 index 0000000..1e76fac --- /dev/null +++ b/src/Domain/Services/Env/EnvManager.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Domain\Services\Env; + +use function array_merge; +use function getenv; + +class EnvManager +{ + private static bool $isLoaded = false; + private static array $settings = []; + + public static function appPath(string $path = ''): string + { + return dirname(__DIR__, 4) + . ($path ? DIRECTORY_SEPARATOR . $path : $path) + ; + } + + public static function get( + string $key, + bool | int | string | null $defaultValue = null + ): bool | int | string | null { + self::load(); + + return self::$settings[$key] ?? $defaultValue; + } + + private static function load(): void + { + if (true !== self::$isLoaded) { + self::$isLoaded = true; + + $envFactory = new EnvFactory(); + $options = self::getOptions(); + $adapter = $options['adapter']; + + $envs = array_merge(getenv(), $_ENV); + $options = $envFactory->newInstance($adapter)->load($options); + $envs = array_merge($envs, $options); + + self::$settings = array_map( + function ($value) { + return match ($value) { + 'true' => true, + 'false' => false, + default => $value, + }; + }, + $envs + ); + } + } + + private static function getOptions(): array + { + $envs = array_merge(getenv(), $_ENV); + $adapter = $envs['APP_ENV_ADAPTER'] ?? 'dotenv'; + $filePath = $envs['APP_ENV_FILE_PATH'] ?? ''; + + return [ + 'adapter' => $adapter, + 'filePath' => $filePath, + ]; + } +} diff --git a/src/Responder/JsonResponder.php b/src/Responder/JsonResponder.php new file mode 100644 index 0000000..9783832 --- /dev/null +++ b/src/Responder/JsonResponder.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Responder; + +use DateTimeImmutable; +use DateTimeZone; +use Phalcon\Api\Domain\Interfaces\ResponderInterface; +use Phalcon\Domain\Payload; +use Phalcon\Http\ResponseInterface; + +use function json_encode; + +final class JsonResponder implements ResponderInterface +{ + public function __construct( + private ResponseInterface $response + ) { + } + + public function __invoke(Payload $payload): ResponseInterface + { + $result = $payload->getResult(); + /** @var string $content */ + $content = $result['results']; + + $timestamp = new DateTimeImmutable('now', new DateTimeZone('UTC')); + $dateTime = $timestamp->format('Y-m-d H:i:s'); + $output = [ + 'data' => [ + $content + ], + 'errors' => [], + 'meta' => [ + 'code' => 200, + 'hash' => '', + 'message' => 'success', + 'timestamp' => $dateTime, + ] + ]; + + $dataErrors = [ + 'data' => $output['data'], + 'errors' => $output['errors'], + ]; + $encoded = json_encode($dataErrors); + $encoded = (false === $encoded) ? '' : $encoded; + $hash = sha1($dateTime . $encoded); + $eTag = sha1($encoded); + + $output['meta']['hash'] = $hash; + + $this + ->response + ->setContentType('application/json') + ->setHeader('E-Tag', $eTag) + ->setJsonContent($output) + ; + + return $this->response; + } +} diff --git a/tests/Fixtures/Domain/Services/Env/.env b/tests/Fixtures/Domain/Services/Env/.env new file mode 100644 index 0000000..b707f0d --- /dev/null +++ b/tests/Fixtures/Domain/Services/Env/.env @@ -0,0 +1,4 @@ +SAMPLE_STRING=sample_value +SAMPLE_INT=1 +SAMPLE_TRUE=true +SAMPLE_FALSE=false diff --git a/tests/Unit/AbstractUnitTestCase.php b/tests/Unit/AbstractUnitTestCase.php new file mode 100644 index 0000000..d248af8 --- /dev/null +++ b/tests/Unit/AbstractUnitTestCase.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit; + +use PHPUnit\Framework\TestCase; + +abstract class AbstractUnitTestCase extends TestCase +{ + /** + * Return a long series of strings to be used as a password + * + * @return string + */ + public function getStrongPassword(): string + { + return substr(base64_encode(random_bytes(512)), 0, 128); + } +} diff --git a/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php new file mode 100644 index 0000000..4171d27 --- /dev/null +++ b/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Services\Env\Adapters; + +use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Services\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Services\Env\EnvFactory; +use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; + +final class DotEnvTest extends AbstractUnitTestCase +{ + private string $envFile; + + protected function setUp(): void + { + $this->envFile = EnvManager::appPath() + . '/tests/Fixtures/Domain/Services/Env/' + ; + } + + public function testLoadSuccess(): void + { + $dotEnv = new DotEnv(); + $options = [ + 'filePath' => $this->envFile, + ]; + + $expected = [ + 'SAMPLE_STRING' => 'sample_value', + 'SAMPLE_INT' => '1', + 'SAMPLE_TRUE' => 'true', + 'SAMPLE_FALSE' => 'false', + ]; + $actual = $dotEnv->load($options); + + $this->assertArrayHasKey('SAMPLE_STRING', $actual); + $this->assertArrayHasKey('SAMPLE_INT', $actual); + $this->assertArrayHasKey('SAMPLE_TRUE', $actual); + $this->assertArrayHasKey('SAMPLE_FALSE', $actual); + + $actualArray = [ + 'SAMPLE_STRING' => $actual['SAMPLE_STRING'], + 'SAMPLE_INT' => $actual['SAMPLE_INT'], + 'SAMPLE_TRUE' => $actual['SAMPLE_TRUE'], + 'SAMPLE_FALSE' => $actual['SAMPLE_FALSE'], + ]; + + $this->assertSame($expected, $actualArray); + } + + public function testLoadExceptionForEmptyFilePath(): void + { + $this->expectException(InvalidConfigurationArgumentException::class); + $this->expectExceptionMessage( + 'The .env file does not exist at the specified path' + ); + + $dotEnv = new DotEnv(); + $options = [ + 'filePath' => '', + ]; + + $dotEnv->load($options); + } + + public function testLoadExceptionForMissingFile(): void + { + $this->expectException(InvalidConfigurationArgumentException::class); + $this->expectExceptionMessage( + 'The .env file does not exist at the specified path' + ); + + $dotEnv = new DotEnv(); + $options = [ + 'filePath' => '/does/not/exist/', + ]; + + $dotEnv->load($options); + } +} diff --git a/tests/Unit/Domain/Services/Env/EnvFactoryTest.php b/tests/Unit/Domain/Services/Env/EnvFactoryTest.php new file mode 100644 index 0000000..94cc4b8 --- /dev/null +++ b/tests/Unit/Domain/Services/Env/EnvFactoryTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Services\Env; + +use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Services\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Services\Env\EnvFactory; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; + +final class EnvFactoryTest extends AbstractUnitTestCase +{ + public function testLoad(): void + { + $factory = new EnvFactory(); + $dotEnv = $factory->newInstance('dotenv'); + + $class = DotEnv::class; + $this->assertInstanceOf($class, $dotEnv); + } + + public function testUnknownService(): void + { + $this->expectException(InvalidConfigurationArgumentException::class); + $this->expectExceptionMessage( + 'Service unknown is not registered' + ); + + $factory = new EnvFactory(); + $factory->newInstance('unknown'); + } +} diff --git a/tests/Unit/Domain/Services/Env/EnvManagerTest.php b/tests/Unit/Domain/Services/Env/EnvManagerTest.php new file mode 100644 index 0000000..f39c493 --- /dev/null +++ b/tests/Unit/Domain/Services/Env/EnvManagerTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Phalcon\Api\Tests\Unit\Domain\Services\Env; + +use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Services\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Services\Env\EnvFactory; +use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Container\Lazy\Env; +use PHPUnit\Framework\Attributes\BackupGlobals; +use ReflectionClass; + +#[BackupGlobals(true)] +final class EnvManagerTest extends AbstractUnitTestCase +{ + protected function setUp(): void + { + $ref = new ReflectionClass(EnvManager::class); + $ref->setStaticPropertyValue('isLoaded', false); + $ref->setStaticPropertyValue('settings', []); + } + + public function testAppPathReturnsRoot(): void + { + $expected = dirname(__DIR__, 5); + $actual = EnvManager::appPath(); + $this->assertSame($expected, $actual); + } + + public function testGetFromDotEnvLoad(): void + { + $_ENV = [ + 'APP_ENV_ADAPTER' => 'dotenv', + 'APP_ENV_FILE_PATH' => EnvManager::appPath() + . '/tests/Fixtures/Domain/Services/Env/' + ]; + + $values = [ + 'SAMPLE_STRING' => 'sample_value', + 'SAMPLE_INT' => '1', + 'SAMPLE_TRUE' => true, + 'SAMPLE_FALSE' => false, + ]; + + $expected = 'default_value'; + $actual = EnvManager::get('NON_EXISTENT', 'default_value'); + $this->assertSame($expected, $actual); + + $expected = $values['SAMPLE_STRING']; + $actual = EnvManager::get('SAMPLE_STRING'); + $this->assertSame($expected, $actual); + + $expected = $values['SAMPLE_INT']; + $actual = EnvManager::get('SAMPLE_INT'); + $this->assertSame($expected, $actual); + + $expected = $values['SAMPLE_TRUE']; + $actual = EnvManager::get('SAMPLE_TRUE'); + $this->assertSame($expected, $actual); + + $expected = $values['SAMPLE_FALSE']; + $actual = EnvManager::get('SAMPLE_FALSE'); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/_output/.gitkeep b/tests/_output/.gitkeep new file mode 100644 index 0000000..e69de29