From ae50a61616f79388427dcc7db89b661c4ba9cdd8 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:18:51 -0500 Subject: [PATCH 001/301] [#.x] - adjusting .gitignore --- .gitignore | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6ffe3aa..8932225 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ -/cache/ -/config/development/ +.cache +.config +.local +composer.lock +vendor/ +.bash_history From c4b40d94a8152dea75afa96eab11da523c5f904a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:18:58 -0500 Subject: [PATCH 002/301] [#.x] - minor changes --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From b61bd850690bd5c28a13f44e933c54403d42b253 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:19:08 -0500 Subject: [PATCH 003/301] [#.x] - added config files --- config/.env.ci | 16 ++++++++++++++++ config/.env.example | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 config/.env.ci create mode 100644 config/.env.example 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 From dc0807dc7d610af4a609753812879de1356281f0 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:19:22 -0500 Subject: [PATCH 004/301] [#.x] - output folder for tests --- tests/_output/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/_output/.gitkeep diff --git a/tests/_output/.gitkeep b/tests/_output/.gitkeep new file mode 100644 index 0000000..e69de29 From 0e118fa4d6bc5ba3a21e5ead38595884077cd3dd Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:19:41 -0500 Subject: [PATCH 005/301] [#.x] - added output folder to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8932225..8d1fcc0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ composer.lock vendor/ .bash_history +tests/_output From a38aec346b1328bd43dad5013eb2e07a9b06a3e7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:20:27 -0500 Subject: [PATCH 006/301] [#.x] - added docker-compose file and dockerfile for the dev environment --- docker-compose.yml | 100 ++++++++++++++++++ resources/docker/Dockerfile | 95 +++++++++++++++++ resources/docker/config/bashrc | 76 +++++++++++++ resources/docker/config/nginx/default.conf | 56 ++++++++++ resources/docker/config/nginx/nginx.conf | 53 ++++++++++ resources/docker/config/php/fpm-pool.conf | 20 ++++ resources/docker/config/php/php.ini | 12 +++ resources/docker/config/public/index.php | 3 + .../docker/config/supervisor/supervisor.conf | 23 ++++ 9 files changed, 438 insertions(+) create mode 100644 docker-compose.yml create mode 100644 resources/docker/Dockerfile create mode 100644 resources/docker/config/bashrc create mode 100644 resources/docker/config/nginx/default.conf create mode 100644 resources/docker/config/nginx/nginx.conf create mode 100644 resources/docker/config/php/fpm-pool.conf create mode 100644 resources/docker/config/php/php.ini create mode 100644 resources/docker/config/public/index.php create mode 100644 resources/docker/config/supervisor/supervisor.conf diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a5468e2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,100 @@ +services: + + app-8.2: + build: + dockerfile: ./resources/docker/Dockerfile + args: + PHP_VERSION: 8.2 + hostname: rest-api-app-8.2 + container_name: "${PROJECT_NAME}-api-8.2" + tty: true + working_dir: /app + volumes: + - ./:/app + depends_on: + - app-db + - app-cache + networks: + - app-network + environment: + - 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-8.3 + container_name: "${PROJECT_NAME}-api-8.3" + tty: true + working_dir: /app + volumes: + - ./:/app + depends_on: + - app-db + - app-cache + networks: + - app-network + environment: + - 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-8.4 + container_name: "${PROJECT_NAME}-api-8.4" + tty: true + working_dir: /app + volumes: + - ./:/app + depends_on: + - app-db + - app-cache + networks: + - app-network + environment: + - 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:7-alpine + networks: + - app-network + +networks: + app-network: + driver: bridge 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..24a49c0 --- /dev/null +++ b/resources/docker/config/bashrc @@ -0,0 +1,76 @@ +#!/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' + +# Zephir related +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..1a1be6f --- /dev/null +++ b/resources/docker/config/php/php.ini @@ -0,0 +1,12 @@ +[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 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 @@ + Date: Mon, 1 Sep 2025 13:20:52 -0500 Subject: [PATCH 007/301] [#.x] - added changelog and contributing guidelines --- CHANGELOG.md | 3 +++ CONTRIBUTING.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md 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 From 44b4dfbb9aaaaf55af3c7eccf278ae4fd8677339 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:21:14 -0500 Subject: [PATCH 008/301] [#.x] - ignoring .env --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8d1fcc0..e405cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ composer.lock vendor/ .bash_history tests/_output +.env From 4515895dbe665f237562e54d0a9797a2084ff540 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:21:54 -0500 Subject: [PATCH 009/301] [#.x] - initial folder structure --- bin/.gitkeep | 0 docs/.gitkeep | 0 public/index.php | 3 +++ src/.gitkeep | 0 4 files changed, 3 insertions(+) create mode 100644 bin/.gitkeep create mode 100644 docs/.gitkeep create mode 100644 public/index.php create mode 100644 src/.gitkeep diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..174d7fd --- /dev/null +++ b/public/index.php @@ -0,0 +1,3 @@ + Date: Mon, 1 Sep 2025 13:22:02 -0500 Subject: [PATCH 010/301] [#.x] - composer with necessary packages --- composer.json | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 composer.json diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f97b7ef --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "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/skeleton": "^1.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.5", + "squizlabs/php_codesniffer": "^3.13" + }, + "config": { + "preferred-install": "dist", + "sort-packages": true, + "optimize-autoloader": true + }, + "replace": { + "symfony/polyfill-php80": "*", + "symfony/polyfill-ctype": "*", + "symfony/polyfill-mbstring": "*" + } +} From 64205f366c0dbfe4cd0fb9b4de83132692e735ea Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:24:36 -0500 Subject: [PATCH 011/301] [#.x] - hello world from index --- public/index.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/index.php b/public/index.php index 174d7fd..86e84a5 100644 --- a/public/index.php +++ b/public/index.php @@ -1,3 +1,5 @@ Date: Mon, 1 Sep 2025 13:39:11 -0500 Subject: [PATCH 012/301] [#.x] - adding cache folder for composer and adjusting gitignore --- .gitignore | 1 + composer.json | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e405cb2..958d87e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .cache .config .local +.composer composer.lock vendor/ .bash_history diff --git a/composer.json b/composer.json index f97b7ef..70b48fa 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "vlucas/phpdotenv": "^5.6" }, "require-dev": { + "pds/composer-script-names": "^1.0", "pds/skeleton": "^1.0", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^11.5", @@ -34,11 +35,18 @@ "config": { "preferred-install": "dist", "sort-packages": true, - "optimize-autoloader": 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", + "test-unit": "vendor/bin/phpunit -c phpunit.xml.dist" } } From c6bde68a8ef628dc133d5eeeea286054f051415c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:39:23 -0500 Subject: [PATCH 013/301] [#.x] - added codesniffer config --- phpcs.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 phpcs.xml 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 + From c8d58280df6d9c7288bf0fd8f3def4db31e1a3a5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:39:29 -0500 Subject: [PATCH 014/301] [#.x] - added phpstan config --- phpstan.neon | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 phpstan.neon 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 From fdf3b7c6fb2f51da1a2e3de9861724041fc54c46 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:39:42 -0500 Subject: [PATCH 015/301] [#.x] - added phpunit config --- phpunit.php | 25 +++++++++++++++++++++++++ phpunit.xml.dist | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 phpunit.php create mode 100644 phpunit.xml.dist 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 + + + From 5551751ffa1bbd61c4ed57f5e069e08473b566cb Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:43:41 -0500 Subject: [PATCH 016/301] [#.x] - added github actions setup --- .github/workflows/main.yml | 144 +++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .github/workflows/main.yml 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 From e08f51c8be369b971cda8e754153503ae48da17c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 1 Sep 2025 13:43:51 -0500 Subject: [PATCH 017/301] [#.x] - added more commands to composer --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 70b48fa..c064535 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,9 @@ "analyze": "vendor/bin/phpstan analyze -c phpstan.neon", "cs": "vendor/bin/phpcs --standard=phpcs.xml", "cs-fix": "vendor/bin/phpcbf --standard=phpcs.xml", - "test-unit": "vendor/bin/phpunit -c phpunit.xml.dist" + "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" } } From ceff2984facca14a982a112cbe0ab30cc10bf656 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 4 Sep 2025 17:01:40 -0500 Subject: [PATCH 018/301] [#.x] - correcting path --- resources/docker/config/bashrc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/docker/config/bashrc b/resources/docker/config/bashrc index 24a49c0..6f95e2e 100644 --- a/resources/docker/config/bashrc +++ b/resources/docker/config/bashrc @@ -69,8 +69,7 @@ alias mv='mv -i' # untar alias untar='tar xvf' -# Zephir related -PATH=$PATH;./vendor/bin/ +PATH=$PATH:./vendor/bin/ PHP_VERSION=`php -r 'echo PHP_VERSION;'` PS1='${debian_chroot:+($debian_chroot)}\u [$PHP_VERSION]@\w\$ ' From fcddd72dcb53fa3300eae5e93f32d7dc7ac05df9 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 4 Sep 2025 17:12:04 -0500 Subject: [PATCH 019/301] [#.x] - minor adjustments to names and ini --- docker-compose.yml | 9 ++++++--- resources/docker/config/php/php.ini | 23 ++++++++++++----------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a5468e2..9aa2f4f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: dockerfile: ./resources/docker/Dockerfile args: PHP_VERSION: 8.2 - hostname: rest-api-app-8.2 + hostname: rest-api-app-82 container_name: "${PROJECT_NAME}-api-8.2" tty: true working_dir: /app @@ -17,6 +17,7 @@ services: networks: - app-network environment: + - PHP_IDE_CONFIG=serverName=rest-api-app-82 - APP_ENV=development - APP_ENV_ADAPTER=dotenv healthcheck: @@ -31,7 +32,7 @@ services: dockerfile: ./resources/docker/Dockerfile args: PHP_VERSION: 8.3 - hostname: rest-api-app-8.3 + hostname: rest-api-app-83 container_name: "${PROJECT_NAME}-api-8.3" tty: true working_dir: /app @@ -43,6 +44,7 @@ services: networks: - app-network environment: + - PHP_IDE_CONFIG=serverName=rest-api-app-83 - APP_ENV=development - APP_ENV_ADAPTER=dotenv healthcheck: @@ -57,7 +59,7 @@ services: dockerfile: ./resources/docker/Dockerfile args: PHP_VERSION: 8.4 - hostname: rest-api-app-8.4 + hostname: rest-api-app-84 container_name: "${PROJECT_NAME}-api-8.4" tty: true working_dir: /app @@ -69,6 +71,7 @@ services: networks: - app-network environment: + - PHP_IDE_CONFIG=serverName=rest-api-app-83 - APP_ENV=development - APP_ENV_ADAPTER=dotenv healthcheck: diff --git a/resources/docker/config/php/php.ini b/resources/docker/config/php/php.ini index 1a1be6f..3be8e2d 100644 --- a/resources/docker/config/php/php.ini +++ b/resources/docker/config/php/php.ini @@ -1,12 +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 +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 From ce9dcd062026afc5ccaa3954d71d185a8ffe42dd Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 8 Sep 2025 13:31:44 -0500 Subject: [PATCH 020/301] [#.x] - upgrading redis to v8 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9aa2f4f..b96a807 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,7 +94,7 @@ services: app-cache: container_name: "${PROJECT_NAME}-cache" - image: redis:7-alpine + image: redis:8-alpine networks: - app-network From 10970c1cc33c19257ac36cd979731cbdebe1f6c6 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 8 Sep 2025 14:00:54 -0500 Subject: [PATCH 021/301] [#.x] - added migrations/seeds folders for phinx --- resources/db/migrations/.gitkeep | 0 resources/db/seeds/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/db/migrations/.gitkeep create mode 100644 resources/db/seeds/.gitkeep diff --git a/resources/db/migrations/.gitkeep b/resources/db/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/db/seeds/.gitkeep b/resources/db/seeds/.gitkeep new file mode 100644 index 0000000..e69de29 From 39f158bebf23fc7f87eef7f86e25d7d8b2a9abf3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 8 Sep 2025 14:10:52 -0500 Subject: [PATCH 022/301] [#.x] - added migration for users table and phinx config --- phinx.php | 35 ++++++++++++ .../20250908190433_add_users_table.php | 57 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 phinx.php create mode 100644 resources/db/migrations/20250908190433_add_users_table.php 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/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(); + } +} From 8c800513f22464f76ef91f2ab2b20158424d7f57 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 9 Sep 2025 08:19:31 -0500 Subject: [PATCH 023/301] [#.x] - added project namespace --- composer.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/composer.json b/composer.json index c064535..e1380a5 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,16 @@ "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, From b3d28a1a6b496eb165baf23c06582e8a0e3e1b5c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 9 Sep 2025 08:19:52 -0500 Subject: [PATCH 024/301] [#.x] - added container for the application --- src/Domain/Services/Container.php | 143 ++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/Domain/Services/Container.php diff --git a/src/Domain/Services/Container.php b/src/Domain/Services/Container.php new file mode 100644 index 0000000..554cf1f --- /dev/null +++ b/src/Domain/Services/Container.php @@ -0,0 +1,143 @@ + + * + * 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\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_ACTION = 'hello.action'; + public const HELLO_SERVICE = 'hello.service'; + public const HELLO_RESPONDER_TEXT = 'hello.responder.text'; + + 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_ACTION => $this->getServiceHelloAction(), + self::HELLO_SERVICE => $this->getServiceSimple(HelloService::class), + self::HELLO_RESPONDER_TEXT => $this->getServiceHelloResponderText(), + ]; + + 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 getServiceHelloAction(): Service + { + return new Service( + [ + 'className' => GetAction::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::HELLO_SERVICE, + ], + [ + 'type' => 'service', + 'name' => self::HELLO_RESPONDER_TEXT, + ] + ] + ] + ); + } + + private function getServiceHelloResponderText(): Service + { + return new Service( + [ + 'className' => HelloTextResponder::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); + } +} From 26bfc05ef481c5acc268f1eb20139023c4010aec Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 9 Sep 2025 08:20:06 -0500 Subject: [PATCH 025/301] [#.x] - bootstrapping the app in index --- public/index.php | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/public/index.php b/public/index.php index 86e84a5..c5507cb 100644 --- a/public/index.php +++ b/public/index.php @@ -2,4 +2,48 @@ declare(strict_types=1); -echo "Hello World!"; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Mvc\Micro; + +require_once dirname(__DIR__) . '/vendor/autoload.php'; + +$container = new Container(); +$application = new Micro($container); + +/** + * Routes + */ +$routes = [ + [ + 'method' => 'get', + 'pattern' => '/', + 'handler' => Container::HELLO_ACTION, + ], +]; + +foreach ($routes as $route) { + $method = $route['method']; + $pattern = $route['pattern']; + $handler = $route['handler']; + + $application->$method( + $pattern, + function () use ($container, $handler) { + $action = $container->get($handler); + + $action(); + } + ); +} + +$application->notFound( + function () { + echo "404 - Not Found - " . date("Y-m-d H:i:s"); + } +); + + +/** @var string $uri */ +$uri = $_SERVER['REQUEST_URI'] ?? ''; + +$application->handle($uri); From 74bec607a44099fb960fa7d44a3d9f565613990c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 9 Sep 2025 08:20:19 -0500 Subject: [PATCH 026/301] [#.x] - added action/domain/responder classes --- src/Action/Hello/GetAction.php | 38 ++++++++++++++++++++++ src/Domain/Hello/HelloService.php | 24 ++++++++++++++ src/Responder/Hello/HelloTextResponder.php | 31 ++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 src/Action/Hello/GetAction.php create mode 100644 src/Domain/Hello/HelloService.php create mode 100644 src/Responder/Hello/HelloTextResponder.php diff --git a/src/Action/Hello/GetAction.php b/src/Action/Hello/GetAction.php new file mode 100644 index 0000000..473645a --- /dev/null +++ b/src/Action/Hello/GetAction.php @@ -0,0 +1,38 @@ + + * + * 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\Action\Hello; + +use Phalcon\Api\Domain\Hello\HelloService; +use Phalcon\Api\Responder\Hello\HelloTextResponder; +use Phalcon\Http\Response; + +final class GetAction +{ + public function __construct( + private readonly HelloService $service, + private readonly HelloTextResponder $responder + ) { + } + + public function __invoke(): void + { + $service = $this->service; + $responder = $this->responder; + + $serviceOutput = $service(); + $outputResponse = $responder($serviceOutput); + + $outputResponse->send(); + } +} diff --git a/src/Domain/Hello/HelloService.php b/src/Domain/Hello/HelloService.php new file mode 100644 index 0000000..7b66a1f --- /dev/null +++ b/src/Domain/Hello/HelloService.php @@ -0,0 +1,24 @@ + + * + * 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 function date; + +final class HelloService +{ + public function __invoke(): string + { + return "Hello World!!! - " . date("Y-m-d H:i:s"); + } +} diff --git a/src/Responder/Hello/HelloTextResponder.php b/src/Responder/Hello/HelloTextResponder.php new file mode 100644 index 0000000..2ee5a54 --- /dev/null +++ b/src/Responder/Hello/HelloTextResponder.php @@ -0,0 +1,31 @@ + + * + * 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\Hello; + +use Phalcon\Http\Response; + +final class HelloTextResponder +{ + public function __construct( + private Response $response + ) { + } + + public function __invoke(string $payload): Response + { + $this->response->setContent($payload); + + return $this->response; + } +} From cf4ebc59e5a225546bed8303048f63dc03c9c601 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 9 Sep 2025 08:20:51 -0500 Subject: [PATCH 027/301] [#.x] - removing gitkeep --- src/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/.gitkeep diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 From 1c16c209a7e97b33c2ce7a767fea3788f3943859 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 13 Sep 2025 14:40:21 -0500 Subject: [PATCH 028/301] [#.x] - added exception classes --- src/Action/Hello/GetAction.php | 38 ------------------- .../InvalidConfigurationArgumentException.php | 18 +++++++++ src/Responder/Hello/HelloTextResponder.php | 31 --------------- 3 files changed, 18 insertions(+), 69 deletions(-) delete mode 100644 src/Action/Hello/GetAction.php create mode 100644 src/Domain/Exceptions/InvalidConfigurationArgumentException.php delete mode 100644 src/Responder/Hello/HelloTextResponder.php diff --git a/src/Action/Hello/GetAction.php b/src/Action/Hello/GetAction.php deleted file mode 100644 index 473645a..0000000 --- a/src/Action/Hello/GetAction.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * 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\Action\Hello; - -use Phalcon\Api\Domain\Hello\HelloService; -use Phalcon\Api\Responder\Hello\HelloTextResponder; -use Phalcon\Http\Response; - -final class GetAction -{ - public function __construct( - private readonly HelloService $service, - private readonly HelloTextResponder $responder - ) { - } - - public function __invoke(): void - { - $service = $this->service; - $responder = $this->responder; - - $serviceOutput = $service(); - $outputResponse = $responder($serviceOutput); - - $outputResponse->send(); - } -} diff --git a/src/Domain/Exceptions/InvalidConfigurationArgumentException.php b/src/Domain/Exceptions/InvalidConfigurationArgumentException.php new file mode 100644 index 0000000..8e9b0cd --- /dev/null +++ b/src/Domain/Exceptions/InvalidConfigurationArgumentException.php @@ -0,0 +1,18 @@ + + * + * 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/Responder/Hello/HelloTextResponder.php b/src/Responder/Hello/HelloTextResponder.php deleted file mode 100644 index 2ee5a54..0000000 --- a/src/Responder/Hello/HelloTextResponder.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * 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\Hello; - -use Phalcon\Http\Response; - -final class HelloTextResponder -{ - public function __construct( - private Response $response - ) { - } - - public function __invoke(string $payload): Response - { - $this->response->setContent($payload); - - return $this->response; - } -} From a01332327a59965c03152e6fb17f1e08c7439ffc Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 13 Sep 2025 14:40:40 -0500 Subject: [PATCH 029/301] [#.x] - added interfaces for ADR --- src/Domain/Interfaces/ActionInterface.php | 19 +++++++++++++++++ src/Domain/Interfaces/DomainInterface.php | 21 +++++++++++++++++++ src/Domain/Interfaces/ResponderInterface.php | 22 ++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/Domain/Interfaces/ActionInterface.php create mode 100644 src/Domain/Interfaces/DomainInterface.php create mode 100644 src/Domain/Interfaces/ResponderInterface.php 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; +} From 09877daa75a16a90c6ce4b732d8c5f436779768e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 13 Sep 2025 14:41:03 -0500 Subject: [PATCH 030/301] [#.x] - moved actions to actionHandler --- src/Domain/Services/ActionHandler.php | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/Domain/Services/ActionHandler.php 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() + ); + } +} From f8a1179d8c8f304fab430676b1cbac1ced22318b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 13 Sep 2025 14:41:18 -0500 Subject: [PATCH 031/301] [#.x] - new json responder --- src/Responder/JsonResponder.php | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/Responder/JsonResponder.php 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; + } +} From 09412b121e234a0d8f1b0ff22aa574de177ea85b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 13 Sep 2025 14:41:37 -0500 Subject: [PATCH 032/301] [#.x] - response sender for sendin responses back --- src/Domain/Middleware/ResponseSender.php | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Domain/Middleware/ResponseSender.php 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(); + } +} From 681b9128dd5a5663b5f13fb03bebfefbcda61176 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 13 Sep 2025 14:42:03 -0500 Subject: [PATCH 033/301] [#.x] - reworked container and domain using payload --- src/Domain/Hello/HelloService.php | 15 +++- src/Domain/Services/Container.php | 31 ++------ .../Env/Adapters/AdapterInterface.php | 19 +++++ src/Domain/Services/Env/Adapters/DotEnv.php | 44 +++++++++++ src/Domain/Services/Env/EnvFactory.php | 49 ++++++++++++ src/Domain/Services/Env/EnvManager.php | 77 +++++++++++++++++++ 6 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 src/Domain/Services/Env/Adapters/AdapterInterface.php create mode 100644 src/Domain/Services/Env/Adapters/DotEnv.php create mode 100644 src/Domain/Services/Env/EnvFactory.php create mode 100644 src/Domain/Services/Env/EnvManager.php diff --git a/src/Domain/Hello/HelloService.php b/src/Domain/Hello/HelloService.php index 7b66a1f..289ddc4 100644 --- a/src/Domain/Hello/HelloService.php +++ b/src/Domain/Hello/HelloService.php @@ -13,12 +13,21 @@ namespace Phalcon\Api\Domain\Hello; +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Interfaces\DomainInterface; +use Phalcon\Domain\Payload; + use function date; -final class HelloService +final class HelloService implements DomainInterface { - public function __invoke(): string + public function __invoke(): Payload { - return "Hello World!!! - " . date("Y-m-d H:i:s"); + return new Payload( + DomainStatus::SUCCESS, + [ + 'results' => "Hello World!!! - " . date("Y-m-d H:i:s") + ] + ); } } diff --git a/src/Domain/Services/Container.php b/src/Domain/Services/Container.php index 554cf1f..82d6870 100644 --- a/src/Domain/Services/Container.php +++ b/src/Domain/Services/Container.php @@ -16,6 +16,8 @@ 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; @@ -43,9 +45,8 @@ class Container extends Di /** * Hello */ - public const HELLO_ACTION = 'hello.action'; public const HELLO_SERVICE = 'hello.service'; - public const HELLO_RESPONDER_TEXT = 'hello.responder.text'; + public const HELLO_RESPONDER_JSON = 'hello.responder.json'; public function __construct() { @@ -55,9 +56,8 @@ public function __construct() self::RESPONSE => $this->getServiceSimple(Response::class, true), self::ROUTER => $this->getServiceRouter(), - self::HELLO_ACTION => $this->getServiceHelloAction(), self::HELLO_SERVICE => $this->getServiceSimple(HelloService::class), - self::HELLO_RESPONDER_TEXT => $this->getServiceHelloResponderText(), + self::HELLO_RESPONDER_JSON => $this->getServiceResponderJson(), ]; parent::__construct(); @@ -94,30 +94,11 @@ private function getServiceRouter(): Service ); } - private function getServiceHelloAction(): Service + private function getServiceResponderJson(): Service { return new Service( [ - 'className' => GetAction::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::HELLO_SERVICE, - ], - [ - 'type' => 'service', - 'name' => self::HELLO_RESPONDER_TEXT, - ] - ] - ] - ); - } - - private function getServiceHelloResponderText(): Service - { - return new Service( - [ - 'className' => HelloTextResponder::class, + 'className' => JsonResponder::class, 'arguments' => [ [ 'type' => 'service', 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, + ]; + } +} From 243e08114e7b5a91c9ea9b64331f4a7475b99e49 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 13 Sep 2025 14:42:19 -0500 Subject: [PATCH 034/301] [#.x] - adjustments to use the new services --- public/index.php | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/public/index.php b/public/index.php index c5507cb..d20791f 100644 --- a/public/index.php +++ b/public/index.php @@ -2,6 +2,10 @@ declare(strict_types=1); +use Phalcon\Api\Domain\Interfaces\DomainInterface; +use Phalcon\Api\Domain\Interfaces\ResponderInterface; +use Phalcon\Api\Domain\Middleware\ResponseSender; +use Phalcon\Api\Domain\Services\ActionHandler; use Phalcon\Api\Domain\Services\Container; use Phalcon\Mvc\Micro; @@ -15,27 +19,42 @@ */ $routes = [ [ - 'method' => 'get', - 'pattern' => '/', - 'handler' => Container::HELLO_ACTION, + 'method' => 'get', + 'pattern' => '/', + 'service' => Container::HELLO_SERVICE, + 'responder' => Container::HELLO_RESPONDER_JSON, ], ]; foreach ($routes as $route) { - $method = $route['method']; - $pattern = $route['pattern']; - $handler = $route['handler']; + $method = $route['method']; + $pattern = $route['pattern']; + $serviceName = $route['service']; + $responderName = $route['responder']; $application->$method( $pattern, - function () use ($container, $handler) { - $action = $container->get($handler); + function () use ($container, $serviceName, $responderName) { + /** @var DomainInterface $service */ + $service = $container->get($serviceName); + /** @var ResponderInterface $responder */ + $responder = $container->get($responderName); - $action(); + $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"); From 37564350dfa59c8ff31113dc0474681fcfc300e8 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 13 Sep 2025 14:42:29 -0500 Subject: [PATCH 035/301] [#.x] - added tests for env manager --- tests/Unit/AbstractUnitTestCase.php | 29 ++++++ .../Services/Env/Adapters/DotEnvTest.php | 92 +++++++++++++++++++ .../Domain/Services/Env/EnvFactoryTest.php | 42 +++++++++ .../Domain/Services/Env/EnvManagerTest.php | 77 ++++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 tests/Unit/AbstractUnitTestCase.php create mode 100644 tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php create mode 100644 tests/Unit/Domain/Services/Env/EnvFactoryTest.php create mode 100644 tests/Unit/Domain/Services/Env/EnvManagerTest.php 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); + } +} From 181be6bdd24a5d3ce996fc292df0133951e38214 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 15 Sep 2025 10:04:08 -0500 Subject: [PATCH 036/301] [#.x] - added .env for tests --- tests/Fixtures/Domain/Services/Env/.env | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/Fixtures/Domain/Services/Env/.env 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 From 50fd73ad3cf97f884b92db8ad5e94634a7b93760 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Mon, 15 Sep 2025 15:41:58 -0500 Subject: [PATCH 037/301] [#.x] - removed unused use statements --- src/Domain/Services/Container.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Domain/Services/Container.php b/src/Domain/Services/Container.php index 82d6870..b6727bc 100644 --- a/src/Domain/Services/Container.php +++ b/src/Domain/Services/Container.php @@ -13,10 +13,7 @@ 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; From 28c55eb7b1dc4703182a81d71f587fc7795b73c5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 24 Sep 2025 10:34:31 -0500 Subject: [PATCH 038/301] [#.x] - phpstan corrections --- .../Env/Adapters/AdapterInterface.php | 11 ++++++++ src/Domain/Services/Env/Adapters/DotEnv.php | 14 ++++++++-- src/Domain/Services/Env/EnvFactory.php | 6 ++++ src/Domain/Services/Env/EnvManager.php | 28 +++++++++++++++++++ src/Domain/Services/Env/EnvManagerTypes.php | 25 +++++++++++++++++ 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/Domain/Services/Env/EnvManagerTypes.php diff --git a/src/Domain/Services/Env/Adapters/AdapterInterface.php b/src/Domain/Services/Env/Adapters/AdapterInterface.php index ae377ad..b15db06 100644 --- a/src/Domain/Services/Env/Adapters/AdapterInterface.php +++ b/src/Domain/Services/Env/Adapters/AdapterInterface.php @@ -13,7 +13,18 @@ namespace Phalcon\Api\Domain\Services\Env\Adapters; +use Phalcon\Api\Domain\Services\Env\EnvManagerTypes; + +/** + * @phpstan-import-type TDotEnvOptions from EnvManagerTypes + * @phpstan-import-type TSettings from EnvManagerTypes + */ interface AdapterInterface { + /** + * @param TDotEnvOptions $options + * + * @return TSettings + */ public function load(array $options): array; } diff --git a/src/Domain/Services/Env/Adapters/DotEnv.php b/src/Domain/Services/Env/Adapters/DotEnv.php index 1d0b64b..bd234fe 100644 --- a/src/Domain/Services/Env/Adapters/DotEnv.php +++ b/src/Domain/Services/Env/Adapters/DotEnv.php @@ -16,13 +16,18 @@ use Dotenv\Dotenv as ParentDotEnv; use Exception; use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Services\Env\EnvManagerTypes; +/** + * @phpstan-import-type TDotEnvOptions from EnvManagerTypes + * @phpstan-import-type TSettings from EnvManagerTypes + */ class DotEnv implements AdapterInterface { /** - * @param array $options + * @param TDotEnvOptions $options * - * @return array + * @return TSettings * @throws Exception */ public function load(array $options): array @@ -39,6 +44,9 @@ public function load(array $options): array $dotenv = ParentDotEnv::createImmutable($filePath); $dotenv->load(); - return $_ENV; + /** @var TSettings $env */ + $env = $_ENV; + + return $env; } } diff --git a/src/Domain/Services/Env/EnvFactory.php b/src/Domain/Services/Env/EnvFactory.php index 5125ce2..0fe58ab 100644 --- a/src/Domain/Services/Env/EnvFactory.php +++ b/src/Domain/Services/Env/EnvFactory.php @@ -19,6 +19,9 @@ class EnvFactory { + /** + * @var array + */ protected array $instances = []; public function newInstance(string $name, mixed ...$parameters): AdapterInterface @@ -40,6 +43,9 @@ public function newInstance(string $name, mixed ...$parameters): AdapterInterfac return $this->instances[$name]; } + /** + * @return array + */ protected function getAdapters(): array { return [ diff --git a/src/Domain/Services/Env/EnvManager.php b/src/Domain/Services/Env/EnvManager.php index 1e76fac..25c7acf 100644 --- a/src/Domain/Services/Env/EnvManager.php +++ b/src/Domain/Services/Env/EnvManager.php @@ -16,11 +16,23 @@ use function array_merge; use function getenv; +/** + * @phpstan-import-type TSettings from EnvManagerTypes + */ class EnvManager { private static bool $isLoaded = false; + + /** + * @var TSettings + */ private static array $settings = []; + /** + * @param string $path + * + * @return string + */ public static function appPath(string $path = ''): string { return dirname(__DIR__, 4) @@ -28,6 +40,12 @@ public static function appPath(string $path = ''): string ; } + /** + * @param string $key + * @param bool|int|string|null $defaultValue + * + * @return bool|int|string|null + */ public static function get( string $key, bool | int | string | null $defaultValue = null @@ -37,6 +55,9 @@ public static function get( return self::$settings[$key] ?? $defaultValue; } + /** + * @return void + */ private static function load(): void { if (true !== self::$isLoaded) { @@ -47,7 +68,9 @@ private static function load(): void $adapter = $options['adapter']; $envs = array_merge(getenv(), $_ENV); + /** @var TSettings $options */ $options = $envFactory->newInstance($adapter)->load($options); + /** @var TSettings $envs */ $envs = array_merge($envs, $options); self::$settings = array_map( @@ -63,10 +86,15 @@ function ($value) { } } + /** + * @return array + */ private static function getOptions(): array { $envs = array_merge(getenv(), $_ENV); + /** @var string $adapter */ $adapter = $envs['APP_ENV_ADAPTER'] ?? 'dotenv'; + /** @var string $filePath */ $filePath = $envs['APP_ENV_FILE_PATH'] ?? ''; return [ diff --git a/src/Domain/Services/Env/EnvManagerTypes.php b/src/Domain/Services/Env/EnvManagerTypes.php new file mode 100644 index 0000000..7819113 --- /dev/null +++ b/src/Domain/Services/Env/EnvManagerTypes.php @@ -0,0 +1,25 @@ + + * + * 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; + +/** + * @phpstan-type TDotEnvOptions array{ + * filePath?: string + * } + * @phpstan-type TOptions array + * @phpstan-type TSettings array + */ +final class EnvManagerTypes +{ +} From 2e8d460c1e7f71ac0e1b7125af665a205e82e069 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 24 Sep 2025 11:41:18 -0500 Subject: [PATCH 039/301] [#.x] - adding storage folder --- .../{ResponseSender.php => ResponseSenderMiddleware.php} | 0 storage/logs/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Domain/Middleware/{ResponseSender.php => ResponseSenderMiddleware.php} (100%) create mode 100644 storage/logs/.gitkeep diff --git a/src/Domain/Middleware/ResponseSender.php b/src/Domain/Middleware/ResponseSenderMiddleware.php similarity index 100% rename from src/Domain/Middleware/ResponseSender.php rename to src/Domain/Middleware/ResponseSenderMiddleware.php diff --git a/storage/logs/.gitkeep b/storage/logs/.gitkeep new file mode 100644 index 0000000..e69de29 From 05f976d03ab85012808f81a64472c8e182d400de Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 05:59:05 -0500 Subject: [PATCH 040/301] [#.x] - adding storage/log to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 958d87e..55f49f2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ vendor/ .bash_history tests/_output .env +storage/logs/ From ac0c05379b400fb5e8b5bbf626cc521ba9ee4b6d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 05:59:34 -0500 Subject: [PATCH 041/301] [#.x] - added dates constants --- src/Domain/Constants/Dates.php | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/Domain/Constants/Dates.php diff --git a/src/Domain/Constants/Dates.php b/src/Domain/Constants/Dates.php new file mode 100644 index 0000000..34a181d --- /dev/null +++ b/src/Domain/Constants/Dates.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\Constants; + +use DateTimeImmutable; +use DateTimeZone; +use Exception; + +final class Dates +{ + public const DATE_FORMAT = 'Y-m-d'; + public const DATE_NOW = 'NOW()'; + public const DATE_TIME_FORMAT = 'Y-m-d H:i:s'; + public const DATE_TIME_UTC_FORMAT = 'Y-m-d\\TH:i:sp'; + public const DATE_TIME_ZONE = 'UTC'; + + /** + * @param string $date + * @param string $format + * + * @return string + * @throws Exception + */ + public static function toUTC( + string $date = 'now', + string $format = self::DATE_TIME_UTC_FORMAT + ): string { + return (new DateTimeImmutable( + $date, + new DateTimeZone(self::DATE_TIME_ZONE) + ))->format($format); + } +} From 4878e1fd8a26521ce535da2d0134d339dd1f0c7d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 05:59:46 -0500 Subject: [PATCH 042/301] [#.x] - added http codes enum --- src/Domain/Services/Http/HttpCodesEnum.php | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/Domain/Services/Http/HttpCodesEnum.php diff --git a/src/Domain/Services/Http/HttpCodesEnum.php b/src/Domain/Services/Http/HttpCodesEnum.php new file mode 100644 index 0000000..2b9216f --- /dev/null +++ b/src/Domain/Services/Http/HttpCodesEnum.php @@ -0,0 +1,68 @@ + + * + * 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\Http; + +/** + * Enumeration for Http Codes. These are not only the regular HTTP codes + * but also the ones generated by the application + */ +enum HttpCodesEnum: int +{ + /** + * Regular HTTP codes + */ + case OK = 200; + case BadRequest = 400; + case NotFound = 404; + case Unauthorized = 401; + + /** + * Application specific codes + */ + case AppMalformedPayload = 3400; + case AppRecordsNotFound = 3401; + case AppResourceNotFound = 3402; + case AppUnauthorized = 3403; + case AppInvalidArguments = 3409; + + /** + * Returns an array with the value as key and text as value + * + * @return array + */ + public function error(): array + { + return [$this->value => $this->text()]; + } + + /** + * Return the text associated with the option + * + * @return string + */ + public function text(): string + { + return match ($this) { + self::OK => 'OK', + self::BadRequest => 'Bad Request', + self::NotFound => 'Not Found', + self::AppMalformedPayload => 'Malformed payload', + self::AppRecordsNotFound => 'Record(s) not found', + self::AppResourceNotFound => 'Resource not found', + self::Unauthorized, + self::AppUnauthorized => 'Unauthorized', + self::AppInvalidArguments => 'Invalid arguments provided', + }; + } +} From e6a1a62913855eb57a2db991d466ad3462285147 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:00:00 -0500 Subject: [PATCH 043/301] [#.x] - custom response object --- src/Domain/Services/Http/Response.php | 134 ++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/Domain/Services/Http/Response.php diff --git a/src/Domain/Services/Http/Response.php b/src/Domain/Services/Http/Response.php new file mode 100644 index 0000000..0f3a12b --- /dev/null +++ b/src/Domain/Services/Http/Response.php @@ -0,0 +1,134 @@ + + * + * 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\Http; + +use Exception; +use Phalcon\Api\Domain\Constants\Dates; +use Phalcon\Http\Response as PhalconResponse; + +use Phalcon\Http\Response\Exception as ResponseException; + +use function json_encode; +use function sha1; + +class Response extends PhalconResponse +{ + private array $payload = []; + + /** + * @return $this + * @throws Exception + */ + public function render(): self + { + $this + ->calculateMeta() + ->setJsonContent($this->payload) + ; + + return $this; + } + + /** + * @param int $code + * @param string $message + * + * @return $this + * @throws ResponseException + */ + public function withCode(int $code, string $message = ''): self + { + $this->setStatusCode($code, $message); + + return $this; + } + + /** + * @param array $data + * + * @return $this + * @throws Exception + */ + public function withPayloadData(array $data): self + { + if (empty($this->payload)) { + $this->initPayload(); + } + + $this->payload['data'] = $data; + + return $this; + } + + /** + * @param array $errors + * + * @return $this + */ + public function withPayloadErrors(array $errors): self + { + if (empty($this->payload)) { + $this->initPayload(); + } + + $this->payload['errors'] = $errors; + $this->payload['meta']['code'] = 3000; + $this->payload['meta']['message'] = 'error'; + + return $this; + } + + /** + * @return self + * @throws Exception + */ + private function calculateMeta(): self + { + $payload = [ + 'data' => $this->payload['data'], + 'errors' => $this->payload['errors'], + ]; + $encoded = json_encode($payload); + $encoded = (false === $encoded) ? '' : $encoded; + $timestamp = Dates::toUTC(); + $hash = sha1($timestamp . $encoded); + $eTag = sha1($encoded); + + $this->payload['meta']['timestamp'] = $timestamp; + $this->payload['meta']['hash'] = $hash; + + $this + ->setHeader('ETag', $eTag) + ; + + return $this; + } + + /** + * @return void + */ + private function initPayload(): void + { + $this->payload = [ + 'data' => [], + 'errors' => [], + 'meta' => [ + 'code' => HttpCodesEnum::OK->value, + 'hash' => '', + 'message' => 'success', + 'timestamp' => '', + ], + ]; + } +} From 1c6e681533f7122aec640f1338cf3138f6524ba7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:01:07 -0500 Subject: [PATCH 044/301] [#.x] - new routes interface and enum --- src/Domain/Interfaces/RoutesInterface.php | 62 +++++++++++++++ src/Domain/Services/Providers/RoutesEnum.php | 80 ++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/Domain/Interfaces/RoutesInterface.php create mode 100644 src/Domain/Services/Providers/RoutesEnum.php diff --git a/src/Domain/Interfaces/RoutesInterface.php b/src/Domain/Interfaces/RoutesInterface.php new file mode 100644 index 0000000..f1ccd28 --- /dev/null +++ b/src/Domain/Interfaces/RoutesInterface.php @@ -0,0 +1,62 @@ + + * + * 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 BackedEnum; + +/** + * Interface for route enumerations + * + * @phpstan-type TMiddleware array + */ +interface RoutesInterface extends BackedEnum +{ + public const DELETE = 'delete'; + public const EVENT_BEFORE = 'before'; + public const EVENT_FINISH = 'finish'; + public const GET = 'get'; + public const POST = 'post'; + public const PUT = 'put'; + + + /** + * @return string + */ + public function endpoint(): string; + + /** + * @return string + */ + public function method(): string; + + /** + * @return TMiddleware + */ + public static function middleware(): array; + + /** + * @return string + */ + public function prefix(): string; + + /** + * @return string + */ + public function service(): string; + + /** + * @return string + */ + public function suffix(): string; +} diff --git a/src/Domain/Services/Providers/RoutesEnum.php b/src/Domain/Services/Providers/RoutesEnum.php new file mode 100644 index 0000000..adad412 --- /dev/null +++ b/src/Domain/Services/Providers/RoutesEnum.php @@ -0,0 +1,80 @@ + + * + * 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\Providers; + +use Phalcon\Api\Domain\Interfaces\RoutesInterface; +use Phalcon\Api\Domain\Services\Container; + +use function str_replace; + +/** + * @phpstan-import-type TMiddleware from RoutesInterface + */ +enum RoutesEnum: string implements RoutesInterface +{ + case helloGet = ''; + + /** + * @return string + */ + public function endpoint(): string + { + return $this->prefix() . $this->suffix(); + } + + /** + * @return string + */ + public function method(): string + { + return match ($this) { + self::helloGet => self::GET, + }; + } + + /** + * @return TMiddleware + */ + public static function middleware(): array + { + return [ + Container::MIDDLEWARE_NOT_FOUND => self::EVENT_BEFORE, + Container::MIDDLEWARE_HEALTH => self::EVENT_BEFORE, + Container::MIDDLEWARE_RESPONSE_SENDER => self::EVENT_FINISH, + ]; + } + + /** + * @return string + */ + public function prefix(): string + { + return '/' . str_replace('-', '/', $this->value); + } + + public function service(): string + { + return match ($this) { + self::helloGet => Container::HELLO_SERVICE, + }; + } + + /** + * @return string + */ + public function suffix(): string + { + return ''; + } +} From 4d7d7ec5d6e9dae4951756cf1d8944f5f46fc31d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:01:51 -0500 Subject: [PATCH 045/301] [#.x] - added more methods/phpstan --- src/Domain/Services/Env/EnvManager.php | 84 +++++++++++++++++++------- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/src/Domain/Services/Env/EnvManager.php b/src/Domain/Services/Env/EnvManager.php index 25c7acf..4b0d3a5 100644 --- a/src/Domain/Services/Env/EnvManager.php +++ b/src/Domain/Services/Env/EnvManager.php @@ -13,6 +13,8 @@ namespace Phalcon\Api\Domain\Services\Env; +use Phalcon\Api\Domain\Constants\Dates; + use function array_merge; use function getenv; @@ -28,6 +30,32 @@ class EnvManager */ private static array $settings = []; + /** + * @return string + */ + public static function appEnv(): string + { + self::load(); + + /** @var string $appEnv */ + $appEnv = self::get('APP_ENV', 'development'); + + return (string)$appEnv; + } + + /** + * @return int + */ + public static function appLogLevel(): int + { + self::load(); + + /** @var int|string $logLevel */ + $logLevel = self::get('APP_LOG_LEVEL', 1); + + return (int)$logLevel; + } + /** * @param string $path * @@ -36,8 +64,20 @@ class EnvManager public static function appPath(string $path = ''): string { return dirname(__DIR__, 4) - . ($path ? DIRECTORY_SEPARATOR . $path : $path) - ; + . ($path ? DIRECTORY_SEPARATOR . $path : $path); + } + + /** + * @return string + */ + public static function appTimezone(): string + { + self::load(); + + /** @var string $logLevel */ + $timezone = self::get('APP_TIMEZONE', Dates::DATE_TIME_ZONE); + + return (string)$timezone; } /** @@ -55,6 +95,23 @@ public static function get( return self::$settings[$key] ?? $defaultValue; } + /** + * @return array + */ + private static function getOptions(): array + { + $envs = array_merge(getenv(), $_ENV); + /** @var string $adapter */ + $adapter = $envs['APP_ENV_ADAPTER'] ?? 'dotenv'; + /** @var string $filePath */ + $filePath = $envs['APP_ENV_FILE_PATH'] ?? self::appPath(); + + return [ + 'adapter' => $adapter, + 'filePath' => $filePath, + ]; + } + /** * @return void */ @@ -67,16 +124,16 @@ private static function load(): void $options = self::getOptions(); $adapter = $options['adapter']; - $envs = array_merge(getenv(), $_ENV); + $envs = array_merge(getenv(), $_ENV); /** @var TSettings $options */ $options = $envFactory->newInstance($adapter)->load($options); /** @var TSettings $envs */ - $envs = array_merge($envs, $options); + $envs = array_merge($envs, $options); self::$settings = array_map( function ($value) { return match ($value) { - 'true' => true, + 'true' => true, 'false' => false, default => $value, }; @@ -85,21 +142,4 @@ function ($value) { ); } } - - /** - * @return array - */ - private static function getOptions(): array - { - $envs = array_merge(getenv(), $_ENV); - /** @var string $adapter */ - $adapter = $envs['APP_ENV_ADAPTER'] ?? 'dotenv'; - /** @var string $filePath */ - $filePath = $envs['APP_ENV_FILE_PATH'] ?? ''; - - return [ - 'adapter' => $adapter, - 'filePath' => $filePath, - ]; - } } From 31cb3a99a26960a6d8fb7430697a5dce8a89839b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:02:06 -0500 Subject: [PATCH 046/301] [#.x] - changed it to a phalcon middleware --- .../Middleware/ResponseSenderMiddleware.php | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Domain/Middleware/ResponseSenderMiddleware.php b/src/Domain/Middleware/ResponseSenderMiddleware.php index 616d1c0..22e7bdd 100644 --- a/src/Domain/Middleware/ResponseSenderMiddleware.php +++ b/src/Domain/Middleware/ResponseSenderMiddleware.php @@ -13,15 +13,28 @@ 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; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Http\Response; +use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\Response\Exception; +use Phalcon\Mvc\Micro; -final readonly class ResponseSender +final class ResponseSenderMiddleware extends AbstractMiddleware { - public function __invoke(ResponseInterface $response): ResponseInterface + /** + * @param Micro $application + * + * @return true + * @throws EventsException + * @throws Exception + */ + public function call(Micro $application): bool { - return $response->send(); + /** @var Response $response */ + $response = $application->getSharedService(Container::RESPONSE); + + $response->send(); + + return true; } } From e0e850b5da50678fa825a94aac3c04da8c69e3f3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:02:32 -0500 Subject: [PATCH 047/301] [#.x] - adjustments to use new response class --- src/Responder/JsonResponder.php | 43 +++++---------------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/src/Responder/JsonResponder.php b/src/Responder/JsonResponder.php index 9783832..1510e98 100644 --- a/src/Responder/JsonResponder.php +++ b/src/Responder/JsonResponder.php @@ -13,58 +13,27 @@ namespace Phalcon\Api\Responder; -use DateTimeImmutable; -use DateTimeZone; use Phalcon\Api\Domain\Interfaces\ResponderInterface; +use Phalcon\Api\Domain\Services\Http\Response; use Phalcon\Domain\Payload; -use Phalcon\Http\ResponseInterface; - -use function json_encode; final class JsonResponder implements ResponderInterface { public function __construct( - private ResponseInterface $response + private Response $response ) { } - public function __invoke(Payload $payload): ResponseInterface + public function __invoke(Payload $payload): Response { - $result = $payload->getResult(); + $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) + ->withPayloadData([$content]) + ->render() ; return $this->response; From 13e78f61feca1f4f14abe422a12b8f7756509776 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:03:11 -0500 Subject: [PATCH 048/301] [#.x] - added logger, evmanager and middleware definitions --- src/Domain/Services/Container.php | 114 ++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 23 deletions(-) diff --git a/src/Domain/Services/Container.php b/src/Domain/Services/Container.php index b6727bc..290b183 100644 --- a/src/Domain/Services/Container.php +++ b/src/Domain/Services/Container.php @@ -13,53 +13,98 @@ namespace Phalcon\Api\Domain\Services; +use Phalcon\Api\Domain\Health\HealthService; use Phalcon\Api\Domain\Hello\HelloService; +use Phalcon\Api\Domain\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Middleware\ResponseSenderMiddleware; +use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Domain\Services\Http\Response; use Phalcon\Api\Responder\JsonResponder; use Phalcon\Di\Di; use Phalcon\Di\Service; +use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\FilterFactory; use Phalcon\Http\Request; -use Phalcon\Http\Response; +use Phalcon\Logger\Adapter\Stream; +use Phalcon\Logger\Logger; use Phalcon\Mvc\Router; class Container extends Di { + /** @var string */ + public const APPLICATION = 'application'; /** @var string */ public const CACHE = 'cache'; /** @var string */ public const CONNECTION = 'connection'; /** @var string */ + public const EVENTS_MANAGER = 'eventsManager'; + /** @var string */ public const FILTER = 'filter'; + /** + * Services + */ + public const HELLO_SERVICE = 'hello.service'; /** @var string */ public const LOGGER = 'logger'; + /** + * Middleware + */ + public const MIDDLEWARE_HEALTH = 'middleware.health'; + public const MIDDLEWARE_NOT_FOUND = 'middleware.not.found'; + public const MIDDLEWARE_RESPONSE_SENDER = 'middleware.response.sender'; /** @var string */ public const REQUEST = 'request'; + /** + * Responders + */ + public const RESPONDER_JSON = 'hello.responder.json'; /** @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'; + /** @var string */ + public const TIME = 'time'; 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::EVENTS_MANAGER => $this->getServiceEventsManger(), + self::FILTER => $this->getServiceFilter(), + self::LOGGER => $this->getServiceLogger(), + 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_SERVICE => $this->getServiceSimple(HelloService::class), - self::HELLO_RESPONDER_JSON => $this->getServiceResponderJson(), + self::MIDDLEWARE_HEALTH => $this->getServiceSimple(HealthMiddleware::class), + self::MIDDLEWARE_NOT_FOUND => $this->getServiceSimple(NotFoundMiddleware::class), + self::MIDDLEWARE_RESPONSE_SENDER => $this->getServiceSimple(ResponseSenderMiddleware::class), + + self::RESPONDER_JSON => $this->getServiceResponderJson(), ]; parent::__construct(); } + /** + * @return Service + */ + private function getServiceEventsManger(): Service + { + return new Service( + function () { + $evm = new EventsManager(); + $evm->enablePriorities(true); + + return $evm; + }, + true + ); + } + /** * @return Service */ @@ -76,18 +121,23 @@ function () { /** * @return Service */ - private function getServiceRouter(): Service + private function getServiceLogger(): Service { + /** @var string $logName */ + $logName = EnvManager::get('LOG_FILENAME', 'rest-api'); + /** @var string $logPath */ + $logPath = EnvManager::get('LOG_PATH', 'storage/logs/'); + $logFile = EnvManager::appPath($logPath) . '/' . $logName . '.log'; + return new Service( - [ - 'className' => Router::class, - 'arguments' => [ + function () use ($logName, $logFile) { + return new Logger( + $logName, [ - 'type' => 'parameter', - 'value' => false, + 'main' => new Stream($logFile), ] - ] - ] + ); + } ); } @@ -100,8 +150,26 @@ private function getServiceResponderJson(): Service [ 'type' => 'service', 'name' => self::RESPONSE, - ] - ] + ], + ], + ] + ); + } + + /** + * @return Service + */ + private function getServiceRouter(): Service + { + return new Service( + [ + 'className' => Router::class, + 'arguments' => [ + [ + 'type' => 'parameter', + 'value' => false, + ], + ], ] ); } From 2c80c47214405d76fbf91a24681d88a151800282 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:03:39 -0500 Subject: [PATCH 049/301] [#.x] - added router provider and error handler provider --- .../Providers/ErrorHandlerProvider.php | 86 +++++++++++++++ .../Services/Providers/RouterProvider.php | 100 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 src/Domain/Services/Providers/ErrorHandlerProvider.php create mode 100644 src/Domain/Services/Providers/RouterProvider.php diff --git a/src/Domain/Services/Providers/ErrorHandlerProvider.php b/src/Domain/Services/Providers/ErrorHandlerProvider.php new file mode 100644 index 0000000..19fb7cd --- /dev/null +++ b/src/Domain/Services/Providers/ErrorHandlerProvider.php @@ -0,0 +1,86 @@ + + * + * 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\Providers; + +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Di\DiInterface; +use Phalcon\Di\ServiceProviderInterface; +use Phalcon\Logger\Logger; + +use function date_default_timezone_set; +use function error_reporting; +use function hrtime; +use function memory_get_usage; +use function number_format; +use function register_shutdown_function; +use function sprintf; + +class ErrorHandlerProvider implements ServiceProviderInterface +{ + public function register(DiInterface $container): void + { + $logger = $container->getShared(Container::LOGGER); + + date_default_timezone_set(EnvManager::appTimezone()); + $errors = 'development' === EnvManager::appEnv() ? 'On' : 'Off'; + ini_set('display_errors', $errors); + error_reporting(E_ALL); + + set_error_handler( + function (int $number, string $message, string $file, int $line) use ($logger) { + $logger + ->error( + sprintf( + '[#:%s]-[L: %s] : %s (%s)', + $number, + $line, + $message, + $file + ) + ) + ; + + return true; + } + ); + + register_shutdown_function([$this, 'onShutdown'], $container); + } + + protected function onShutdown(DiInterface $container): bool + { + /** @var Logger $logger */ + $logger = $container->getShared(Container::LOGGER); + /** @var int $time */ + $time = $container->getShared(Container::TIME); + $memory = memory_get_usage() / 1000000; + $execution = hrtime(true) - $time; + $execution = $execution / 1000000000; + + if (EnvManager::appLogLevel() >= 1) { + $logger + ->info( + sprintf( + 'Shutdown completed [%s]s - [%s]MB', + number_format($execution, 4), + number_format($memory, 2), + ) + ) + ; + } + + return true; + } +} diff --git a/src/Domain/Services/Providers/RouterProvider.php b/src/Domain/Services/Providers/RouterProvider.php new file mode 100644 index 0000000..abf6f2d --- /dev/null +++ b/src/Domain/Services/Providers/RouterProvider.php @@ -0,0 +1,100 @@ + + * + * 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\Providers; + +use Phalcon\Api\Domain\Interfaces\DomainInterface; +use Phalcon\Api\Domain\Interfaces\ResponderInterface; +use Phalcon\Api\Domain\Interfaces\RoutesInterface; +use Phalcon\Api\Domain\Services\ActionHandler; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Di\DiInterface; +use Phalcon\Di\ServiceProviderInterface; +use Phalcon\Events\Manager as EventsManager; +use Phalcon\Mvc\Micro; + +/** + * @phpstan-import-type TMiddleware from RoutesInterface + */ +class RouterProvider implements ServiceProviderInterface +{ + public function register(DiInterface $container): void + { + /** @var Micro $application */ + $application = $container->getShared(Container::APPLICATION); + /** @var EventsManager $eventsManager */ + $eventsManager = $container->getShared(Container::EVENTS_MANAGER); + + $this->attachRoutes($application); + $this->attachMiddleware($application, $eventsManager); + + $application->get('/health', function () { + /** empty */ + }); + + $application->setEventsManager($eventsManager); + } + + /** + * @param Micro $application + * @param EventsManager $eventsManager + * + * @return void + */ + private function attachMiddleware( + Micro $application, + EventsManager $eventsManager + ): void { + /** @var TMiddleware $middleware */ + $middleware = RoutesEnum::middleware(); + foreach ($middleware as $service => $method) { + $instance = $application->getService($service); + $eventsManager->attach('micro', $instance); + $application->$method($instance); + } + } + + /** + * Attaches routes to the application, lazy loaded + * + * @param Micro $application + * + * @return void + */ + private function attachRoutes(Micro $application): void + { + /** @var ResponderInterface $responder */ + $responder = $application->getService(Container::RESPONDER_JSON); + + /** @var array $routes */ + $routes = RoutesEnum::cases(); + foreach ($routes as $route) { + $serviceName = $route->service(); + $collection = new Micro\Collection(); + /** @var DomainInterface $service */ + $service = $application->getService($serviceName); + $action = new ActionHandler($service, $responder); + + $collection + ->setHandler($action) + ->setPrefix($route->prefix()) + ->{$route->method()}( + $route->suffix(), + '__invoke' + ) + ; + + $application->mount($collection); + } + } +} From 48116130f50bc35371993b5dae98a6825543296e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:03:57 -0500 Subject: [PATCH 050/301] [#.x] - health and 404 middleware --- src/Domain/Middleware/AbstractMiddleware.php | 45 ++++++++++++++++ src/Domain/Middleware/HealthMiddleware.php | 56 ++++++++++++++++++++ src/Domain/Middleware/NotFoundMiddleware.php | 48 +++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 src/Domain/Middleware/AbstractMiddleware.php create mode 100644 src/Domain/Middleware/HealthMiddleware.php create mode 100644 src/Domain/Middleware/NotFoundMiddleware.php diff --git a/src/Domain/Middleware/AbstractMiddleware.php b/src/Domain/Middleware/AbstractMiddleware.php new file mode 100644 index 0000000..4487a1f --- /dev/null +++ b/src/Domain/Middleware/AbstractMiddleware.php @@ -0,0 +1,45 @@ + + * + * 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\Services\Container; +use Phalcon\Api\Domain\Services\Http\Response; +use Phalcon\Mvc\Micro; +use Phalcon\Mvc\Micro\MiddlewareInterface; + +abstract class AbstractMiddleware implements MiddlewareInterface +{ + protected function halt( + Micro $application, + int $code, + string $message = '', + array $data = [], + array $errors = [] + ): void { + /** @var Response $response */ + $response = $application->getSharedService(Container::RESPONSE); + + $application->stop(); + + $response->withCode($code, $message); + + if (true === empty($errors)) { + $response->withPayloadData($data); + } else { + $response->withPayloadErrors($errors); + } + + $response->render()->send(); + } +} diff --git a/src/Domain/Middleware/HealthMiddleware.php b/src/Domain/Middleware/HealthMiddleware.php new file mode 100644 index 0000000..898a51f --- /dev/null +++ b/src/Domain/Middleware/HealthMiddleware.php @@ -0,0 +1,56 @@ + + * + * 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\Services\Container; +use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; +use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\Request; +use Phalcon\Http\Response\Exception; +use Phalcon\Mvc\Micro; + +final class HealthMiddleware extends AbstractMiddleware +{ + /** + * @param Micro $application + * + * @return true + * @throws EventsException + * @throws Exception + */ + public function call(Micro $application): bool + { + /** @var Request $request */ + $request = $application->getSharedService(Container::REQUEST); + + if ( + '/health' === $request->getURI() && + true === $request->isGet() + ) { + $payload = [ + 'status' => 'ok', + 'message' => 'service operational', + ]; + + $this->halt( + $application, + HttpCodesEnum::OK->value, + HttpCodesEnum::OK->text(), + $payload + ); + } + + return true; + } +} diff --git a/src/Domain/Middleware/NotFoundMiddleware.php b/src/Domain/Middleware/NotFoundMiddleware.php new file mode 100644 index 0000000..eb268be --- /dev/null +++ b/src/Domain/Middleware/NotFoundMiddleware.php @@ -0,0 +1,48 @@ + + * + * 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\Services\Http\HttpCodesEnum; +use Phalcon\Events\Event; +use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\Response\Exception; +use Phalcon\Mvc\Micro; + +final class NotFoundMiddleware extends AbstractMiddleware +{ + public function beforeNotFound(Event $event, Micro $application): bool + { + $this->halt( + $application, + HttpCodesEnum::NotFound->value, + 'error', + [], + HttpCodesEnum::AppResourceNotFound->error() + ); + + return false; + } + + /** + * @param Micro $application + * + * @return true + * @throws EventsException + * @throws Exception + */ + public function call(Micro $application): bool + { + return true; + } +} From 3a98171d0a4675b290d52c272d24dabd9e2063c1 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:04:16 -0500 Subject: [PATCH 051/301] [#.x] - phpcs and phpstan --- .../InvalidConfigurationArgumentException.php | 4 +- src/Domain/Hello/HelloService.php | 2 +- src/Domain/Services/Env/EnvFactory.php | 2 +- .../Services/Env/Adapters/DotEnvTest.php | 66 +++++++++---------- .../Domain/Services/Env/EnvManagerTest.php | 20 +++--- 5 files changed, 45 insertions(+), 49 deletions(-) diff --git a/src/Domain/Exceptions/InvalidConfigurationArgumentException.php b/src/Domain/Exceptions/InvalidConfigurationArgumentException.php index 8e9b0cd..cf9c6de 100644 --- a/src/Domain/Exceptions/InvalidConfigurationArgumentException.php +++ b/src/Domain/Exceptions/InvalidConfigurationArgumentException.php @@ -13,6 +13,8 @@ namespace Phalcon\Api\Domain\Exceptions; -class InvalidConfigurationArgumentException extends \InvalidArgumentException +use InvalidArgumentException; + +class InvalidConfigurationArgumentException extends InvalidArgumentException { } diff --git a/src/Domain/Hello/HelloService.php b/src/Domain/Hello/HelloService.php index 289ddc4..2991e1e 100644 --- a/src/Domain/Hello/HelloService.php +++ b/src/Domain/Hello/HelloService.php @@ -26,7 +26,7 @@ public function __invoke(): Payload return new Payload( DomainStatus::SUCCESS, [ - 'results' => "Hello World!!! - " . date("Y-m-d H:i:s") + 'results' => "Hello World!!! - " . date("Y-m-d H:i:s"), ] ); } diff --git a/src/Domain/Services/Env/EnvFactory.php b/src/Domain/Services/Env/EnvFactory.php index 0fe58ab..84910e2 100644 --- a/src/Domain/Services/Env/EnvFactory.php +++ b/src/Domain/Services/Env/EnvFactory.php @@ -36,7 +36,7 @@ public function newInstance(string $name, mixed ...$parameters): AdapterInterfac $definition = $adapters[$name]; /** @var AdapterInterface $instance */ - $instance = new $definition(...$parameters); + $instance = new $definition(...$parameters); $this->instances[$name] = $instance; } diff --git a/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php index 4171d27..37b47bf 100644 --- a/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php +++ b/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php @@ -15,7 +15,6 @@ 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; @@ -23,16 +22,39 @@ final class DotEnvTest extends AbstractUnitTestCase { private string $envFile; - protected function setUp(): void + public function testLoadExceptionForEmptyFilePath(): void { - $this->envFile = EnvManager::appPath() - . '/tests/Fixtures/Domain/Services/Env/' - ; + $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); } public function testLoadSuccess(): void { - $dotEnv = new DotEnv(); + $dotEnv = new DotEnv(); $options = [ 'filePath' => $this->envFile, ]; @@ -43,7 +65,7 @@ public function testLoadSuccess(): void 'SAMPLE_TRUE' => 'true', 'SAMPLE_FALSE' => 'false', ]; - $actual = $dotEnv->load($options); + $actual = $dotEnv->load($options); $this->assertArrayHasKey('SAMPLE_STRING', $actual); $this->assertArrayHasKey('SAMPLE_INT', $actual); @@ -60,33 +82,9 @@ public function testLoadSuccess(): void $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 + protected function setUp(): 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); + $this->envFile = EnvManager::appPath() + . '/tests/Fixtures/Domain/Services/Env/'; } } diff --git a/tests/Unit/Domain/Services/Env/EnvManagerTest.php b/tests/Unit/Domain/Services/Env/EnvManagerTest.php index f39c493..d299e06 100644 --- a/tests/Unit/Domain/Services/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Services/Env/EnvManagerTest.php @@ -13,25 +13,14 @@ 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); @@ -44,7 +33,7 @@ public function testGetFromDotEnvLoad(): void $_ENV = [ 'APP_ENV_ADAPTER' => 'dotenv', 'APP_ENV_FILE_PATH' => EnvManager::appPath() - . '/tests/Fixtures/Domain/Services/Env/' + . '/tests/Fixtures/Domain/Services/Env/', ]; $values = [ @@ -74,4 +63,11 @@ public function testGetFromDotEnvLoad(): void $actual = EnvManager::get('SAMPLE_FALSE'); $this->assertSame($expected, $actual); } + + protected function setUp(): void + { + $ref = new ReflectionClass(EnvManager::class); + $ref->setStaticPropertyValue('isLoaded', false); + $ref->setStaticPropertyValue('settings', []); + } } From f7f18a7b51f769b491a31a6e68f31a83e73f6795 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:04:36 -0500 Subject: [PATCH 052/301] [#.x] - started using providers, moved routing out --- public/index.php | 66 +++++++++++++++--------------------------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/public/index.php b/public/index.php index d20791f..5906089 100644 --- a/public/index.php +++ b/public/index.php @@ -2,65 +2,41 @@ declare(strict_types=1); -use Phalcon\Api\Domain\Interfaces\DomainInterface; -use Phalcon\Api\Domain\Interfaces\ResponderInterface; -use Phalcon\Api\Domain\Middleware\ResponseSender; -use Phalcon\Api\Domain\Services\ActionHandler; use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Providers\ErrorHandlerProvider; +use Phalcon\Api\Domain\Services\Providers\RouterProvider; +use Phalcon\Di\ServiceProviderInterface; use Phalcon\Mvc\Micro; require_once dirname(__DIR__) . '/vendor/autoload.php'; $container = new Container(); $application = new Micro($container); +$container->set(Container::APPLICATION, $application, true); +$now = hrtime(true); +$container->set( + Container::TIME, + function () use ($now) { + return $now; + }, + true +); /** - * Routes + * Providers */ -$routes = [ - [ - 'method' => 'get', - 'pattern' => '/', - 'service' => Container::HELLO_SERVICE, - 'responder' => Container::HELLO_RESPONDER_JSON, - ], +$providers = [ + ErrorHandlerProvider::class, + RouterProvider::class, ]; -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(); - } - ); +/** @var class-string $provider */ +foreach ($providers as $provider) { + /** @var ServiceProviderInterface $service */ + $service = new $provider(); + $container->register($service); } -$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'] ?? ''; From 25547b7af0bf9f8b9ad87c4d40c653b2238de5bd Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 06:05:46 -0500 Subject: [PATCH 053/301] [#.x] - phpcs --- src/Domain/Interfaces/RoutesInterface.php | 8 ++++---- src/Domain/Services/Http/Response.php | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Domain/Interfaces/RoutesInterface.php b/src/Domain/Interfaces/RoutesInterface.php index f1ccd28..eb6d7c0 100644 --- a/src/Domain/Interfaces/RoutesInterface.php +++ b/src/Domain/Interfaces/RoutesInterface.php @@ -22,12 +22,12 @@ */ interface RoutesInterface extends BackedEnum { - public const DELETE = 'delete'; + public const DELETE = 'delete'; public const EVENT_BEFORE = 'before'; public const EVENT_FINISH = 'finish'; - public const GET = 'get'; - public const POST = 'post'; - public const PUT = 'put'; + public const GET = 'get'; + public const POST = 'post'; + public const PUT = 'put'; /** diff --git a/src/Domain/Services/Http/Response.php b/src/Domain/Services/Http/Response.php index 0f3a12b..b19755e 100644 --- a/src/Domain/Services/Http/Response.php +++ b/src/Domain/Services/Http/Response.php @@ -16,7 +16,6 @@ use Exception; use Phalcon\Api\Domain\Constants\Dates; use Phalcon\Http\Response as PhalconResponse; - use Phalcon\Http\Response\Exception as ResponseException; use function json_encode; From 5098f87a8a63b38031ea926de835b2a7dbd347d5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 07:55:18 -0500 Subject: [PATCH 054/301] [#.x] - phpstan --- src/Domain/Services/Env/EnvManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Services/Env/EnvManager.php b/src/Domain/Services/Env/EnvManager.php index 4b0d3a5..6dde7a2 100644 --- a/src/Domain/Services/Env/EnvManager.php +++ b/src/Domain/Services/Env/EnvManager.php @@ -74,7 +74,7 @@ public static function appTimezone(): string { self::load(); - /** @var string $logLevel */ + /** @var string $timezone */ $timezone = self::get('APP_TIMEZONE', Dates::DATE_TIME_ZONE); return (string)$timezone; From 2fbd9e13903a8c8ba3343597f555a86f69dfb1db Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 25 Sep 2025 07:55:33 -0500 Subject: [PATCH 055/301] [#.x] - phpstan --- src/Domain/Services/Providers/RouterProvider.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Domain/Services/Providers/RouterProvider.php b/src/Domain/Services/Providers/RouterProvider.php index abf6f2d..21640e9 100644 --- a/src/Domain/Services/Providers/RouterProvider.php +++ b/src/Domain/Services/Providers/RouterProvider.php @@ -22,6 +22,7 @@ use Phalcon\Di\ServiceProviderInterface; use Phalcon\Events\Manager as EventsManager; use Phalcon\Mvc\Micro; +use Phalcon\Mvc\Micro\Collection; /** * @phpstan-import-type TMiddleware from RoutesInterface @@ -58,6 +59,7 @@ private function attachMiddleware( /** @var TMiddleware $middleware */ $middleware = RoutesEnum::middleware(); foreach ($middleware as $service => $method) { + /** @var Micro\MiddlewareInterface $instance */ $instance = $application->getService($service); $eventsManager->attach('micro', $instance); $application->$method($instance); @@ -76,11 +78,10 @@ private function attachRoutes(Micro $application): void /** @var ResponderInterface $responder */ $responder = $application->getService(Container::RESPONDER_JSON); - /** @var array $routes */ $routes = RoutesEnum::cases(); foreach ($routes as $route) { $serviceName = $route->service(); - $collection = new Micro\Collection(); + $collection = new Collection(); /** @var DomainInterface $service */ $service = $application->getService($serviceName); $action = new ActionHandler($service, $responder); From 585ca25edff8a68521f46acf4302e0b266501d8f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 26 Sep 2025 09:30:31 -0500 Subject: [PATCH 056/301] [#.x] - upgrading packages --- .github/workflows/main.yml | 2 +- .gitignore | 10 +++++----- composer.json | 2 +- src/Domain/{Services => ADR}/ActionHandler.php | 0 src/Domain/Interfaces/{ => ADR}/ActionInterface.php | 0 src/Domain/Interfaces/{ => ADR}/DomainInterface.php | 0 src/Domain/Interfaces/{ => ADR}/ResponderInterface.php | 0 7 files changed, 7 insertions(+), 7 deletions(-) rename src/Domain/{Services => ADR}/ActionHandler.php (100%) rename src/Domain/Interfaces/{ => ADR}/ActionInterface.php (100%) rename src/Domain/Interfaces/{ => ADR}/DomainInterface.php (100%) rename src/Domain/Interfaces/{ => ADR}/ResponderInterface.php (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6b9135c..0affd28 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -127,7 +127,7 @@ jobs: composer test-unit-coverage - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@v5 + uses: SonarSource/sonarqube-scan-action@v6.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore index 55f49f2..596f531 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ +.bash_history .cache .config -.local -.composer +.composer/ composer.lock -vendor/ -.bash_history -tests/_output .env +.local storage/logs/ +tests/_output +vendor/ diff --git a/composer.json b/composer.json index e1380a5..6e97c9d 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "pds/skeleton": "^1.0", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^11.5", - "squizlabs/php_codesniffer": "^3.13" + "squizlabs/php_codesniffer": "^4.0" }, "autoload": { "psr-4": { diff --git a/src/Domain/Services/ActionHandler.php b/src/Domain/ADR/ActionHandler.php similarity index 100% rename from src/Domain/Services/ActionHandler.php rename to src/Domain/ADR/ActionHandler.php diff --git a/src/Domain/Interfaces/ActionInterface.php b/src/Domain/Interfaces/ADR/ActionInterface.php similarity index 100% rename from src/Domain/Interfaces/ActionInterface.php rename to src/Domain/Interfaces/ADR/ActionInterface.php diff --git a/src/Domain/Interfaces/DomainInterface.php b/src/Domain/Interfaces/ADR/DomainInterface.php similarity index 100% rename from src/Domain/Interfaces/DomainInterface.php rename to src/Domain/Interfaces/ADR/DomainInterface.php diff --git a/src/Domain/Interfaces/ResponderInterface.php b/src/Domain/Interfaces/ADR/ResponderInterface.php similarity index 100% rename from src/Domain/Interfaces/ResponderInterface.php rename to src/Domain/Interfaces/ADR/ResponderInterface.php From cfb21813a1703a1980e999d83dee110aa181564c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 28 Sep 2025 13:49:19 -0500 Subject: [PATCH 057/301] [#.x] - phpstan corrections --- src/Domain/ADR/{ => Action}/ActionHandler.php | 7 ++-- .../ADR => ADR/Action}/ActionInterface.php | 2 +- .../ADR => ADR/Domain}/DomainInterface.php | 2 +- .../ADR}/Responder/JsonResponder.php | 3 +- .../Responder}/ResponderInterface.php | 2 +- src/Domain/Hello/HelloService.php | 5 +-- src/Domain/Middleware/AbstractMiddleware.php | 18 ++++++++++ src/Domain/Middleware/NotFoundMiddleware.php | 12 +++++-- src/Domain/Services/Container.php | 2 +- src/Domain/Services/Http/Response.php | 12 +++++-- src/Domain/Services/Http/ResponseTypes.php | 35 +++++++++++++++++++ .../Providers/ErrorHandlerProvider.php | 1 + .../Services/Providers/RouterProvider.php | 6 ++-- 13 files changed, 86 insertions(+), 21 deletions(-) rename src/Domain/ADR/{ => Action}/ActionHandler.php (75%) rename src/Domain/{Interfaces/ADR => ADR/Action}/ActionInterface.php (88%) rename src/Domain/{Interfaces/ADR => ADR/Domain}/DomainInterface.php (89%) rename src/{ => Domain/ADR}/Responder/JsonResponder.php (90%) rename src/Domain/{Interfaces/ADR => ADR/Responder}/ResponderInterface.php (90%) create mode 100644 src/Domain/Services/Http/ResponseTypes.php diff --git a/src/Domain/ADR/ActionHandler.php b/src/Domain/ADR/Action/ActionHandler.php similarity index 75% rename from src/Domain/ADR/ActionHandler.php rename to src/Domain/ADR/Action/ActionHandler.php index 6577ec4..2855f7d 100644 --- a/src/Domain/ADR/ActionHandler.php +++ b/src/Domain/ADR/Action/ActionHandler.php @@ -11,11 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services; +namespace Phalcon\Api\Domain\ADR\Action; -use Phalcon\Api\Domain\Interfaces\ActionInterface; -use Phalcon\Api\Domain\Interfaces\DomainInterface; -use Phalcon\Api\Domain\Interfaces\ResponderInterface; +use Phalcon\Api\Domain\ADR\Domain\DomainInterface; +use Phalcon\Api\Domain\ADR\Responder\ResponderInterface; final readonly class ActionHandler implements ActionInterface { diff --git a/src/Domain/Interfaces/ADR/ActionInterface.php b/src/Domain/ADR/Action/ActionInterface.php similarity index 88% rename from src/Domain/Interfaces/ADR/ActionInterface.php rename to src/Domain/ADR/Action/ActionInterface.php index bb74052..1d6586b 100644 --- a/src/Domain/Interfaces/ADR/ActionInterface.php +++ b/src/Domain/ADR/Action/ActionInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Interfaces; +namespace Phalcon\Api\Domain\ADR\Action; interface ActionInterface { diff --git a/src/Domain/Interfaces/ADR/DomainInterface.php b/src/Domain/ADR/Domain/DomainInterface.php similarity index 89% rename from src/Domain/Interfaces/ADR/DomainInterface.php rename to src/Domain/ADR/Domain/DomainInterface.php index ef9d164..b8747ff 100644 --- a/src/Domain/Interfaces/ADR/DomainInterface.php +++ b/src/Domain/ADR/Domain/DomainInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Interfaces; +namespace Phalcon\Api\Domain\ADR\Domain; use Phalcon\Domain\Payload; diff --git a/src/Responder/JsonResponder.php b/src/Domain/ADR/Responder/JsonResponder.php similarity index 90% rename from src/Responder/JsonResponder.php rename to src/Domain/ADR/Responder/JsonResponder.php index 1510e98..0e9cec1 100644 --- a/src/Responder/JsonResponder.php +++ b/src/Domain/ADR/Responder/JsonResponder.php @@ -11,9 +11,8 @@ declare(strict_types=1); -namespace Phalcon\Api\Responder; +namespace Phalcon\Api\Domain\ADR\Responder; -use Phalcon\Api\Domain\Interfaces\ResponderInterface; use Phalcon\Api\Domain\Services\Http\Response; use Phalcon\Domain\Payload; diff --git a/src/Domain/Interfaces/ADR/ResponderInterface.php b/src/Domain/ADR/Responder/ResponderInterface.php similarity index 90% rename from src/Domain/Interfaces/ADR/ResponderInterface.php rename to src/Domain/ADR/Responder/ResponderInterface.php index d15b29f..c96e0db 100644 --- a/src/Domain/Interfaces/ADR/ResponderInterface.php +++ b/src/Domain/ADR/Responder/ResponderInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Interfaces; +namespace Phalcon\Api\Domain\ADR\Responder; use Phalcon\Domain\Payload; use Phalcon\Http\ResponseInterface; diff --git a/src/Domain/Hello/HelloService.php b/src/Domain/Hello/HelloService.php index 2991e1e..505dae1 100644 --- a/src/Domain/Hello/HelloService.php +++ b/src/Domain/Hello/HelloService.php @@ -14,7 +14,8 @@ namespace Phalcon\Api\Domain\Hello; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Interfaces\DomainInterface; +use Phalcon\Api\Domain\ADR\Domain\DomainInterface; +use Phalcon\Api\Domain\Constants\Dates; use Phalcon\Domain\Payload; use function date; @@ -26,7 +27,7 @@ public function __invoke(): Payload return new Payload( DomainStatus::SUCCESS, [ - 'results' => "Hello World!!! - " . date("Y-m-d H:i:s"), + 'results' => "Hello World!!! - " . date(Dates::DATE_TIME_FORMAT), ] ); } diff --git a/src/Domain/Middleware/AbstractMiddleware.php b/src/Domain/Middleware/AbstractMiddleware.php index 4487a1f..06c2513 100644 --- a/src/Domain/Middleware/AbstractMiddleware.php +++ b/src/Domain/Middleware/AbstractMiddleware.php @@ -15,11 +15,29 @@ use Phalcon\Api\Domain\Services\Container; use Phalcon\Api\Domain\Services\Http\Response; +use Phalcon\Api\Domain\Services\Http\ResponseTypes; +use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\Response\Exception as ResponseException; use Phalcon\Mvc\Micro; use Phalcon\Mvc\Micro\MiddlewareInterface; +/** + * @phpstan-import-type TData from ResponseTypes + * @phpstan-import-type TErrors from ResponseTypes + */ abstract class AbstractMiddleware implements MiddlewareInterface { + /** + * @param Micro $application + * @param int $code + * @param string $message + * @param TData $data + * @param TErrors $errors + * + * @return void + * @throws EventsException + * @throws ResponseException + */ protected function halt( Micro $application, int $code, diff --git a/src/Domain/Middleware/NotFoundMiddleware.php b/src/Domain/Middleware/NotFoundMiddleware.php index eb268be..a5926e9 100644 --- a/src/Domain/Middleware/NotFoundMiddleware.php +++ b/src/Domain/Middleware/NotFoundMiddleware.php @@ -21,6 +21,14 @@ final class NotFoundMiddleware extends AbstractMiddleware { + /** + * @param Event $event + * @param Micro $application + * + * @return bool + * @throws EventsException + * @throws Exception + */ public function beforeNotFound(Event $event, Micro $application): bool { $this->halt( @@ -28,7 +36,7 @@ public function beforeNotFound(Event $event, Micro $application): bool HttpCodesEnum::NotFound->value, 'error', [], - HttpCodesEnum::AppResourceNotFound->error() + [HttpCodesEnum::AppResourceNotFound->error()] ); return false; @@ -38,8 +46,6 @@ public function beforeNotFound(Event $event, Micro $application): bool * @param Micro $application * * @return true - * @throws EventsException - * @throws Exception */ public function call(Micro $application): bool { diff --git a/src/Domain/Services/Container.php b/src/Domain/Services/Container.php index 290b183..91c1a29 100644 --- a/src/Domain/Services/Container.php +++ b/src/Domain/Services/Container.php @@ -13,6 +13,7 @@ namespace Phalcon\Api\Domain\Services; +use Phalcon\Api\Domain\ADR\Responder\JsonResponder; use Phalcon\Api\Domain\Health\HealthService; use Phalcon\Api\Domain\Hello\HelloService; use Phalcon\Api\Domain\Middleware\HealthMiddleware; @@ -20,7 +21,6 @@ use Phalcon\Api\Domain\Middleware\ResponseSenderMiddleware; use Phalcon\Api\Domain\Services\Env\EnvManager; use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Api\Responder\JsonResponder; use Phalcon\Di\Di; use Phalcon\Di\Service; use Phalcon\Events\Manager as EventsManager; diff --git a/src/Domain/Services/Http/Response.php b/src/Domain/Services/Http/Response.php index b19755e..45495cb 100644 --- a/src/Domain/Services/Http/Response.php +++ b/src/Domain/Services/Http/Response.php @@ -21,9 +21,15 @@ use function json_encode; use function sha1; +/** + * @phpstan-import-type TData from ResponseTypes + * @phpstan-import-type TErrors from ResponseTypes + * @phpstan-import-type TResponsePayload from ResponseTypes + */ class Response extends PhalconResponse { - private array $payload = []; + /** @var TResponsePayload */ + private array $payload; /** * @return $this @@ -54,7 +60,7 @@ public function withCode(int $code, string $message = ''): self } /** - * @param array $data + * @param TData $data * * @return $this * @throws Exception @@ -71,7 +77,7 @@ public function withPayloadData(array $data): self } /** - * @param array $errors + * @param TErrors $errors * * @return $this */ diff --git a/src/Domain/Services/Http/ResponseTypes.php b/src/Domain/Services/Http/ResponseTypes.php new file mode 100644 index 0000000..377200f --- /dev/null +++ b/src/Domain/Services/Http/ResponseTypes.php @@ -0,0 +1,35 @@ + + * + * 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\Http; + +/** + * This class is used to define phpstan types so that they are all in one + * place. + * + * @phpstan-type TData array|array{} + * @phpstan-type TErrors array>|array{} + * @phpstan-type TResponsePayload array{ + * data: TData, + * errors: TErrors, + * meta: array{ + * code: int, + * hash: string, + * message: string, + * timestamp: string + * } + * } + */ +final class ResponseTypes +{ +} diff --git a/src/Domain/Services/Providers/ErrorHandlerProvider.php b/src/Domain/Services/Providers/ErrorHandlerProvider.php index 19fb7cd..d455014 100644 --- a/src/Domain/Services/Providers/ErrorHandlerProvider.php +++ b/src/Domain/Services/Providers/ErrorHandlerProvider.php @@ -31,6 +31,7 @@ class ErrorHandlerProvider implements ServiceProviderInterface { public function register(DiInterface $container): void { + /** @var Logger $logger */ $logger = $container->getShared(Container::LOGGER); date_default_timezone_set(EnvManager::appTimezone()); diff --git a/src/Domain/Services/Providers/RouterProvider.php b/src/Domain/Services/Providers/RouterProvider.php index 21640e9..ee17143 100644 --- a/src/Domain/Services/Providers/RouterProvider.php +++ b/src/Domain/Services/Providers/RouterProvider.php @@ -13,10 +13,10 @@ namespace Phalcon\Api\Domain\Services\Providers; -use Phalcon\Api\Domain\Interfaces\DomainInterface; -use Phalcon\Api\Domain\Interfaces\ResponderInterface; +use Phalcon\Api\Domain\ADR\Action\ActionHandler; +use Phalcon\Api\Domain\ADR\Domain\DomainInterface; +use Phalcon\Api\Domain\ADR\Responder\ResponderInterface; use Phalcon\Api\Domain\Interfaces\RoutesInterface; -use Phalcon\Api\Domain\Services\ActionHandler; use Phalcon\Api\Domain\Services\Container; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; From 8a8e7913a55cbeda4ea8f97ec374a072a38a3073 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 28 Sep 2025 13:49:28 -0500 Subject: [PATCH 058/301] [#.x] - tests and more tests! --- tests/Unit/AbstractUnitTestCase.php | 12 ++ tests/Unit/Domain/Hello/HelloServiceTest.php | 82 +++++++++++ .../Middleware/HealthMiddlewareTest.php | 56 ++++++++ .../Middleware/NotFoundMiddlewareTest.php | 78 ++++++++++ .../ResponseSenderMiddlewareTest.php | 58 ++++++++ tests/Unit/Domain/Services/ContainerTest.php | 44 ++++++ .../Domain/Services/Env/EnvManagerTest.php | 48 +++++++ .../Services/Http/HttpCodesEnumTest.php | 98 +++++++++++++ .../Domain/Services/Http/ResponseTest.php | 112 +++++++++++++++ .../Providers/ErrorHandlerProviderTest.php | 133 ++++++++++++++++++ .../Services/Providers/RouterProviderTest.php | 60 ++++++++ .../Services/Providers/RoutesEnumTest.php | 67 +++++++++ 12 files changed, 848 insertions(+) create mode 100644 tests/Unit/Domain/Hello/HelloServiceTest.php create mode 100644 tests/Unit/Domain/Middleware/HealthMiddlewareTest.php create mode 100644 tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php create mode 100644 tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php create mode 100644 tests/Unit/Domain/Services/ContainerTest.php create mode 100644 tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php create mode 100644 tests/Unit/Domain/Services/Http/ResponseTest.php create mode 100644 tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php create mode 100644 tests/Unit/Domain/Services/Providers/RouterProviderTest.php create mode 100644 tests/Unit/Domain/Services/Providers/RoutesEnumTest.php diff --git a/tests/Unit/AbstractUnitTestCase.php b/tests/Unit/AbstractUnitTestCase.php index d248af8..4031277 100644 --- a/tests/Unit/AbstractUnitTestCase.php +++ b/tests/Unit/AbstractUnitTestCase.php @@ -17,6 +17,18 @@ abstract class AbstractUnitTestCase extends TestCase { + /** + * @param string $fileName + * @param string $stream + * + * @return void + */ + public function assertFileContentsContains(string $fileName, string $stream): void + { + $contents = file_get_contents($fileName); + $this->assertStringContainsString($stream, $contents); + } + /** * Return a long series of strings to be used as a password * diff --git a/tests/Unit/Domain/Hello/HelloServiceTest.php b/tests/Unit/Domain/Hello/HelloServiceTest.php new file mode 100644 index 0000000..56f216e --- /dev/null +++ b/tests/Unit/Domain/Hello/HelloServiceTest.php @@ -0,0 +1,82 @@ + + * + * 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\Hello; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Hello\HelloService; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use PHPUnit\Framework\Attributes\BackupGlobals; + +use function ob_get_clean; +use function ob_start; +use function restore_error_handler; +use function time; + +#[BackupGlobals(true)] +final class HelloServiceTest extends AbstractUnitTestCase +{ + public function testDispatch(): void + { + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'REQUEST_URI' => '/', + ]; + + ob_start(); + require_once EnvManager::appPath('public/index.php'); + $response = ob_get_clean(); + + $contents = json_decode($response, true); + + restore_error_handler(); + + $this->assertArrayHasKey('data', $contents); + $this->assertArrayHasKey('errors', $contents); + + $data = $contents['data']; + $errors = $contents['errors']; + + $expected = []; + $actual = $errors; + $this->assertSame($expected, $actual); + + $expected = 'Hello World!!! - '; + $actual = $data[0]; + $this->assertStringContainsString($expected, $actual); + } + + public function testService(): void + { + $container = new Container(); + /** @var HelloService $service */ + $service = $container->get(Container::HELLO_SERVICE); + + $payload = $service->__invoke(); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('results', $actual); + + $expected = 'Hello World!!! - '; + $actual = $actual['results']; + $this->assertStringContainsString($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php b/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php new file mode 100644 index 0000000..c1f488e --- /dev/null +++ b/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php @@ -0,0 +1,56 @@ + + * + * 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\Middleware; + +use Phalcon\Api\Domain\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Mvc\Micro; +use PHPUnit\Framework\Attributes\BackupGlobals; + +use function ob_get_clean; +use function ob_start; + +#[BackupGlobals(true)] +final class HealthMiddlewareTest extends AbstractUnitTestCase +{ + public function testCall(): void + { + $container = new Container(); + $application = new Micro($container); + $middleware = new HealthMiddleware(); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'REQUEST_URI' => '/health', + ]; + + ob_start(); + $actual = $middleware->call($application); + $contents = ob_get_clean(); + + $this->assertTrue($actual); + + $contents = json_decode($contents, true); + + $expected = [ + 'status' => 'ok', + 'message' => 'service operational', + ]; + $actual = $contents['data']; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php b/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php new file mode 100644 index 0000000..f0eab64 --- /dev/null +++ b/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php @@ -0,0 +1,78 @@ + + * + * 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\Middleware; + +use Phalcon\Api\Domain\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Events\Event; +use Phalcon\Mvc\Micro; +use PHPUnit\Framework\Attributes\BackupGlobals; + +use function ob_get_clean; +use function ob_start; + +#[BackupGlobals(true)] +final class NotFoundMiddlewareTest extends AbstractUnitTestCase +{ + public function testBeforeNotFound(): void + { + $container = new Container(); + $application = new Micro($container); + $middleware = new NotFoundMiddleware(); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'REQUEST_URI' => '/unknown', + ]; + + ob_start(); + $actual = $middleware->beforeNotFound( + new Event('ev1'), + $application + ); + $contents = ob_get_clean(); + + $this->assertFalse($actual); + + $contents = json_decode($contents, true); + + $expected = [HttpCodesEnum::AppResourceNotFound->error()]; + $actual = $contents['errors']; + $this->assertSame($expected, $actual); + } + + public function testCall(): void + { + $container = new Container(); + $application = new Micro($container); + $middleware = new NotFoundMiddleware(); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'REQUEST_URI' => '/unknown', + ]; + + ob_start(); + $actual = $middleware->call($application); + ob_get_clean(); + + $this->assertTrue($actual); + } +} diff --git a/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php b/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php new file mode 100644 index 0000000..7ab105f --- /dev/null +++ b/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php @@ -0,0 +1,58 @@ + + * + * 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\Middleware; + +use Phalcon\Api\Domain\Middleware\ResponseSenderMiddleware; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Http\Response; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Mvc\Micro; +use PHPUnit\Framework\Attributes\BackupGlobals; + +use function ob_get_clean; +use function ob_start; +use function uniqid; + +#[BackupGlobals(true)] +final class ResponseSenderMiddlewareTest extends AbstractUnitTestCase +{ + public function testCall(): void + { + $container = new Container(); + $application = new Micro($container); + $middleware = new ResponseSenderMiddleware(); + /** @var Response $response */ + $response = $container->getShared(Container::RESPONSE); + + $content = uniqid('content-'); + $response->setContent($content); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'REQUEST_URI' => '/health', + ]; + + ob_start(); + $actual = $middleware->call($application); + $contents = ob_get_clean(); + + $this->assertTrue($actual); + + $expected = $content; + $actual = $contents; + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Services/ContainerTest.php b/tests/Unit/Domain/Services/ContainerTest.php new file mode 100644 index 0000000..1edb010 --- /dev/null +++ b/tests/Unit/Domain/Services/ContainerTest.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\Tests\Unit\Domain\Services; + +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Events\Manager as EventsManager; +use Phalcon\Filter\Filter; + +final class ContainerTest extends AbstractUnitTestCase +{ + public function testContainerEventManager(): void + { + $container = new Container(); + + $actual = $container->has(Container::EVENTS_MANAGER); + $this->assertTrue($actual); + + $eventsManager = $container->getShared(Container::EVENTS_MANAGER); + $this->assertInstanceOf(EventsManager::class, $eventsManager); + } + + public function testContainerFilter(): void + { + $container = new Container(); + + $actual = $container->has(Container::FILTER); + $this->assertTrue($actual); + + $eventsManager = $container->getShared(Container::FILTER); + $this->assertInstanceOf(Filter::class, $eventsManager); + } +} diff --git a/tests/Unit/Domain/Services/Env/EnvManagerTest.php b/tests/Unit/Domain/Services/Env/EnvManagerTest.php index d299e06..0b891f2 100644 --- a/tests/Unit/Domain/Services/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Services/Env/EnvManagerTest.php @@ -21,6 +21,38 @@ #[BackupGlobals(true)] final class EnvManagerTest extends AbstractUnitTestCase { + public function testAppEnvReturnsDefault(): void + { + $expected = 'development'; + $actual = EnvManager::appEnv(); + $this->assertSame($expected, $actual); + } + + public function testAppEnvReturnsValue(): void + { + $_ENV = ['APP_ENV' => 'production']; + + $expected = 'production'; + $actual = EnvManager::appEnv(); + $this->assertSame($expected, $actual); + } + + public function testAppLogLevelReturnsDefault(): void + { + $expected = 1; + $actual = EnvManager::appLogLevel(); + $this->assertSame($expected, $actual); + } + + public function testAppLogLevelReturnsValue(): void + { + $_ENV = ['APP_LOG_LEVEL' => 5]; + + $expected = 5; + $actual = EnvManager::appLogLevel(); + $this->assertSame($expected, $actual); + } + public function testAppPathReturnsRoot(): void { $expected = dirname(__DIR__, 5); @@ -28,6 +60,22 @@ public function testAppPathReturnsRoot(): void $this->assertSame($expected, $actual); } + public function testAppTimezoneReturnsDefault(): void + { + $expected = 'UTC'; + $actual = EnvManager::appTimezone(); + $this->assertSame($expected, $actual); + } + + public function testAppTimezoneReturnsValue(): void + { + $_ENV = ['APP_TIMEZONE' => 'America/Los_Angeles']; + + $expected = 'America/Los_Angeles'; + $actual = EnvManager::appTimezone(); + $this->assertSame($expected, $actual); + } + public function testGetFromDotEnvLoad(): void { $_ENV = [ diff --git a/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php b/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php new file mode 100644 index 0000000..d381c16 --- /dev/null +++ b/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php @@ -0,0 +1,98 @@ + + * + * 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\Http; + +use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +final class HttpCodesEnumTest extends AbstractUnitTestCase +{ + public static function getExamples(): array + { + return [ + [ + HttpCodesEnum::OK, + 200, + 'OK', + ], + [ + HttpCodesEnum::BadRequest, + 400, + 'Bad Request', + ], + [ + HttpCodesEnum::NotFound, + 404, + 'Not Found', + ], + [ + HttpCodesEnum::Unauthorized, + 401, + 'Unauthorized', + ], + [ + HttpCodesEnum::AppMalformedPayload, + 3400, + 'Malformed payload', + ], + [ + HttpCodesEnum::AppRecordsNotFound, + 3401, + 'Record(s) not found', + ], + [ + HttpCodesEnum::AppResourceNotFound, + 3402, + 'Resource not found', + ], + [ + HttpCodesEnum::AppUnauthorized, + 3403, + 'Unauthorized', + ], + [ + HttpCodesEnum::AppInvalidArguments, + 3409, + 'Invalid arguments provided', + ], + ]; + } + + public function testCheckCount(): void + { + $expected = 9; + $actual = HttpCodesEnum::cases(); + $this->assertCount($expected, $actual); + } + + #[DataProvider('getExamples')] + public function testCheckItems( + HttpCodesEnum $element, + int $value, + string $text + ) { + $expected = $value; + $actual = $element->value; + $this->assertSame($expected, $actual); + + $expected = $text; + $actual = $element->text(); + $this->assertSame($expected, $actual); + + $expected = [$value => $text]; + $actual = $element->error(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Services/Http/ResponseTest.php b/tests/Unit/Domain/Services/Http/ResponseTest.php new file mode 100644 index 0000000..45b97d0 --- /dev/null +++ b/tests/Unit/Domain/Services/Http/ResponseTest.php @@ -0,0 +1,112 @@ + + * + * 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\Http; + +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Http\Response; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; + +use function ob_get_clean; +use function ob_start; +use function uniqid; + +final class ResponseTest extends AbstractUnitTestCase +{ + public function testWithPayloadMeta(): void + { + $container = new Container(); + /** @var Response $response */ + $response = $container->getShared(Container::RESPONSE); + + $message = uniqid('message-'); + $key = uniqid('key-'); + $value = uniqid('value-'); + + $response + ->withCode(404, $message) + ->withPayloadData([$key => $value]) + ; + + ob_start(); + $response->render()->send(); + $data = ob_get_clean(); + + $expected = 404; + $actual = $response->getStatusCode(); + $this->assertSame($expected, $actual); + + $expected = $message; + $actual = $response->getReasonPhrase(); + $this->assertSame($expected, $actual); + + /** + * Remove the timestamp and hash because they are always changing + */ + $data = json_decode($data, true); + unset($data['meta']['timestamp']); + unset($data['meta']['hash']); + + $expected = [ + 'data' => [ + $key => $value, + ], + 'errors' => [], + 'meta' => [ + 'code' => 200, + 'message' => 'success', + ], + ]; + $actual = $data; + $this->assertSame($expected, $actual); + } + + public function testWithPayloadMetaErrors(): void + { + $container = new Container(); + /** @var Response $response */ + $response = $container->getShared(Container::RESPONSE); + + $message = uniqid('message-'); + + $response + ->withPayloadErrors([[$message]]) + ; + + ob_start(); + $response->render()->send(); + $data = ob_get_clean(); + + /** + * Remove the timestamp and hash because they are always changing + */ + $data = json_decode($data, true); + unset($data['meta']['timestamp']); + unset($data['meta']['hash']); + + $expected = [ + 'data' => [], + 'errors' => [ + [ + $message, + ], + ], + 'meta' => [ + 'code' => 3000, + 'message' => 'error', + ], + ]; + $actual = $data; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php new file mode 100644 index 0000000..0b1154a --- /dev/null +++ b/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php @@ -0,0 +1,133 @@ + + * + * 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\Providers; + +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Domain\Services\Providers\ErrorHandlerProvider; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use PHPUnit\Framework\Attributes\BackupGlobals; +use ReflectionClass; + +use function date_default_timezone_get; +use function hrtime; +use function restore_error_handler; +use function trigger_error; +use function uniqid; + +use const E_ALL; + +#[BackupGlobals(true)] +final class ErrorHandlerProviderTest extends AbstractUnitTestCase +{ + public function testCheckRegistration(): void + { + $container = new Container(); + $now = hrtime(true); + $container->set( + Container::TIME, + function () use ($now) { + return $now; + }, + true + ); + + $provider = new ErrorHandlerProvider(); + $provider->register($container); + + $expected = date_default_timezone_get(); + $actual = EnvManager::appTimezone(); + $this->assertSame($expected, $actual); + + $expected = 'On'; + $actual = ini_get('display_errors'); + $this->assertSame($expected, $actual); + + $expected = E_ALL; + $actual = (int)ini_get('error_reporting'); + $this->assertSame($expected, $actual); + + restore_error_handler(); + } + + public function testRegisterSetsHandlersAndLogs(): void + { + $container = new Container(); + $now = hrtime(true); + $container->set( + Container::TIME, + function () use ($now) { + return $now; + }, + true + ); + + $provider = new ErrorHandlerProvider(); + $provider->register($container); + + $message = uniqid('msg-'); + // Trigger an error + @trigger_error($message); + + restore_error_handler(); + + /** @var string $logName */ + $logName = EnvManager::get('LOG_FILENAME', 'rest-api'); + /** @var string $logPath */ + $logPath = EnvManager::get('LOG_PATH', 'storage/logs/'); + $logFile = EnvManager::appPath($logPath) . '/' . $logName . '.log'; + + $this->assertFileContentsContains($logFile, $message); + } + + public function testRegisterShutdown(): void + { + /** @var string $logName */ + $logName = EnvManager::get('LOG_FILENAME', 'rest-api'); + /** @var string $logPath */ + $logPath = EnvManager::get('LOG_PATH', 'storage/logs/'); + $logFile = EnvManager::appPath($logPath) . '/' . $logName . '.log'; + + $_ENV['APP_ENV'] = 'development'; + $_ENV['APP_LOG_LEVEL'] = 2; + + $container = new Container(); + $now = hrtime(true); + $container->set( + Container::TIME, + function () use ($now) { + return $now; + }, + true + ); + + $provider = new ErrorHandlerProvider(); + $provider->register($container); + + // Directly call shutdown for coverage and checking if it works + $reflection = new ReflectionClass($provider); + $method = $reflection->getMethod('onShutdown'); + $method->setAccessible(true); + $shutdown = $method->invoke($provider, $container); + + restore_error_handler(); + + $this->assertTrue($shutdown); + + $this->assertFileExists($logFile); + + $message = 'Shutdown'; + $this->assertFileContentsContains($logFile, $message); + } +} diff --git a/tests/Unit/Domain/Services/Providers/RouterProviderTest.php b/tests/Unit/Domain/Services/Providers/RouterProviderTest.php new file mode 100644 index 0000000..c6952c1 --- /dev/null +++ b/tests/Unit/Domain/Services/Providers/RouterProviderTest.php @@ -0,0 +1,60 @@ + + * + * 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\Providers; + +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Providers\RouterProvider; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Mvc\Micro; + +final class RouterProviderTest extends AbstractUnitTestCase +{ + public function testCheckRegistration(): void + { + $container = new Container(); + $application = new Micro($container); + $container->setShared(Container::APPLICATION, $application); + + $provider = new RouterProvider(); + $provider->register($container); + + $router = $application->getRouter(); + $routes = $router->getRoutes(); + + $data = [ + [ + 'method' => 'GET', + 'pattern' => '/', + ], + [ + 'method' => 'GET', + 'pattern' => '/health', + ], + ]; + + $expected = count($data); + $actual = count($routes); + $this->assertSame($expected, $actual); + + foreach ($data as $index => $route) { + $expected = $route['method']; + $actual = $routes[$index]->getHttpMethods(); + $this->assertSame($expected, $actual); + + $expected = $route['pattern']; + $actual = $routes[$index]->getPattern(); + $this->assertSame($expected, $actual); + } + } +} diff --git a/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php b/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php new file mode 100644 index 0000000..e7075dd --- /dev/null +++ b/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php @@ -0,0 +1,67 @@ + + * + * 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\Providers; + +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Providers\RoutesEnum; +use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +final class RoutesEnumTest extends AbstractUnitTestCase +{ + public static function getExamples(): array + { + return [ + [ + RoutesEnum::helloGet, + '', + '/', + RoutesEnum::GET, + Container::HELLO_SERVICE, + ], + ]; + } + + public function testCheckCount(): void + { + $expected = 1; + $actual = RoutesEnum::cases(); + $this->assertCount($expected, $actual); + } + + #[DataProvider('getExamples')] + public function testCheckItems( + RoutesEnum $element, + string $value, + string $endpoint, + string $method, + string $service + ) { + $expected = $value; + $actual = $element->value; + $this->assertSame($expected, $actual); + + $expected = $endpoint; + $actual = $element->endpoint(); + $this->assertSame($expected, $actual); + + $expected = $method; + $actual = $element->method(); + $this->assertSame($expected, $actual); + + $expected = $service; + $actual = $element->service(); + $this->assertSame($expected, $actual); + } +} From 5a2354b877fe09e615dabba92d5a5dff74048b05 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:04:04 -0500 Subject: [PATCH 059/301] [#.x] - added user endpoint in routes and fixed tests --- src/Domain/Services/Providers/RouterProvider.php | 13 ++++++++++++- src/Domain/Services/Providers/RoutesEnum.php | 5 ++++- .../Services/Providers/RouterProviderTest.php | 6 +++++- .../Domain/Services/Providers/RoutesEnumTest.php | 11 +++++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/Domain/Services/Providers/RouterProvider.php b/src/Domain/Services/Providers/RouterProvider.php index ee17143..fe13b29 100644 --- a/src/Domain/Services/Providers/RouterProvider.php +++ b/src/Domain/Services/Providers/RouterProvider.php @@ -18,9 +18,11 @@ use Phalcon\Api\Domain\ADR\Responder\ResponderInterface; use Phalcon\Api\Domain\Interfaces\RoutesInterface; use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Services\Http\Response; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Events\Manager as EventsManager; +use Phalcon\Http\Request; use Phalcon\Mvc\Micro; use Phalcon\Mvc\Micro\Collection; @@ -75,6 +77,10 @@ private function attachMiddleware( */ private function attachRoutes(Micro $application): void { + /** @var Request $request */ + $request = $application->getService(Container::REQUEST); + /** @var Response $response */ + $response = $application->getService(Container::RESPONSE); /** @var ResponderInterface $responder */ $responder = $application->getService(Container::RESPONDER_JSON); @@ -84,7 +90,12 @@ private function attachRoutes(Micro $application): void $collection = new Collection(); /** @var DomainInterface $service */ $service = $application->getService($serviceName); - $action = new ActionHandler($service, $responder); + $action = new ActionHandler( + $request, + $response, + $service, + $responder + ); $collection ->setHandler($action) diff --git a/src/Domain/Services/Providers/RoutesEnum.php b/src/Domain/Services/Providers/RoutesEnum.php index adad412..3dc6b1c 100644 --- a/src/Domain/Services/Providers/RoutesEnum.php +++ b/src/Domain/Services/Providers/RoutesEnum.php @@ -24,6 +24,7 @@ enum RoutesEnum: string implements RoutesInterface { case helloGet = ''; + case userGet = 'user'; /** * @return string @@ -39,7 +40,8 @@ public function endpoint(): string public function method(): string { return match ($this) { - self::helloGet => self::GET, + self::helloGet, + self::userGet => self::GET, }; } @@ -67,6 +69,7 @@ public function service(): string { return match ($this) { self::helloGet => Container::HELLO_SERVICE, + self::userGet => Container::USER_GET_SERVICE, }; } diff --git a/tests/Unit/Domain/Services/Providers/RouterProviderTest.php b/tests/Unit/Domain/Services/Providers/RouterProviderTest.php index c6952c1..77810d6 100644 --- a/tests/Unit/Domain/Services/Providers/RouterProviderTest.php +++ b/tests/Unit/Domain/Services/Providers/RouterProviderTest.php @@ -15,7 +15,7 @@ use Phalcon\Api\Domain\Services\Container; use Phalcon\Api\Domain\Services\Providers\RouterProvider; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; final class RouterProviderTest extends AbstractUnitTestCase @@ -37,6 +37,10 @@ public function testCheckRegistration(): void 'method' => 'GET', 'pattern' => '/', ], + [ + 'method' => 'GET', + 'pattern' => '/user', + ], [ 'method' => 'GET', 'pattern' => '/health', diff --git a/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php b/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php index e7075dd..43c35fa 100644 --- a/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php +++ b/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php @@ -15,7 +15,7 @@ use Phalcon\Api\Domain\Services\Container; use Phalcon\Api\Domain\Services\Providers\RoutesEnum; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; final class RoutesEnumTest extends AbstractUnitTestCase @@ -30,12 +30,19 @@ public static function getExamples(): array RoutesEnum::GET, Container::HELLO_SERVICE, ], + [ + RoutesEnum::userGet, + 'user', + '/user', + RoutesEnum::GET, + Container::USER_GET_SERVICE, + ], ]; } public function testCheckCount(): void { - $expected = 1; + $expected = 2; $actual = RoutesEnum::cases(); $this->assertCount($expected, $actual); } From 3fae00fa8a870a468894009b0014e285c607c700 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:04:31 -0500 Subject: [PATCH 060/301] [#.x] - new input class --- src/Domain/ADR/Domain/Input.php | 35 ++++++++++++++++++++++++ src/Domain/ADR/Domain/InputInterface.php | 29 ++++++++++++++++++++ src/Domain/ADR/Domain/InputTypes.php | 25 +++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 src/Domain/ADR/Domain/Input.php create mode 100644 src/Domain/ADR/Domain/InputInterface.php create mode 100644 src/Domain/ADR/Domain/InputTypes.php diff --git a/src/Domain/ADR/Domain/Input.php b/src/Domain/ADR/Domain/Input.php new file mode 100644 index 0000000..7d15456 --- /dev/null +++ b/src/Domain/ADR/Domain/Input.php @@ -0,0 +1,35 @@ + + * + * 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\ADR\Domain; + +use Phalcon\Http\Request; + +/** + * @phpstan-import-type TRequestQuery from InputTypes + */ +final class Input implements InputInterface +{ + /** + * @param Request $request + * + * @return TRequestQuery + */ + public function __invoke(Request $request): array + { + /** @var TRequestQuery $query */ + $query = $request->getQuery(); + + return $query; + } +} diff --git a/src/Domain/ADR/Domain/InputInterface.php b/src/Domain/ADR/Domain/InputInterface.php new file mode 100644 index 0000000..d4a4b22 --- /dev/null +++ b/src/Domain/ADR/Domain/InputInterface.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\Domain\ADR\Domain; + +use Phalcon\Http\Request; + +/** + * @phpstan-import-type TRequestQuery from InputTypes + */ +interface InputInterface +{ + /** + * @param Request $request + * + * @return TRequestQuery + */ + public function __invoke(Request $request): array; +} diff --git a/src/Domain/ADR/Domain/InputTypes.php b/src/Domain/ADR/Domain/InputTypes.php new file mode 100644 index 0000000..f85bfce --- /dev/null +++ b/src/Domain/ADR/Domain/InputTypes.php @@ -0,0 +1,25 @@ + + * + * 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\ADR\Domain; + +/** + * @phpstan-type THelloInput array{} + * @phpstan-type TUserInput array{ + * userId?: int + * } + * @phpstan-type TRequestQuery array + */ +final class InputTypes +{ +} From 31c252e9e913733552553c3f47684816a17cef57 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:05:04 -0500 Subject: [PATCH 061/301] [#.x] - moved abstract test class --- tests/AbstractUnitTestCase.php | 125 ++++++++++++++++++++++++++++ tests/Unit/AbstractUnitTestCase.php | 41 --------- 2 files changed, 125 insertions(+), 41 deletions(-) create mode 100644 tests/AbstractUnitTestCase.php delete mode 100644 tests/Unit/AbstractUnitTestCase.php diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php new file mode 100644 index 0000000..8bee0db --- /dev/null +++ b/tests/AbstractUnitTestCase.php @@ -0,0 +1,125 @@ + + * + * 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; + +use PDO; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\DataMapper\Pdo\Connection; +use PHPUnit\Framework\TestCase; + +use function uniqid; + +abstract class AbstractUnitTestCase extends TestCase +{ + protected ?Connection $connection = null; + + /** + * @param string $fileName + * @param string $stream + * + * @return void + */ + public function assertFileContentsContains(string $fileName, string $stream): void + { + $contents = file_get_contents($fileName); + $this->assertStringContainsString($stream, $contents); + } + + public function assertInDatabase(string $table, array $criteria = []): void + { + $records = $this->getFromDatabase($table, $criteria); + + $this->assertNotEmpty($records); + } + + public function assertNotInDatabase(string $table, array $criteria = []): void + { + $records = $this->getFromDatabase($table, $criteria); + + $this->assertEmpty($records); + } + + /** + * @param UsersMigration $migration + * @param array $fields + * + * @return array + */ + public function getNewUser(UsersMigration $migration, array $fields = []): array + { + $status = $fields['usr_status_flag'] ?? 1; + $username = $fields['usr_username'] ?? uniqid('name-'); + $password = $fields['usr_password'] ?? $this->getStrongPassword(); + + $migration->insert(null, $status, $username, $password); + + $dbUser = $this->getFromDatabase( + 'co_users', + [ + 'usr_username' => $username, + ] + ); + + return $dbUser[0]; + } + + /** + * 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); + } + + /** + * @param Connection $connection + * + * @return void + */ + public function setConnection(Connection $connection): void + { + $this->connection = $connection; + } + + /** + * @param string $table + * @param array $criteria + * + * @return array + */ + protected function getFromDatabase( + string $table, + array $criteria + ): array { + $sql = 'SELECT * FROM ' . $table . ' WHERE '; + $where = []; + foreach ($criteria as $key => $value) { + $val = $value; + if (true === is_string($value)) { + $val = '"' . $value . '"'; + } + + $where[] = $key . ' = ' . $val; + } + + $sql .= implode(' AND ', $where); + + $result = $this->connection?->query($sql); + $records = $result?->fetchAll(PDO::FETCH_ASSOC); + + return $records; + } +} diff --git a/tests/Unit/AbstractUnitTestCase.php b/tests/Unit/AbstractUnitTestCase.php deleted file mode 100644 index 4031277..0000000 --- a/tests/Unit/AbstractUnitTestCase.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * 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 -{ - /** - * @param string $fileName - * @param string $stream - * - * @return void - */ - public function assertFileContentsContains(string $fileName, string $stream): void - { - $contents = file_get_contents($fileName); - $this->assertStringContainsString($stream, $contents); - } - - /** - * 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); - } -} From 3035432ff69307e689a6b9c87ba686eaf42164aa Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:05:19 -0500 Subject: [PATCH 062/301] [#.x] - new test migration class --- .../Domain/Migrations/AbstractMigration.php | 43 +++++++++++++++++ .../Domain/Migrations/UsersMigration.php | 46 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 tests/Fixtures/Domain/Migrations/AbstractMigration.php create mode 100644 tests/Fixtures/Domain/Migrations/UsersMigration.php diff --git a/tests/Fixtures/Domain/Migrations/AbstractMigration.php b/tests/Fixtures/Domain/Migrations/AbstractMigration.php new file mode 100644 index 0000000..b4d39b7 --- /dev/null +++ b/tests/Fixtures/Domain/Migrations/AbstractMigration.php @@ -0,0 +1,43 @@ + + * + * 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\Fixtures\Domain\Migrations; + +use Phalcon\DataMapper\Pdo\Connection; + +abstract class AbstractMigration +{ + protected string $table = ''; + + public function __construct( + protected readonly Connection $connection + ) { + $this->clear(); + } + + /** + * @return int + */ + public function clear(): int + { + return $this->connection->exec('DELETE FROM ' . $this->table); + } + + /** + * @return string + */ + public function getTable(): string + { + return $this->table; + } +} diff --git a/tests/Fixtures/Domain/Migrations/UsersMigration.php b/tests/Fixtures/Domain/Migrations/UsersMigration.php new file mode 100644 index 0000000..41d0d29 --- /dev/null +++ b/tests/Fixtures/Domain/Migrations/UsersMigration.php @@ -0,0 +1,46 @@ + + * + * 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\Fixtures\Domain\Migrations; + +use PHPUnit\Framework\Assert; + +final class UsersMigration extends AbstractMigration +{ + protected string $table = 'co_users'; + + public function insert( + ?int $id = null, + int $status = 0, + ?string $username = null, + ?string $password = null, + ) { + $id = $id ?: 'null'; + $sql = <<table} ( + usr_id, usr_status_flag, usr_username, usr_password +) VALUES ( + $id, $status, '$username', '$password' +) +SQL; + + $result = $this->connection->exec($sql); + if (!$result) { + Assert::fail( + "Failed to insert id [#$id] into table [$this->table]" + ); + } + + return $result; + } +} From a91d1469517d8906312f0790d6abb3993a56aa90 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:05:38 -0500 Subject: [PATCH 063/301] [#.x] - added user transport class --- src/Domain/DataSource/User/UserTransport.php | 77 ++++++++++++++++++++ src/Domain/DataSource/User/UserTypes.php | 34 +++++++++ 2 files changed, 111 insertions(+) create mode 100644 src/Domain/DataSource/User/UserTransport.php create mode 100644 src/Domain/DataSource/User/UserTypes.php diff --git a/src/Domain/DataSource/User/UserTransport.php b/src/Domain/DataSource/User/UserTransport.php new file mode 100644 index 0000000..21a9a93 --- /dev/null +++ b/src/Domain/DataSource/User/UserTransport.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\DataSource\User; + +use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; + +/** + * @method int getId() + * @method int getStatus() + * @method string getUsername() + * @method string getPassword() + * + * @phpstan-import-type TUserRecord from UserTypes + * @phpstan-import-type TUserTransport from UserTypes + */ +final class UserTransport +{ + /** @var TUserTransport */ + private array $store; + + /** + * @param TUserRecord $input + */ + public function __construct(array $input) + { + $this->store = [ + 'id' => (int)($input['usr_id'] ?? 0), + 'status' => (int)($input['usr_status_flag'] ?? 0), + 'username' => (string)($input['usr_username'] ?? ''), + 'password' => (string)($input['usr_password'] ?? ''), + ]; + } + + /** + * @param string $name + * @param array $arguments + * + * @return int|string + */ + public function __call(string $name, array $arguments): int | string + { + return match ($name) { + 'getId' => $this->store['id'], + 'getStatus' => $this->store['status'], + 'getUsername' => $this->store['username'], + 'getPassword' => $this->store['password'], + default => throw new InvalidConfigurationArgumentException( + 'The ' . $name . ' method is not supported. [' + . json_encode($arguments) . ']', + ), + }; + } + + public function isEmpty(): bool + { + return 0 === $this->store['id']; + } + + /** + * @return array + */ + public function toArray(): array + { + return [$this->store['id'] => $this->store]; + } +} diff --git a/src/Domain/DataSource/User/UserTypes.php b/src/Domain/DataSource/User/UserTypes.php new file mode 100644 index 0000000..a8997df --- /dev/null +++ b/src/Domain/DataSource/User/UserTypes.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\DataSource\User; + + +/** + * @phpstan-type TUserRecord array{}|array{ + * usr_id: int, + * usr_status_flag: int, + * usr_username: string, + * usr_password: string + * } + * + * @phpstan-type TUserTransport array{ + * id: int, + * status: int, + * username: string, + * password: string + * } + */ +final class UserTypes +{ +} From 127132e0394d34acce341167ef8a6ca68913d63d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:06:05 -0500 Subject: [PATCH 064/301] [#.x] - registering services --- src/Domain/Services/Container.php | 121 +++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 35 deletions(-) diff --git a/src/Domain/Services/Container.php b/src/Domain/Services/Container.php index 91c1a29..3916969 100644 --- a/src/Domain/Services/Container.php +++ b/src/Domain/Services/Container.php @@ -14,13 +14,15 @@ namespace Phalcon\Api\Domain\Services; use Phalcon\Api\Domain\ADR\Responder\JsonResponder; -use Phalcon\Api\Domain\Health\HealthService; +use Phalcon\Api\Domain\DataSource\User\UserRepository; use Phalcon\Api\Domain\Hello\HelloService; use Phalcon\Api\Domain\Middleware\HealthMiddleware; use Phalcon\Api\Domain\Middleware\NotFoundMiddleware; use Phalcon\Api\Domain\Middleware\ResponseSenderMiddleware; use Phalcon\Api\Domain\Services\Env\EnvManager; use Phalcon\Api\Domain\Services\Http\Response; +use Phalcon\Api\Domain\User\UserGetService; +use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Di\Di; use Phalcon\Di\Service; use Phalcon\Events\Manager as EventsManager; @@ -42,24 +44,10 @@ class Container extends Di public const EVENTS_MANAGER = 'eventsManager'; /** @var string */ public const FILTER = 'filter'; - /** - * Services - */ - public const HELLO_SERVICE = 'hello.service'; /** @var string */ public const LOGGER = 'logger'; - /** - * Middleware - */ - public const MIDDLEWARE_HEALTH = 'middleware.health'; - public const MIDDLEWARE_NOT_FOUND = 'middleware.not.found'; - public const MIDDLEWARE_RESPONSE_SENDER = 'middleware.response.sender'; /** @var string */ public const REQUEST = 'request'; - /** - * Responders - */ - public const RESPONDER_JSON = 'hello.responder.json'; /** @var string */ public const RESPONSE = 'response'; /** @var string */ @@ -67,28 +55,83 @@ class Container extends Di /** @var string */ public const TIME = 'time'; + /** + * Services + */ + public const HELLO_SERVICE = HelloService::class; + public const USER_GET_SERVICE = 'service.user.get'; + /** + * Middleware + */ + public const MIDDLEWARE_HEALTH = HealthMiddleware::class; + public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; + public const MIDDLEWARE_RESPONSE_SENDER = ResponseSenderMiddleware::class; + /** + * Repositories + */ + public const REPOSITORY_USER = 'repository.user'; + /** + * Responders + */ + public const RESPONDER_JSON = JsonResponder::class; + public function __construct() { $this->services = [ + self::CONNECTION => $this->getServiceConnection(), self::EVENTS_MANAGER => $this->getServiceEventsManger(), self::FILTER => $this->getServiceFilter(), self::LOGGER => $this->getServiceLogger(), - self::REQUEST => $this->getServiceSimple(Request::class, true), - self::RESPONSE => $this->getServiceSimple(Response::class, true), + self::REQUEST => new Service(Request::class, true), + self::RESPONSE => new Service(Response::class, true), self::ROUTER => $this->getServiceRouter(), - self::HELLO_SERVICE => $this->getServiceSimple(HelloService::class), - - self::MIDDLEWARE_HEALTH => $this->getServiceSimple(HealthMiddleware::class), - self::MIDDLEWARE_NOT_FOUND => $this->getServiceSimple(NotFoundMiddleware::class), - self::MIDDLEWARE_RESPONSE_SENDER => $this->getServiceSimple(ResponseSenderMiddleware::class), - - self::RESPONDER_JSON => $this->getServiceResponderJson(), + self::USER_GET_SERVICE => $this->getServiceUserGet(), + self::REPOSITORY_USER => $this->getServiceRepositoryUser(), ]; parent::__construct(); } + /** + * @return Service + */ + private function getServiceConnection(): Service + { + return new Service( + function () { + /** @var string $dbName */ + $dbName = EnvManager::get('DB_NAME','phalcon'); + /** @var string $host */ + $host = EnvManager::get('DB_HOST','rest-db'); + /** @var string $password */ + $password = EnvManager::get('DB_PASS','secret'); + $port = (int)EnvManager::get('DB_PORT',3306); + /** @var string $username */ + $username = EnvManager::get('DB_USER','root'); + /** @var string $encoding */ + $encoding = EnvManager::get('DB_CHARSET','utf8'); + $queries = ['SET NAMES utf8mb4']; + $dsn = sprintf( + 'mysql:host=%s;port=%s;dbname=%s;charset=%s', + $host, + $port, + $dbName, + $encoding + ); + + return new Connection( + $dsn, + $username, + $password, + [], + $queries + ); + }, + true + ); + } + /** * @return Service */ @@ -141,15 +184,18 @@ function () use ($logName, $logFile) { ); } - private function getServiceResponderJson(): Service + /** + * @return Service + */ + private function getServiceRepositoryUser(): Service { return new Service( [ - 'className' => JsonResponder::class, + 'className' => UserRepository::class, 'arguments' => [ [ 'type' => 'service', - 'name' => self::RESPONSE, + 'name' => self::CONNECTION, ], ], ] @@ -175,15 +221,20 @@ private function getServiceRouter(): Service } /** - * @param string $className - * @param bool $isShared - * * @return Service */ - private function getServiceSimple( - string $className, - bool $isShared = false - ): Service { - return new Service($className, $isShared); + private function getServiceUserGet(): Service + { + return new Service( + [ + 'className' => UserGetService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::REPOSITORY_USER, + ], + ], + ] + ); } } From 350a5a16cd8b47f1da8398d7ffd65e4a1273b087 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:06:25 -0500 Subject: [PATCH 065/301] [#.x] - refactoring responders --- src/Domain/ADR/Responder/JsonResponder.php | 31 ++++++++++++------- .../ADR/Responder/ResponderInterface.php | 6 +++- src/Domain/ADR/Responder/ResponderTypes.php | 24 ++++++++++++++ 3 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 src/Domain/ADR/Responder/ResponderTypes.php diff --git a/src/Domain/ADR/Responder/JsonResponder.php b/src/Domain/ADR/Responder/JsonResponder.php index 0e9cec1..3afbfeb 100644 --- a/src/Domain/ADR/Responder/JsonResponder.php +++ b/src/Domain/ADR/Responder/JsonResponder.php @@ -13,28 +13,35 @@ namespace Phalcon\Api\Domain\ADR\Responder; +use Exception as BaseException; use Phalcon\Api\Domain\Services\Http\Response; use Phalcon\Domain\Payload; +/** + * @phpstan-import-type TResult from ResponderTypes + */ final class JsonResponder implements ResponderInterface { - public function __construct( - private Response $response - ) { - } - - public function __invoke(Payload $payload): Response - { + /** + * @param Response $response + * @param Payload $payload + * + * @return Response + * @throws BaseException + */ + public function __invoke( + Response $response, + Payload $payload + ): Response { $result = $payload->getResult(); - /** @var string $content */ + /** @var TResult $content */ $content = $result['results']; - $this - ->response - ->withPayloadData([$content]) + $response + ->withPayloadData($content) ->render() ; - return $this->response; + return $response; } } diff --git a/src/Domain/ADR/Responder/ResponderInterface.php b/src/Domain/ADR/Responder/ResponderInterface.php index c96e0db..a458a05 100644 --- a/src/Domain/ADR/Responder/ResponderInterface.php +++ b/src/Domain/ADR/Responder/ResponderInterface.php @@ -13,10 +13,14 @@ namespace Phalcon\Api\Domain\ADR\Responder; +use Phalcon\Api\Domain\Services\Http\Response; use Phalcon\Domain\Payload; use Phalcon\Http\ResponseInterface; interface ResponderInterface { - public function __invoke(Payload $payload): ResponseInterface; + public function __invoke( + Response $response, + Payload $payload + ): ResponseInterface; } diff --git a/src/Domain/ADR/Responder/ResponderTypes.php b/src/Domain/ADR/Responder/ResponderTypes.php new file mode 100644 index 0000000..7a895ee --- /dev/null +++ b/src/Domain/ADR/Responder/ResponderTypes.php @@ -0,0 +1,24 @@ + + * + * 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\ADR\Responder; + +/** + * @phpstan-type TResultItem array + * @phpstan-type TResult array{ + * result: array + * } + */ +final class ResponderTypes +{ +} From 265f5a3a4c3a4ae4720d04eabe62e480db4924c7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:06:41 -0500 Subject: [PATCH 066/301] [#.x] - new user repository class --- src/Domain/DataSource/User/UserRepository.php | 50 +++++++++++++++++ .../DataSource/User/UserRepositoryTest.php | 56 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/Domain/DataSource/User/UserRepository.php create mode 100644 tests/Unit/Domain/DataSource/User/UserRepositoryTest.php diff --git a/src/Domain/DataSource/User/UserRepository.php b/src/Domain/DataSource/User/UserRepository.php new file mode 100644 index 0000000..dd4ef84 --- /dev/null +++ b/src/Domain/DataSource/User/UserRepository.php @@ -0,0 +1,50 @@ + + * + * 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\DataSource\User; + +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\DataMapper\Query\Select; + +/** + * @phpstan-import-type TUserRecord from UserTypes + */ +final class UserRepository +{ + public function __construct( + private readonly Connection $connection, + ) { + } + + /** + * @param int|string $userId + * + * @return UserTransport + */ + public function findById(int | string $userId): UserTransport + { + $result = []; + if (true !== empty($userId)) { + $select = Select::new($this->connection); + + /** @var TUserRecord $result */ + $result = $select + ->from('co_users') + ->where('usr_id = ', $userId) + ->fetchOne() + ; + } + + return new UserTransport($result); + } +} diff --git a/tests/Unit/Domain/DataSource/User/UserRepositoryTest.php b/tests/Unit/Domain/DataSource/User/UserRepositoryTest.php new file mode 100644 index 0000000..16f4330 --- /dev/null +++ b/tests/Unit/Domain/DataSource/User/UserRepositoryTest.php @@ -0,0 +1,56 @@ + + * + * 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\DataSource\User; + +use Phalcon\Api\Domain\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\DataMapper\Pdo\Connection; + +final class UserRepositoryTest extends AbstractUnitTestCase +{ + public function testFind(): void + { + $container = new Container(); + /** @var Connection $connection */ + $connection = $container->getShared(Container::CONNECTION); + /** @var UserRepository $repository */ + $repository = $container->get(Container::REPOSITORY_USER); + + $migration = new UsersMigration($connection); + $this->setConnection($connection); + + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + + $user = $repository->findById($userId); + + $expected = $dbUser['usr_id']; + $actual = $user->getId(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_status_flag']; + $actual = $user->getStatus(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_username']; + $actual = $user->getUsername(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_password']; + $actual = $user->getPassword(); + $this->assertSame($expected, $actual); + } +} From fd89a7c42e4b25cf38ae2af84a45b310ffe08f46 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:07:07 -0500 Subject: [PATCH 067/301] [#.x] - refactored domain to accept input --- src/Domain/ADR/Action/ActionHandler.php | 11 ++++++++++- src/Domain/ADR/Domain/DomainInterface.php | 11 ++++++++++- src/Domain/Hello/HelloService.php | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/Domain/ADR/Action/ActionHandler.php b/src/Domain/ADR/Action/ActionHandler.php index 2855f7d..29d604a 100644 --- a/src/Domain/ADR/Action/ActionHandler.php +++ b/src/Domain/ADR/Action/ActionHandler.php @@ -14,11 +14,16 @@ namespace Phalcon\Api\Domain\ADR\Action; use Phalcon\Api\Domain\ADR\Domain\DomainInterface; +use Phalcon\Api\Domain\ADR\Domain\Input; use Phalcon\Api\Domain\ADR\Responder\ResponderInterface; +use Phalcon\Api\Domain\Services\Http\Response; +use Phalcon\Http\Request; final readonly class ActionHandler implements ActionInterface { public function __construct( + private Request $request, + private Response $response, private DomainInterface $service, private ResponderInterface $responder ) { @@ -26,8 +31,12 @@ public function __construct( public function __invoke(): void { + $input = new Input(); + $data = $input->__invoke($this->request); + $this->responder->__invoke( - $this->service->__invoke() + $this->response, + $this->service->__invoke($data) ); } } diff --git a/src/Domain/ADR/Domain/DomainInterface.php b/src/Domain/ADR/Domain/DomainInterface.php index b8747ff..d5c6a20 100644 --- a/src/Domain/ADR/Domain/DomainInterface.php +++ b/src/Domain/ADR/Domain/DomainInterface.php @@ -15,7 +15,16 @@ use Phalcon\Domain\Payload; +/** + * @phpstan-import-type THelloInput from InputTypes + * @phpstan-import-type TUserInput from InputTypes + */ interface DomainInterface { - public function __invoke(): Payload; + /** + * @param THelloInput|TUserInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload; } diff --git a/src/Domain/Hello/HelloService.php b/src/Domain/Hello/HelloService.php index 505dae1..0102a77 100644 --- a/src/Domain/Hello/HelloService.php +++ b/src/Domain/Hello/HelloService.php @@ -15,19 +15,30 @@ use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\Domain\DomainInterface; +use Phalcon\Api\Domain\ADR\Domain\InputTypes; use Phalcon\Api\Domain\Constants\Dates; use Phalcon\Domain\Payload; use function date; +/** + * @phpstan-import-type THelloInput from InputTypes + */ final class HelloService implements DomainInterface { - public function __invoke(): Payload + /** + * @param THelloInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload { return new Payload( DomainStatus::SUCCESS, [ - 'results' => "Hello World!!! - " . date(Dates::DATE_TIME_FORMAT), + 'results' => [ + "Hello World!!! - " . date(Dates::DATE_TIME_FORMAT) + ], ] ); } From 73a9b7f3c4190b6115d6a63ba33186a4d35dc514 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:07:24 -0500 Subject: [PATCH 068/301] [#.x] - added user get service --- src/Domain/User/UserGetService.php | 71 ++++++++++ tests/Unit/Domain/User/UserServiceTest.php | 149 +++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 src/Domain/User/UserGetService.php create mode 100644 tests/Unit/Domain/User/UserServiceTest.php diff --git a/src/Domain/User/UserGetService.php b/src/Domain/User/UserGetService.php new file mode 100644 index 0000000..227b168 --- /dev/null +++ b/src/Domain/User/UserGetService.php @@ -0,0 +1,71 @@ + + * + * 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\User; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\Domain\DomainInterface; +use Phalcon\Api\Domain\ADR\Domain\InputTypes; +use Phalcon\Api\Domain\DataSource\User\UserRepository; +use Phalcon\Domain\Payload; + +use function abs; + +/** + * @phpstan-import-type TUserInput from InputTypes + */ +final readonly class UserGetService implements DomainInterface +{ + public function __construct( + private UserRepository $userRepository + ) { + } + + /** + * @param TUserInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + $userId = abs((int)($input['userId'] ?? 0)); + + /** + * Success + */ + if ($userId > 0) { + $user = $this->userRepository->findById($userId); + + if (true !== $user->isEmpty()) { + return new Payload( + DomainStatus::SUCCESS, + [ + 'results' => $user->toArray(), + ] + ); + } + } + + /** + * 404 + */ + return new Payload( + DomainStatus::NOT_FOUND, + [ + 'results' => [ + 'Record(s) not found', + ], + ] + ); + } +} diff --git a/tests/Unit/Domain/User/UserServiceTest.php b/tests/Unit/Domain/User/UserServiceTest.php new file mode 100644 index 0000000..87c7c02 --- /dev/null +++ b/tests/Unit/Domain/User/UserServiceTest.php @@ -0,0 +1,149 @@ + + * + * 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\User; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\User\UserGetService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\DataMapper\Pdo\Connection; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class UserServiceTest extends AbstractUnitTestCase +{ +// public function testDispatch(): void +// { +// $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); +// $_SERVER = [ +// 'REQUEST_METHOD' => 'GET', +// 'REQUEST_TIME_FLOAT' => $time, +// 'REQUEST_URI' => '/', +// ]; +// +// ob_start(); +// require_once EnvManager::appPath('public/index.php'); +// $response = ob_get_clean(); +// +// $contents = json_decode($response, true); +// +// restore_error_handler(); +// +// $this->assertArrayHasKey('data', $contents); +// $this->assertArrayHasKey('errors', $contents); +// +// $data = $contents['data']; +// $errors = $contents['errors']; +// +// $expected = []; +// $actual = $errors; +// $this->assertSame($expected, $actual); +// +// $expected = 'Hello World!!! - '; +// $actual = $data[0]; +// $this->assertStringContainsString($expected, $actual); +// } +// + public function testServiceEmptyUserId(): void + { + $container = new Container(); + /** @var UserGetService $service */ + $service = $container->get(Container::USER_GET_SERVICE); + + $payload = $service->__invoke([]); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('results', $actual); + + $expected = 'Record(s) not found'; + $actual = $actual['results'][0]; + $this->assertStringContainsString($expected, $actual); + } + + public function testServiceWrongUserId(): void + { + $container = new Container(); + /** @var UserGetService $service */ + $service = $container->get(Container::USER_GET_SERVICE); + + $payload = $service->__invoke( + [ + 'userId' => 999999 + ] + ); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('results', $actual); + + $expected = 'Record(s) not found'; + $actual = $actual['results'][0]; + $this->assertStringContainsString($expected, $actual); + } + + public function testServiceWithUserId(): void + { + $container = new Container(); + /** @var Connection $connection */ + $connection = $container->getShared(Container::CONNECTION); + /** @var UserGetService $service */ + $service = $container->get(Container::USER_GET_SERVICE); + + $migration = new UsersMigration($connection); + $this->setConnection($connection); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + + $payload = $service->__invoke( + [ + 'userId' => $userId + ] + ); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('results', $actual); + + $user = $actual['results']; + $key = array_key_first($user); + $user = $user[$key]; + + $expected = $dbUser['usr_id']; + $actual = $user['id']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_status_flag']; + $actual = $user['status']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_username']; + $actual = $user['username']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_password']; + $actual = $user['password']; + $this->assertSame($expected, $actual); + } +} From d26a743bce8e56f207887557aa0cab9eb8fb1288 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:07:39 -0500 Subject: [PATCH 069/301] [#.x] - test case refactoring and adjusting tests --- tests/Unit/Domain/Hello/HelloServiceTest.php | 6 +++--- tests/Unit/Domain/Middleware/HealthMiddlewareTest.php | 2 +- tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php | 2 +- .../Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php | 2 +- tests/Unit/Domain/Services/ContainerTest.php | 2 +- tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php | 2 +- tests/Unit/Domain/Services/Env/EnvFactoryTest.php | 2 +- tests/Unit/Domain/Services/Env/EnvManagerTest.php | 2 +- tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php | 2 +- tests/Unit/Domain/Services/Http/ResponseTest.php | 2 +- .../Domain/Services/Providers/ErrorHandlerProviderTest.php | 2 +- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/Unit/Domain/Hello/HelloServiceTest.php b/tests/Unit/Domain/Hello/HelloServiceTest.php index 56f216e..64082ac 100644 --- a/tests/Unit/Domain/Hello/HelloServiceTest.php +++ b/tests/Unit/Domain/Hello/HelloServiceTest.php @@ -17,7 +17,7 @@ use Phalcon\Api\Domain\Hello\HelloService; use Phalcon\Api\Domain\Services\Container; use Phalcon\Api\Domain\Services\Env\EnvManager; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; use function ob_get_clean; @@ -66,7 +66,7 @@ public function testService(): void /** @var HelloService $service */ $service = $container->get(Container::HELLO_SERVICE); - $payload = $service->__invoke(); + $payload = $service->__invoke([]); $expected = DomainStatus::SUCCESS; $actual = $payload->getStatus(); @@ -76,7 +76,7 @@ public function testService(): void $this->assertArrayHasKey('results', $actual); $expected = 'Hello World!!! - '; - $actual = $actual['results']; + $actual = $actual['results'][0]; $this->assertStringContainsString($expected, $actual); } } diff --git a/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php b/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php index c1f488e..4d652cb 100644 --- a/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php +++ b/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php @@ -15,7 +15,7 @@ use Phalcon\Api\Domain\Middleware\HealthMiddleware; use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php b/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php index f0eab64..3c167e8 100644 --- a/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php +++ b/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php @@ -16,7 +16,7 @@ use Phalcon\Api\Domain\Middleware\NotFoundMiddleware; use Phalcon\Api\Domain\Services\Container; use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Events\Event; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php b/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php index 7ab105f..5a1984a 100644 --- a/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php +++ b/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php @@ -16,7 +16,7 @@ use Phalcon\Api\Domain\Middleware\ResponseSenderMiddleware; use Phalcon\Api\Domain\Services\Container; use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Services/ContainerTest.php b/tests/Unit/Domain/Services/ContainerTest.php index 1edb010..073e821 100644 --- a/tests/Unit/Domain/Services/ContainerTest.php +++ b/tests/Unit/Domain/Services/ContainerTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services; use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\Filter; diff --git a/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php index 37b47bf..458b147 100644 --- a/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php +++ b/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php @@ -16,7 +16,7 @@ use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; use Phalcon\Api\Domain\Services\Env\Adapters\DotEnv; use Phalcon\Api\Domain\Services\Env\EnvManager; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; final class DotEnvTest extends AbstractUnitTestCase { diff --git a/tests/Unit/Domain/Services/Env/EnvFactoryTest.php b/tests/Unit/Domain/Services/Env/EnvFactoryTest.php index 94cc4b8..57319a7 100644 --- a/tests/Unit/Domain/Services/Env/EnvFactoryTest.php +++ b/tests/Unit/Domain/Services/Env/EnvFactoryTest.php @@ -16,7 +16,7 @@ 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; +use Phalcon\Api\Tests\AbstractUnitTestCase; final class EnvFactoryTest extends AbstractUnitTestCase { diff --git a/tests/Unit/Domain/Services/Env/EnvManagerTest.php b/tests/Unit/Domain/Services/Env/EnvManagerTest.php index 0b891f2..7af51a7 100644 --- a/tests/Unit/Domain/Services/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Services/Env/EnvManagerTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Env; use Phalcon\Api\Domain\Services\Env\EnvManager; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; use ReflectionClass; diff --git a/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php b/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php index d381c16..40d6d06 100644 --- a/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php +++ b/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Http; use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; final class HttpCodesEnumTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Services/Http/ResponseTest.php b/tests/Unit/Domain/Services/Http/ResponseTest.php index 45b97d0..99681d1 100644 --- a/tests/Unit/Domain/Services/Http/ResponseTest.php +++ b/tests/Unit/Domain/Services/Http/ResponseTest.php @@ -15,7 +15,7 @@ use Phalcon\Api\Domain\Services\Container; use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use function ob_get_clean; use function ob_start; diff --git a/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php index 0b1154a..9db3fd2 100644 --- a/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php +++ b/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php @@ -16,7 +16,7 @@ use Phalcon\Api\Domain\Services\Container; use Phalcon\Api\Domain\Services\Env\EnvManager; use Phalcon\Api\Domain\Services\Providers\ErrorHandlerProvider; -use Phalcon\Api\Tests\Unit\AbstractUnitTestCase; +use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; use ReflectionClass; From 92448357becb78fd85b5d82021abc47c5aa1daa5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:11:50 -0500 Subject: [PATCH 070/301] [#.x] - phpcs --- src/Domain/DataSource/User/UserTransport.php | 2 +- src/Domain/DataSource/User/UserTypes.php | 1 - src/Domain/Hello/HelloService.php | 2 +- src/Domain/Services/Container.php | 37 +++++++------- .../Domain/Migrations/UsersMigration.php | 2 +- tests/Unit/Domain/User/UserServiceTest.php | 50 +++++++++---------- 6 files changed, 46 insertions(+), 48 deletions(-) diff --git a/src/Domain/DataSource/User/UserTransport.php b/src/Domain/DataSource/User/UserTransport.php index 21a9a93..6652a21 100644 --- a/src/Domain/DataSource/User/UserTransport.php +++ b/src/Domain/DataSource/User/UserTransport.php @@ -26,7 +26,7 @@ */ final class UserTransport { - /** @var TUserTransport */ + /** @var TUserTransport */ private array $store; /** diff --git a/src/Domain/DataSource/User/UserTypes.php b/src/Domain/DataSource/User/UserTypes.php index a8997df..711373a 100644 --- a/src/Domain/DataSource/User/UserTypes.php +++ b/src/Domain/DataSource/User/UserTypes.php @@ -13,7 +13,6 @@ namespace Phalcon\Api\Domain\DataSource\User; - /** * @phpstan-type TUserRecord array{}|array{ * usr_id: int, diff --git a/src/Domain/Hello/HelloService.php b/src/Domain/Hello/HelloService.php index 0102a77..caf9fe0 100644 --- a/src/Domain/Hello/HelloService.php +++ b/src/Domain/Hello/HelloService.php @@ -37,7 +37,7 @@ public function __invoke(array $input): Payload DomainStatus::SUCCESS, [ 'results' => [ - "Hello World!!! - " . date(Dates::DATE_TIME_FORMAT) + "Hello World!!! - " . date(Dates::DATE_TIME_FORMAT), ], ] ); diff --git a/src/Domain/Services/Container.php b/src/Domain/Services/Container.php index 3916969..d648f35 100644 --- a/src/Domain/Services/Container.php +++ b/src/Domain/Services/Container.php @@ -44,22 +44,12 @@ class Container extends Di public const EVENTS_MANAGER = 'eventsManager'; /** @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'; - /** @var string */ - public const TIME = 'time'; - /** * Services */ public const HELLO_SERVICE = HelloService::class; - public const USER_GET_SERVICE = 'service.user.get'; + /** @var string */ + public const LOGGER = 'logger'; /** * Middleware */ @@ -70,10 +60,19 @@ class Container extends Di * Repositories */ public const REPOSITORY_USER = 'repository.user'; + /** @var string */ + public const REQUEST = 'request'; /** * Responders */ public const RESPONDER_JSON = JsonResponder::class; + /** @var string */ + public const RESPONSE = 'response'; + /** @var string */ + public const ROUTER = 'router'; + /** @var string */ + public const TIME = 'time'; + public const USER_GET_SERVICE = 'service.user.get'; public function __construct() { @@ -101,18 +100,18 @@ private function getServiceConnection(): Service return new Service( function () { /** @var string $dbName */ - $dbName = EnvManager::get('DB_NAME','phalcon'); + $dbName = EnvManager::get('DB_NAME', 'phalcon'); /** @var string $host */ - $host = EnvManager::get('DB_HOST','rest-db'); + $host = EnvManager::get('DB_HOST', 'rest-db'); /** @var string $password */ - $password = EnvManager::get('DB_PASS','secret'); - $port = (int)EnvManager::get('DB_PORT',3306); + $password = EnvManager::get('DB_PASS', 'secret'); + $port = (int)EnvManager::get('DB_PORT', 3306); /** @var string $username */ - $username = EnvManager::get('DB_USER','root'); + $username = EnvManager::get('DB_USER', 'root'); /** @var string $encoding */ - $encoding = EnvManager::get('DB_CHARSET','utf8'); + $encoding = EnvManager::get('DB_CHARSET', 'utf8'); $queries = ['SET NAMES utf8mb4']; - $dsn = sprintf( + $dsn = sprintf( 'mysql:host=%s;port=%s;dbname=%s;charset=%s', $host, $port, diff --git a/tests/Fixtures/Domain/Migrations/UsersMigration.php b/tests/Fixtures/Domain/Migrations/UsersMigration.php index 41d0d29..6e7c6cb 100644 --- a/tests/Fixtures/Domain/Migrations/UsersMigration.php +++ b/tests/Fixtures/Domain/Migrations/UsersMigration.php @@ -25,7 +25,7 @@ public function insert( ?string $username = null, ?string $password = null, ) { - $id = $id ?: 'null'; + $id = $id ?: 'null'; $sql = <<table} ( usr_id, usr_status_flag, usr_username, usr_password diff --git a/tests/Unit/Domain/User/UserServiceTest.php b/tests/Unit/Domain/User/UserServiceTest.php index 87c7c02..85933d7 100644 --- a/tests/Unit/Domain/User/UserServiceTest.php +++ b/tests/Unit/Domain/User/UserServiceTest.php @@ -76,30 +76,6 @@ public function testServiceEmptyUserId(): void $this->assertStringContainsString($expected, $actual); } - public function testServiceWrongUserId(): void - { - $container = new Container(); - /** @var UserGetService $service */ - $service = $container->get(Container::USER_GET_SERVICE); - - $payload = $service->__invoke( - [ - 'userId' => 999999 - ] - ); - - $expected = DomainStatus::NOT_FOUND; - $actual = $payload->getStatus(); - $this->assertSame($expected, $actual); - - $actual = $payload->getResult(); - $this->assertArrayHasKey('results', $actual); - - $expected = 'Record(s) not found'; - $actual = $actual['results'][0]; - $this->assertStringContainsString($expected, $actual); - } - public function testServiceWithUserId(): void { $container = new Container(); @@ -115,7 +91,7 @@ public function testServiceWithUserId(): void $payload = $service->__invoke( [ - 'userId' => $userId + 'userId' => $userId, ] ); @@ -146,4 +122,28 @@ public function testServiceWithUserId(): void $actual = $user['password']; $this->assertSame($expected, $actual); } + + public function testServiceWrongUserId(): void + { + $container = new Container(); + /** @var UserGetService $service */ + $service = $container->get(Container::USER_GET_SERVICE); + + $payload = $service->__invoke( + [ + 'userId' => 999999, + ] + ); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('results', $actual); + + $expected = 'Record(s) not found'; + $actual = $actual['results'][0]; + $this->assertStringContainsString($expected, $actual); + } } From cff4a1a635ef626e8aa0debba7e6a1b75f02dd6d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 8 Oct 2025 11:18:49 -0500 Subject: [PATCH 071/301] [#.x] - fixes from copilot --- tests/AbstractUnitTestCase.php | 25 +++++++++---------- .../Domain/Migrations/UsersMigration.php | 23 +++++++++-------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 8bee0db..3abc5b3 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -104,21 +104,20 @@ protected function getFromDatabase( string $table, array $criteria ): array { - $sql = 'SELECT * FROM ' . $table . ' WHERE '; - $where = []; + $where = []; + $params = []; foreach ($criteria as $key => $value) { - $val = $value; - if (true === is_string($value)) { - $val = '"' . $value . '"'; - } - - $where[] = $key . ' = ' . $val; + $param = ':' . $key; + $where[] = $key . ' = ' . $param; + $params[$param] = $value; } - - $sql .= implode(' AND ', $where); - - $result = $this->connection?->query($sql); - $records = $result?->fetchAll(PDO::FETCH_ASSOC); + $sql = 'SELECT * FROM ' . $table; + if (!empty($where)) { + $sql .= ' WHERE ' . implode(' AND ', $where); + } + $stmt = $this->connection?->prepare($sql); + $stmt?->execute($params); + $records = $stmt?->fetchAll(PDO::FETCH_ASSOC); return $records; } diff --git a/tests/Fixtures/Domain/Migrations/UsersMigration.php b/tests/Fixtures/Domain/Migrations/UsersMigration.php index 6e7c6cb..e164b4e 100644 --- a/tests/Fixtures/Domain/Migrations/UsersMigration.php +++ b/tests/Fixtures/Domain/Migrations/UsersMigration.php @@ -25,16 +25,19 @@ public function insert( ?string $username = null, ?string $password = null, ) { - $id = $id ?: 'null'; - $sql = <<table} ( - usr_id, usr_status_flag, usr_username, usr_password -) VALUES ( - $id, $status, '$username', '$password' -) -SQL; - - $result = $this->connection->exec($sql); + $sql = "INSERT INTO {$this->table} ( + usr_id, usr_status_flag, usr_username, usr_password + ) VALUES ( + :id, :status, :username, :password + )"; + $stmt = $this->connection->prepare($sql); + $params = [ + ':id' => $id, + ':status' => $status, + ':username' => $username, + ':password' => $password, + ]; + $result = $stmt->execute($params); if (!$result) { Assert::fail( "Failed to insert id [#$id] into table [$this->table]" From 90f43a681cc00d2e5c5158abaeaf0d88eae7e789 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 17 Oct 2025 19:23:20 -0500 Subject: [PATCH 072/301] [#.x] - changing the users table --- ...7133633_add_more_fields_to_users_table.php | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 resources/db/migrations/20251017133633_add_more_fields_to_users_table.php diff --git a/resources/db/migrations/20251017133633_add_more_fields_to_users_table.php b/resources/db/migrations/20251017133633_add_more_fields_to_users_table.php new file mode 100644 index 0000000..bf8b7f0 --- /dev/null +++ b/resources/db/migrations/20251017133633_add_more_fields_to_users_table.php @@ -0,0 +1,221 @@ +table('co_users'); + + $table + ->removeIndex('usr_status_flag') + ->removeIndex('usr_username') + ->save() + ; + + $table->removeColumn('usr_username')->save(); + + $table + ->addColumn( + 'usr_email', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'after' => 'usr_status_flag' + ] + ) + ->addColumn( + 'usr_name_prefix', + 'string', + [ + 'limit' => 16, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'usr_name_first', + 'string', + [ + 'limit' => 64, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'usr_name_middle', + 'string', + [ + 'limit' => 64, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'usr_name_last', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'usr_name_suffix', + 'string', + [ + 'limit' => 16, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'usr_issuer', + 'string', + [ + 'limit' => 128, + 'null' => false, + ] + ) + ->addColumn( + 'usr_token_password', + 'string', + [ + 'limit' => 128, + 'null' => false, + ] + ) + ->addColumn( + 'usr_token_id', + 'string', + [ + 'limit' => 128, + 'null' => false, + ] + ) + ->addColumn( + 'usr_preferences', + 'text', + [ + 'null' => true + ] + ) + ->addColumn( + 'usr_created_date', + 'timestamp', + [ + 'timezone' => true, + 'default' => 'CURRENT_TIMESTAMP', + ] + ) + ->addColumn( + 'usr_created_usr_id', + 'biginteger', + [ + 'null' => false, + 'default' => 0, + ] + ) + ->addColumn( + 'usr_updated_date', + 'timestamp', + [ + 'timezone' => true, + 'default' => null, + ] + ) + ->addColumn( + 'usr_updated_usr_id', + 'biginteger', + [ + 'null' => false, + 'default' => 0, + ] + ) + ->addIndex( + [ + 'usr_email', + 'usr_status_flag', + ], + [ + 'name' => 'i_email_x_status' + ] + ) + ->addIndex( + [ + 'usr_id', + 'usr_status_flag', + ], + [ + 'name' => 'i_id_x_status' + ] + ) + ->addIndex( + [ + 'usr_token_id' + ], + [ + 'unique' => true, + 'name' => 'i_token_id' + ] + ) + ->addIndex( + [ + 'usr_email', + ], + [ + 'unique' => true, + 'name' => 'i_email' + ] + ) + ->save() + ; + } + + public function down(): void + { + $table = $this->table('co_users'); + $table + ->removeIndexByName('i_email_x_status') + ->removeIndexByName('i_id_x_status') + ->removeIndexByName('i_token_id') + ->removeIndexByName('i_email') + ->save(); + + $table + ->removeColumn('usr_email') + ->removeColumn('usr_name_prefix') + ->removeColumn('usr_name_first') + ->removeColumn('usr_name_middle') + ->removeColumn('usr_name_last') + ->removeColumn('usr_name_suffix') + ->removeColumn('usr_issuer') + ->removeColumn('usr_token_password') + ->removeColumn('usr_token_id') + ->removeColumn('usr_preferences') + ->removeColumn('usr_created_date') + ->removeColumn('usr_created_usr_id') + ->removeColumn('usr_updated_date') + ->removeColumn('usr_updated_usr_id') + ->save(); + + $table + ->addColumn( + 'usr_username', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + 'after' => 'usr_status_flag', + ] + ) + ->addIndex('usr_status_flag') + ->addIndex('usr_username') + ->save(); + } +} From 390dda9fa1f78f6017469692a50aa94d9220a17a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 17 Oct 2025 19:23:41 -0500 Subject: [PATCH 073/301] [#.x] - added flags enum, interface and tests --- .../Components/Enums/Common/FlagsEnum.php | 33 +++++++++++ .../Components/Enums/EnumsInterface.php | 26 +++++++++ .../Components/Enums/Common/FlagsEnumTest.php | 57 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/Domain/Components/Enums/Common/FlagsEnum.php create mode 100644 src/Domain/Components/Enums/EnumsInterface.php create mode 100644 tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php diff --git a/src/Domain/Components/Enums/Common/FlagsEnum.php b/src/Domain/Components/Enums/Common/FlagsEnum.php new file mode 100644 index 0000000..0a0be5b --- /dev/null +++ b/src/Domain/Components/Enums/Common/FlagsEnum.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\Components\Enums\Common; + +use Phalcon\Api\Domain\Components\Enums\EnumsInterface; + +enum FlagsEnum: int implements EnumsInterface +{ + case Active = 1; + case Disabled = 2; + case Inactive = 3; + + /** + * Return the text associated with the option + * + * @return string + */ + public function text(): string + { + return $this->name; + } +} diff --git a/src/Domain/Components/Enums/EnumsInterface.php b/src/Domain/Components/Enums/EnumsInterface.php new file mode 100644 index 0000000..b763f88 --- /dev/null +++ b/src/Domain/Components/Enums/EnumsInterface.php @@ -0,0 +1,26 @@ + + * + * 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\Components\Enums; + +use BackedEnum; + +interface EnumsInterface extends BackedEnum +{ + /** + * Return the text associated with the option + * + * @return string + */ + public function text(): string; +} diff --git a/tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php b/tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php new file mode 100644 index 0000000..6c95c0f --- /dev/null +++ b/tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php @@ -0,0 +1,57 @@ + + * + * 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\Components\Enums\Common; + +use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class FlagsEnumTest extends AbstractUnitTestCase +{ + public function testCheckCount(): void + { + $expected = 3; + $actual = FlagsEnum::cases(); + $this->assertCount($expected, $actual); + } + + public function testCheckText(): void + { + $expected = 'Active'; + $actual = FlagsEnum::Active->text(); + $this->assertSame($expected, $actual); + + $expected = 'Disabled'; + $actual = FlagsEnum::Disabled->text(); + $this->assertSame($expected, $actual); + + $expected = 'Inactive'; + $actual = FlagsEnum::Inactive->text(); + $this->assertSame($expected, $actual); + } + + public function testCheckValues(): void + { + $expected = 1; + $actual = FlagsEnum::Active->value; + $this->assertSame($expected, $actual); + + $expected = 2; + $actual = FlagsEnum::Disabled->value; + $this->assertSame($expected, $actual); + + $expected = 3; + $actual = FlagsEnum::Inactive->value; + $this->assertSame($expected, $actual); + } +} From a953db4a0349fd8122e2b0bca58d3859eefe8eb8 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 17 Oct 2025 19:24:26 -0500 Subject: [PATCH 074/301] [#.x] - moved actionhandler and added tests --- src/{Domain/ADR => }/Action/ActionHandler.php | 16 ++-- .../ADR => }/Action/ActionInterface.php | 2 +- tests/Unit/Action/ActionHandlerTest.php | 73 +++++++++++++++++++ 3 files changed, 82 insertions(+), 9 deletions(-) rename src/{Domain/ADR => }/Action/ActionHandler.php (68%) rename src/{Domain/ADR => }/Action/ActionInterface.php (88%) create mode 100644 tests/Unit/Action/ActionHandlerTest.php diff --git a/src/Domain/ADR/Action/ActionHandler.php b/src/Action/ActionHandler.php similarity index 68% rename from src/Domain/ADR/Action/ActionHandler.php rename to src/Action/ActionHandler.php index 29d604a..e8f6491 100644 --- a/src/Domain/ADR/Action/ActionHandler.php +++ b/src/Action/ActionHandler.php @@ -11,19 +11,19 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\ADR\Action; +namespace Phalcon\Api\Action; -use Phalcon\Api\Domain\ADR\Domain\DomainInterface; -use Phalcon\Api\Domain\ADR\Domain\Input; -use Phalcon\Api\Domain\ADR\Responder\ResponderInterface; -use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Http\Request; +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\ADR\Input; +use Phalcon\Api\Responder\ResponderInterface; +use Phalcon\Http\RequestInterface; +use Phalcon\Http\ResponseInterface; final readonly class ActionHandler implements ActionInterface { public function __construct( - private Request $request, - private Response $response, + private RequestInterface $request, + private ResponseInterface $response, private DomainInterface $service, private ResponderInterface $responder ) { diff --git a/src/Domain/ADR/Action/ActionInterface.php b/src/Action/ActionInterface.php similarity index 88% rename from src/Domain/ADR/Action/ActionInterface.php rename to src/Action/ActionInterface.php index 1d6586b..2b508bc 100644 --- a/src/Domain/ADR/Action/ActionInterface.php +++ b/src/Action/ActionInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\ADR\Action; +namespace Phalcon\Api\Action; interface ActionInterface { diff --git a/tests/Unit/Action/ActionHandlerTest.php b/tests/Unit/Action/ActionHandlerTest.php new file mode 100644 index 0000000..14b257d --- /dev/null +++ b/tests/Unit/Action/ActionHandlerTest.php @@ -0,0 +1,73 @@ + + * + * 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\Action; + +use Phalcon\Api\Action\ActionHandler; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Responder\ResponderInterface; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Services\ServiceFixture; +use Phalcon\Http\RequestInterface; +use Phalcon\Http\ResponseInterface; +use PHPUnit\Framework\Attributes\BackupGlobals; + +use function ob_get_clean; +use function ob_start; +use function uniqid; + +#[BackupGlobals(true)] +final class ActionHandlerTest extends AbstractUnitTestCase +{ + public function testInvoke(): void + { + /** @var RequestInterface $request */ + $request = $this->container->getShared(Container::REQUEST); + /** @var ResponseInterface $response */ + $response = $this->container->getShared(Container::RESPONSE); + /** @var ResponderInterface $responder */ + $responder = $this->container->getShared(Container::RESPONDER_JSON); + + $getData = [ + 'key' => uniqid('key-'), + 'data' => [ + uniqid('data-'), + ] + ]; + + $_GET = $getData; + + $service = new ServiceFixture(); + $action = new ActionHandler( + $request, + $response, + $service, + $responder + ); + + ob_start(); + $action->__invoke(); + $content = ob_get_clean(); + + $content = json_decode($content, true); + + $data = $content['data']; + $errors = $content['errors']; + + $this->assertEmpty($errors); + + $expected = $getData; + $actual = $data; + $this->assertSame($expected, $actual); + } +} From 7308d26f4ecd4ae8a7f5c3db7e8946176bd213c4 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:19:38 -0500 Subject: [PATCH 075/301] [#.x] - moved responder and added tests --- src/Domain/ADR/Responder/JsonResponder.php | 47 ------ src/Responder/JsonResponder.php | 106 ++++++++++++ tests/Unit/Responder/JsonResponderTest.php | 184 +++++++++++++++++++++ 3 files changed, 290 insertions(+), 47 deletions(-) delete mode 100644 src/Domain/ADR/Responder/JsonResponder.php create mode 100644 src/Responder/JsonResponder.php create mode 100644 tests/Unit/Responder/JsonResponderTest.php diff --git a/src/Domain/ADR/Responder/JsonResponder.php b/src/Domain/ADR/Responder/JsonResponder.php deleted file mode 100644 index 3afbfeb..0000000 --- a/src/Domain/ADR/Responder/JsonResponder.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * 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\ADR\Responder; - -use Exception as BaseException; -use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Domain\Payload; - -/** - * @phpstan-import-type TResult from ResponderTypes - */ -final class JsonResponder implements ResponderInterface -{ - /** - * @param Response $response - * @param Payload $payload - * - * @return Response - * @throws BaseException - */ - public function __invoke( - Response $response, - Payload $payload - ): Response { - $result = $payload->getResult(); - /** @var TResult $content */ - $content = $result['results']; - - $response - ->withPayloadData($content) - ->render() - ; - - return $response; - } -} diff --git a/src/Responder/JsonResponder.php b/src/Responder/JsonResponder.php new file mode 100644 index 0000000..7d7a7d2 --- /dev/null +++ b/src/Responder/JsonResponder.php @@ -0,0 +1,106 @@ + + * + * 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 Exception as BaseException; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Domain\Payload; +use Phalcon\Http\ResponseInterface; + +use function sha1; + +/** + * @phpstan-import-type TData from ResponderTypes + * @phpstan-import-type TErrors from ResponderTypes + * @phpstan-import-type TResult from ResponderTypes + * @phpstan-import-type TResponsePayload from ResponderTypes + */ +final class JsonResponder implements ResponderInterface +{ + /** + * @param ResponseInterface $response + * @param Payload $payload + * + * @return ResponseInterface + * @throws BaseException + */ + public function __invoke( + ResponseInterface $response, + Payload $payload + ): ResponseInterface { + $result = $payload->getResult(); + + /** + * Check the code, message, data and errors + */ + /** @var int $code */ + $code = $result['code'] ?? HttpCodesEnum::OK->value; + /** @var string $message */ + $message = $result['message'] ?? HttpCodesEnum::OK->text(); + /** @var TData $data */ + $data = $result['data'] ?? []; + /** @var TErrors $errors */ + $errors = $result['errors'] ?? []; + $statusMessage = true === empty($errors) ? 'success' : 'error'; + + $content = [ + 'data' => $data, + 'errors' => $errors, + 'meta' => [ + 'code' => $code, + 'hash' => '', + 'message' => $statusMessage, + 'timestamp' => '', + ], + ]; + + [$content, $eTag] = $this->calculateMeta($content); + + $response + ->setHeader('ETag', $eTag) + ->setStatusCode($code, $message) + ->setJsonContent($content) + ->send() + ; + + return $response; + } + + /** + * @param TResponsePayload $content + * + * @return array{0: TResponsePayload, 1: string} + */ + private function calculateMeta(array $content): array + { + $payload = [ + 'data' => $content['data'], + 'errors' => $content['errors'], + ]; + $encoded = json_encode($payload); + $encoded = (false === $encoded) ? '' : $encoded; + $timestamp = Dates::toUTC(); + $hash = sha1($timestamp . $encoded); + $eTag = sha1($encoded); + + $content['meta']['timestamp'] = $timestamp; + $content['meta']['hash'] = $hash; + + + return [$content, $eTag]; + } + + +} diff --git a/tests/Unit/Responder/JsonResponderTest.php b/tests/Unit/Responder/JsonResponderTest.php new file mode 100644 index 0000000..fd052bf --- /dev/null +++ b/tests/Unit/Responder/JsonResponderTest.php @@ -0,0 +1,184 @@ + + * + * 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\Responder; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Responder\ResponderInterface; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Domain\Payload; +use Phalcon\Http\ResponseInterface; +use PHPUnit\Framework\Attributes\BackupGlobals; + +use function http_response_code; +use function ob_get_clean; +use function ob_start; +use function uniqid; + +#[BackupGlobals(true)] +final class JsonResponderTest extends AbstractUnitTestCase +{ + public function testFailure(): void + { + /** @var ResponseInterface $response */ + $response = $this->container->getShared(Container::RESPONSE); + /** @var ResponderInterface $responder */ + $responder = $this->container->get(Container::RESPONDER_JSON); + + $errorContent = uniqid('error-'); + $payload = new Payload( + DomainStatus::UNAUTHORIZED, + [ + 'code' => HttpCodesEnum::Unauthorized->value, + 'message' => HttpCodesEnum::Unauthorized->text(), + 'data' => [], + 'errors' => [ + [1234 => $errorContent], + ], + ] + ); + + ob_start(); + $outputResponse = $responder->__invoke($response, $payload); + $output = ob_get_clean(); + $contents = json_decode($output, true); + + $expected = HttpCodesEnum::Unauthorized->value; + $actual = http_response_code(); + $this->assertSame($expected, $actual); + + $expected = ResponseInterface::class; + $this->assertInstanceOf($expected, $outputResponse); + + $this->assertArrayHasKey('data', $contents); + $this->assertArrayHasKey('errors', $contents); + $this->assertArrayHasKey('meta', $contents); + + $data = $contents['data']; + $errors = $contents['errors']; + $meta = $contents['meta']; + + unset($meta['hash'], $meta['timestamp']); + + $this->assertEmpty($data); + + $expected = [ + [1234 => $errorContent], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + + + $expected = [ + 'code' => HttpCodesEnum::Unauthorized->value, + 'message' => 'error', + ]; + $this->assertSame($expected, $meta); + + $headers = $outputResponse->getHeaders()->toArray(); + + $expected = 'ETag'; + $this->assertArrayHasKey($expected, $headers); + $expected = 'HTTP/1.1 401 Unauthorized'; + $this->assertArrayHasKey($expected, $headers); + $expected = 'Status'; + $this->assertArrayHasKey($expected, $headers); + $expected = 'Content-Type'; + $this->assertArrayHasKey($expected, $headers); + + $expected = '401 Unauthorized'; + $actual = $headers['Status']; + $this->assertSame($expected, $actual); + + $expected = 'application/json'; + $actual = $headers['Content-Type']; + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var ResponseInterface $response */ + $response = $this->container->getShared(Container::RESPONSE); + /** @var ResponderInterface $responder */ + $responder = $this->container->getShared(Container::RESPONDER_JSON); + + $dataContent = uniqid('data-'); + $payload = new Payload( + DomainStatus::SUCCESS, + [ + 'code' => HttpCodesEnum::OK->value, + 'message' => HttpCodesEnum::OK->text(), + 'data' => [ + $dataContent, + ], + 'errors' => [], + ] + ); + + ob_start(); + $outputResponse = $responder->__invoke($response, $payload); + $content = ob_get_clean(); + + $content = json_decode($content, true); + + $expected = HttpCodesEnum::OK->value; + $actual = http_response_code(); + $this->assertSame($expected, $actual); + + $expected = ResponseInterface::class; + $this->assertInstanceOf($expected, $outputResponse); + + $data = $content['data']; + $errors = $content['errors']; + $meta = $content['meta']; + + unset($meta['hash'], $meta['timestamp']); + + $expected = [ + $dataContent, + ]; + $actual = $data; + $this->assertSame($expected, $actual); + + $this->assertEmpty($errors); + + $expected = [ + 'code' => HttpCodesEnum::OK->value, + 'message' => 'success', + ]; + $actual = $meta; + $this->assertSame($expected, $actual); + + $headers = $outputResponse->getHeaders()->toArray(); + + $expected = 'ETag'; + $this->assertArrayHasKey($expected, $headers); + $expected = 'HTTP/1.1 200 OK'; + $this->assertArrayHasKey($expected, $headers); + $expected = 'Status'; + $this->assertArrayHasKey($expected, $headers); + $expected = 'Content-Type'; + $this->assertArrayHasKey($expected, $headers); + + $expected = '200 OK'; + $actual = $headers['Status']; + $this->assertSame($expected, $actual); + + $expected = 'application/json'; + $actual = $headers['Content-Type']; + $this->assertSame($expected, $actual); + } + +} From 4b57c2d2ef34911a91491a8020ad9fea0b277e01 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:20:13 -0500 Subject: [PATCH 076/301] [#.x] - added security class --- src/Domain/Components/Encryption/Security.php | 50 +++++++++++++++++++ .../Components/Encryption/SecurityTest.php | 35 +++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/Domain/Components/Encryption/Security.php create mode 100644 tests/Unit/Domain/Components/Encryption/SecurityTest.php diff --git a/src/Domain/Components/Encryption/Security.php b/src/Domain/Components/Encryption/Security.php new file mode 100644 index 0000000..1294156 --- /dev/null +++ b/src/Domain/Components/Encryption/Security.php @@ -0,0 +1,50 @@ + + * + * 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\Components\Encryption; + +use const PASSWORD_ARGON2I; + +/** + * NOTE: if the algorithm changes (for `password_hash`) it will invalidate + * all passwords in the database. + */ +final class Security +{ + private const ALGO = PASSWORD_ARGON2I; + + /** + * Creates a password hash using password_hash + * + * @param string $password + * + * @return string + */ + public function hash(string $password): string + { + return password_hash($password, self::ALGO); + } + + /** + * Checks a plain text password and its hash version to see if they match + * + * @param string $password + * @param string $hash + * + * @return bool + */ + public function verify(string $password, string $hash): bool + { + return password_verify($password, $hash); + } +} diff --git a/tests/Unit/Domain/Components/Encryption/SecurityTest.php b/tests/Unit/Domain/Components/Encryption/SecurityTest.php new file mode 100644 index 0000000..d0350c5 --- /dev/null +++ b/tests/Unit/Domain/Components/Encryption/SecurityTest.php @@ -0,0 +1,35 @@ + + * + * 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\Components\Encryption; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; + +final class SecurityTest extends AbstractUnitTestCase +{ + public function testSecurity(): void + { + /** @var Security $security */ + $security = $this->container->get(Container::SECURITY); + $password = $this->getStrongPassword(); + + $hashed = $security->hash($password); + + $actual = $security->verify($password, $hashed); + $this->assertTrue($actual); + } +} From ca66b4d477a3b2063e474cfcc7b016c16bd9acf2 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:20:43 -0500 Subject: [PATCH 077/301] [#.x] - added transport repository class --- .../DataSource/TransportRepository.php | 33 ++++ .../DataSource/TransportRepositoryTest.php | 142 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 src/Domain/Components/DataSource/TransportRepository.php create mode 100644 tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php diff --git a/src/Domain/Components/DataSource/TransportRepository.php b/src/Domain/Components/DataSource/TransportRepository.php new file mode 100644 index 0000000..545b098 --- /dev/null +++ b/src/Domain/Components/DataSource/TransportRepository.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\Components\DataSource; + +use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; + +/** + * @phpstan-import-type TUserRecord from UserTypes + */ +final readonly class TransportRepository +{ + /** + * @param TUserRecord $dbUser + * + * @return UserTransport + */ + public function newUser(array $dbUser): UserTransport + { + return new UserTransport($dbUser); + } +} diff --git a/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php b/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php new file mode 100644 index 0000000..dbf9083 --- /dev/null +++ b/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php @@ -0,0 +1,142 @@ + + * + * 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\Components\DataSource; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; + +final class TransportRepositoryTest extends AbstractUnitTestCase +{ + public function testNewUser(): void + { + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $user = $transport->newUser([]); + $actual = $user->isEmpty(); + $this->assertTrue($actual); + + $dbUser = $this->getNewUserData(); + $user = $transport->newUser($dbUser); + + $fullName = trim( + $dbUser['usr_name_last'] + . ', ' + . $dbUser['usr_name_first'] + . ' ' + . $dbUser['usr_name_middle'] + ); + + $expected = [ + $dbUser['usr_id'] => [ + 'id' => $dbUser['usr_id'], + 'status' => $dbUser['usr_status_flag'], + 'email' => $dbUser['usr_email'], + 'password' => $dbUser['usr_password'], + 'namePrefix' => $dbUser['usr_name_prefix'], + 'nameFirst' => $dbUser['usr_name_first'], + 'nameMiddle' => $dbUser['usr_name_middle'], + 'nameLast' => $dbUser['usr_name_last'], + 'nameSuffix' => $dbUser['usr_name_suffix'], + 'issuer' => $dbUser['usr_issuer'], + 'tokenPassword' => $dbUser['usr_token_password'], + 'tokenId' => $dbUser['usr_token_id'], + 'preferences' => $dbUser['usr_preferences'], + 'createdDate' => $dbUser['usr_created_date'], + 'createdUserId' => $dbUser['usr_created_usr_id'], + 'updatedDate' => $dbUser['usr_updated_date'], + 'updatedUserId' => $dbUser['usr_updated_usr_id'], + 'fullName' => $fullName, + ], + ]; + $actual = $user->toArray(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_id']; + $actual = $user->getId(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_status_flag']; + $actual = $user->getStatus(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_email']; + $actual = $user->getEmail(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_password']; + $actual = $user->getPassword(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_prefix']; + $actual = $user->getNamePrefix(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_first']; + $actual = $user->getNameFirst(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_middle']; + $actual = $user->getNameMiddle(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_last']; + $actual = $user->getNameLast(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_suffix']; + $actual = $user->getNameSuffix(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_issuer']; + $actual = $user->getIssuer(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_token_password']; + $actual = $user->getTokenPassword(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_token_id']; + $actual = $user->getTokenId(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_preferences']; + $actual = $user->getPreferences(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_date']; + $actual = $user->getCreatedDate(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_usr_id']; + $actual = $user->getCreatedUserId(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_date']; + $actual = $user->getUpdatedDate(); + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_usr_id']; + $actual = $user->getUpdatedUserId(); + $this->assertSame($expected, $actual); + + $expected = $fullName; + $actual = $user->getFullName(); + $this->assertSame($expected, $actual); + + } +} From 8ec768058b5854cce6e2a0e1e659bf37fb67c58d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:20:57 -0500 Subject: [PATCH 078/301] [#.x] - added query repository class --- .../Components/DataSource/QueryRepository.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Domain/Components/DataSource/QueryRepository.php diff --git a/src/Domain/Components/DataSource/QueryRepository.php b/src/Domain/Components/DataSource/QueryRepository.php new file mode 100644 index 0000000..5e5f58b --- /dev/null +++ b/src/Domain/Components/DataSource/QueryRepository.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\Domain\Components\DataSource; + +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\DataMapper\Pdo\Connection; + +class QueryRepository +{ + private ?UserRepository $user = null; + + /** + * @param Connection $connection + */ + public function __construct( + private readonly Connection $connection, + ) { + } + + /** + * @return UserRepository + */ + public function user(): UserRepository + { + if (null === $this->user) { + $this->user = new UserRepository($this->connection); + } + + return $this->user; + } +} From 65457c80361aa7b727846ce7aa490f3f952d1483 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:21:11 -0500 Subject: [PATCH 079/301] [#.x] - service fixture for tests --- .../Domain/Services/ServiceFixture.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/Fixtures/Domain/Services/ServiceFixture.php diff --git a/tests/Fixtures/Domain/Services/ServiceFixture.php b/tests/Fixtures/Domain/Services/ServiceFixture.php new file mode 100644 index 0000000..8e04a05 --- /dev/null +++ b/tests/Fixtures/Domain/Services/ServiceFixture.php @@ -0,0 +1,31 @@ + + * + * 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\Fixtures\Domain\Services; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Domain\Payload; + +final readonly class ServiceFixture implements DomainInterface +{ + public function __invoke(array $input): Payload + { + return new Payload( + DomainStatus::SUCCESS, + [ + 'data' => $input, + ] + ); + } +} From c7d222d9f7453eb9252d1863d2aee7cde7e0b776 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:21:26 -0500 Subject: [PATCH 080/301] [#.x] - added fakerphp to composer --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 6e97c9d..37f0bdf 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "vlucas/phpdotenv": "^5.6" }, "require-dev": { + "fakerphp/faker": "^1.24", "pds/composer-script-names": "^1.0", "pds/skeleton": "^1.0", "phpstan/phpstan": "^2.1", From 231b77cf595921ff9540e6bf0d22b09ff3722f24 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:22:00 -0500 Subject: [PATCH 081/301] [#.x] - removed custom response and finish middleware --- .../Middleware/ResponseSenderMiddleware.php | 40 ----- src/Domain/Services/Http/Response.php | 139 ------------------ src/Domain/Services/Http/ResponseTypes.php | 35 ----- .../ResponseSenderMiddlewareTest.php | 58 -------- .../Domain/Services/Http/ResponseTest.php | 112 -------------- 5 files changed, 384 deletions(-) delete mode 100644 src/Domain/Middleware/ResponseSenderMiddleware.php delete mode 100644 src/Domain/Services/Http/Response.php delete mode 100644 src/Domain/Services/Http/ResponseTypes.php delete mode 100644 tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php delete mode 100644 tests/Unit/Domain/Services/Http/ResponseTest.php diff --git a/src/Domain/Middleware/ResponseSenderMiddleware.php b/src/Domain/Middleware/ResponseSenderMiddleware.php deleted file mode 100644 index 22e7bdd..0000000 --- a/src/Domain/Middleware/ResponseSenderMiddleware.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * 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\Services\Container; -use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Events\Exception as EventsException; -use Phalcon\Http\Response\Exception; -use Phalcon\Mvc\Micro; - -final class ResponseSenderMiddleware extends AbstractMiddleware -{ - /** - * @param Micro $application - * - * @return true - * @throws EventsException - * @throws Exception - */ - public function call(Micro $application): bool - { - /** @var Response $response */ - $response = $application->getSharedService(Container::RESPONSE); - - $response->send(); - - return true; - } -} diff --git a/src/Domain/Services/Http/Response.php b/src/Domain/Services/Http/Response.php deleted file mode 100644 index 45495cb..0000000 --- a/src/Domain/Services/Http/Response.php +++ /dev/null @@ -1,139 +0,0 @@ - - * - * 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\Http; - -use Exception; -use Phalcon\Api\Domain\Constants\Dates; -use Phalcon\Http\Response as PhalconResponse; -use Phalcon\Http\Response\Exception as ResponseException; - -use function json_encode; -use function sha1; - -/** - * @phpstan-import-type TData from ResponseTypes - * @phpstan-import-type TErrors from ResponseTypes - * @phpstan-import-type TResponsePayload from ResponseTypes - */ -class Response extends PhalconResponse -{ - /** @var TResponsePayload */ - private array $payload; - - /** - * @return $this - * @throws Exception - */ - public function render(): self - { - $this - ->calculateMeta() - ->setJsonContent($this->payload) - ; - - return $this; - } - - /** - * @param int $code - * @param string $message - * - * @return $this - * @throws ResponseException - */ - public function withCode(int $code, string $message = ''): self - { - $this->setStatusCode($code, $message); - - return $this; - } - - /** - * @param TData $data - * - * @return $this - * @throws Exception - */ - public function withPayloadData(array $data): self - { - if (empty($this->payload)) { - $this->initPayload(); - } - - $this->payload['data'] = $data; - - return $this; - } - - /** - * @param TErrors $errors - * - * @return $this - */ - public function withPayloadErrors(array $errors): self - { - if (empty($this->payload)) { - $this->initPayload(); - } - - $this->payload['errors'] = $errors; - $this->payload['meta']['code'] = 3000; - $this->payload['meta']['message'] = 'error'; - - return $this; - } - - /** - * @return self - * @throws Exception - */ - private function calculateMeta(): self - { - $payload = [ - 'data' => $this->payload['data'], - 'errors' => $this->payload['errors'], - ]; - $encoded = json_encode($payload); - $encoded = (false === $encoded) ? '' : $encoded; - $timestamp = Dates::toUTC(); - $hash = sha1($timestamp . $encoded); - $eTag = sha1($encoded); - - $this->payload['meta']['timestamp'] = $timestamp; - $this->payload['meta']['hash'] = $hash; - - $this - ->setHeader('ETag', $eTag) - ; - - return $this; - } - - /** - * @return void - */ - private function initPayload(): void - { - $this->payload = [ - 'data' => [], - 'errors' => [], - 'meta' => [ - 'code' => HttpCodesEnum::OK->value, - 'hash' => '', - 'message' => 'success', - 'timestamp' => '', - ], - ]; - } -} diff --git a/src/Domain/Services/Http/ResponseTypes.php b/src/Domain/Services/Http/ResponseTypes.php deleted file mode 100644 index 377200f..0000000 --- a/src/Domain/Services/Http/ResponseTypes.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * 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\Http; - -/** - * This class is used to define phpstan types so that they are all in one - * place. - * - * @phpstan-type TData array|array{} - * @phpstan-type TErrors array>|array{} - * @phpstan-type TResponsePayload array{ - * data: TData, - * errors: TErrors, - * meta: array{ - * code: int, - * hash: string, - * message: string, - * timestamp: string - * } - * } - */ -final class ResponseTypes -{ -} diff --git a/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php b/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php deleted file mode 100644 index 5a1984a..0000000 --- a/tests/Unit/Domain/Middleware/ResponseSenderMiddlewareTest.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * 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\Middleware; - -use Phalcon\Api\Domain\Middleware\ResponseSenderMiddleware; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Mvc\Micro; -use PHPUnit\Framework\Attributes\BackupGlobals; - -use function ob_get_clean; -use function ob_start; -use function uniqid; - -#[BackupGlobals(true)] -final class ResponseSenderMiddlewareTest extends AbstractUnitTestCase -{ - public function testCall(): void - { - $container = new Container(); - $application = new Micro($container); - $middleware = new ResponseSenderMiddleware(); - /** @var Response $response */ - $response = $container->getShared(Container::RESPONSE); - - $content = uniqid('content-'); - $response->setContent($content); - - $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); - $_SERVER = [ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_TIME_FLOAT' => $time, - 'REQUEST_URI' => '/health', - ]; - - ob_start(); - $actual = $middleware->call($application); - $contents = ob_get_clean(); - - $this->assertTrue($actual); - - $expected = $content; - $actual = $contents; - $this->assertEquals($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Services/Http/ResponseTest.php b/tests/Unit/Domain/Services/Http/ResponseTest.php deleted file mode 100644 index 99681d1..0000000 --- a/tests/Unit/Domain/Services/Http/ResponseTest.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * 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\Http; - -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Api\Tests\AbstractUnitTestCase; - -use function ob_get_clean; -use function ob_start; -use function uniqid; - -final class ResponseTest extends AbstractUnitTestCase -{ - public function testWithPayloadMeta(): void - { - $container = new Container(); - /** @var Response $response */ - $response = $container->getShared(Container::RESPONSE); - - $message = uniqid('message-'); - $key = uniqid('key-'); - $value = uniqid('value-'); - - $response - ->withCode(404, $message) - ->withPayloadData([$key => $value]) - ; - - ob_start(); - $response->render()->send(); - $data = ob_get_clean(); - - $expected = 404; - $actual = $response->getStatusCode(); - $this->assertSame($expected, $actual); - - $expected = $message; - $actual = $response->getReasonPhrase(); - $this->assertSame($expected, $actual); - - /** - * Remove the timestamp and hash because they are always changing - */ - $data = json_decode($data, true); - unset($data['meta']['timestamp']); - unset($data['meta']['hash']); - - $expected = [ - 'data' => [ - $key => $value, - ], - 'errors' => [], - 'meta' => [ - 'code' => 200, - 'message' => 'success', - ], - ]; - $actual = $data; - $this->assertSame($expected, $actual); - } - - public function testWithPayloadMetaErrors(): void - { - $container = new Container(); - /** @var Response $response */ - $response = $container->getShared(Container::RESPONSE); - - $message = uniqid('message-'); - - $response - ->withPayloadErrors([[$message]]) - ; - - ob_start(); - $response->render()->send(); - $data = ob_get_clean(); - - /** - * Remove the timestamp and hash because they are always changing - */ - $data = json_decode($data, true); - unset($data['meta']['timestamp']); - unset($data['meta']['hash']); - - $expected = [ - 'data' => [], - 'errors' => [ - [ - $message, - ], - ], - 'meta' => [ - 'code' => 3000, - 'message' => 'error', - ], - ]; - $actual = $data; - $this->assertSame($expected, $actual); - } -} From a9e2fcb6876d8525b76d3c54c5ac3f9c57713fa9 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:22:35 -0500 Subject: [PATCH 082/301] [#.x] - moved user repositories and added more methods --- .../DataSource/User/UserRepository.php | 87 ++++++++++++ src/Domain/DataSource/User/UserRepository.php | 50 ------- .../DataSource/User/UserRepositoryTest.php | 132 ++++++++++++++++++ .../DataSource/User/UserRepositoryTest.php | 56 -------- 4 files changed, 219 insertions(+), 106 deletions(-) create mode 100644 src/Domain/Components/DataSource/User/UserRepository.php delete mode 100644 src/Domain/DataSource/User/UserRepository.php create mode 100644 tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php delete mode 100644 tests/Unit/Domain/DataSource/User/UserRepositoryTest.php diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php new file mode 100644 index 0000000..c288ede --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -0,0 +1,87 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\DataMapper\Query\Select; + +/** + * @phpstan-import-type TUserRecord from UserTypes + */ +final class UserRepository +{ + public function __construct( + private readonly Connection $connection, + ) { + } + + /** + * @param string $email + * + * @return TUserRecord + */ + public function findByEmail(string $email): array + { + $result = []; + if (true !== empty($email)) { + return $this->findOneBy( + [ + 'usr_email' => $email, + 'usr_status_flag' => FlagsEnum::Active->value, + ] + ); + } + + return $result; + } + + /** + * @param int $userId + * + * @return TUserRecord + */ + public function findById(int $userId): array + { + $result = []; + if ($userId > 0) { + return $this->findOneBy( + [ + 'usr_id' => $userId, + ] + ); + } + + return $result; + } + + /** + * @param array $criteria + * + * @return TUserRecord + */ + public function findOneBy(array $criteria): array + { + $select = Select::new($this->connection); + + /** @var TUserRecord $result */ + $result = $select + ->from('co_users') + ->whereEquals($criteria) + ->fetchOne() + ; + + return $result; + } +} diff --git a/src/Domain/DataSource/User/UserRepository.php b/src/Domain/DataSource/User/UserRepository.php deleted file mode 100644 index dd4ef84..0000000 --- a/src/Domain/DataSource/User/UserRepository.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * 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\DataSource\User; - -use Phalcon\DataMapper\Pdo\Connection; -use Phalcon\DataMapper\Query\Select; - -/** - * @phpstan-import-type TUserRecord from UserTypes - */ -final class UserRepository -{ - public function __construct( - private readonly Connection $connection, - ) { - } - - /** - * @param int|string $userId - * - * @return UserTransport - */ - public function findById(int | string $userId): UserTransport - { - $result = []; - if (true !== empty($userId)) { - $select = Select::new($this->connection); - - /** @var TUserRecord $result */ - $result = $select - ->from('co_users') - ->where('usr_id = ', $userId) - ->fetchOne() - ; - } - - return new UserTransport($result); - } -} diff --git a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php b/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php new file mode 100644 index 0000000..6e0f09f --- /dev/null +++ b/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php @@ -0,0 +1,132 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\DataMapper\Pdo\Connection; + +final class UserRepositoryTest extends AbstractUnitTestCase +{ + public function testFindByEmail(): void + { + /** @var QueryRepository $repository */ + $repository = $this->container->get(Container::REPOSITORY); + + $migration = new UsersMigration($this->getConnection()); + + $repositoryUser = $repository->user()->findByEmail(''); + $this->assertEmpty($repositoryUser); + + $migrationUser = $this->getNewUser($migration); + $email = $migrationUser['usr_email']; + + $repositoryUser = $repository->user()->findByEmail($email); + + $this->runAssertions($migrationUser, $repositoryUser); + } + + public function testFindById(): void + { + /** @var QueryRepository $repository */ + $repository = $this->container->get(Container::REPOSITORY); + + $migration = new UsersMigration($this->getConnection()); + + $repositoryUser = $repository->user()->findById(0); + $this->assertEmpty($repositoryUser); + + $migrationUser = $this->getNewUser($migration); + $userId = $migrationUser['usr_id']; + + $repositoryUser = $repository->user()->findById($userId); + + $this->runAssertions($migrationUser, $repositoryUser); + } + + private function runAssertions(array $dbUser, array $user): void + { + $expected = $dbUser['usr_id']; + $actual = $user['usr_id']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_status_flag']; + $actual = $user['usr_status_flag']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_email']; + $actual = $user['usr_email']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_password']; + $actual = $user['usr_password']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_prefix']; + $actual = $user['usr_name_prefix']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_first']; + $actual = $user['usr_name_first']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_middle']; + $actual = $user['usr_name_middle']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_last']; + $actual = $user['usr_name_last']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_suffix']; + $actual = $user['usr_name_suffix']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_issuer']; + $actual = $user['usr_issuer']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_token_password']; + $actual = $user['usr_token_password']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_token_id']; + $actual = $user['usr_token_id']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_preferences']; + $actual = $user['usr_preferences']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_date']; + $actual = $user['usr_created_date']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_usr_id']; + $actual = $user['usr_created_usr_id']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_date']; + $actual = $user['usr_updated_date']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_usr_id']; + $actual = $user['usr_updated_usr_id']; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/DataSource/User/UserRepositoryTest.php b/tests/Unit/Domain/DataSource/User/UserRepositoryTest.php deleted file mode 100644 index 16f4330..0000000 --- a/tests/Unit/Domain/DataSource/User/UserRepositoryTest.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * 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\DataSource\User; - -use Phalcon\Api\Domain\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; -use Phalcon\DataMapper\Pdo\Connection; - -final class UserRepositoryTest extends AbstractUnitTestCase -{ - public function testFind(): void - { - $container = new Container(); - /** @var Connection $connection */ - $connection = $container->getShared(Container::CONNECTION); - /** @var UserRepository $repository */ - $repository = $container->get(Container::REPOSITORY_USER); - - $migration = new UsersMigration($connection); - $this->setConnection($connection); - - $dbUser = $this->getNewUser($migration); - $userId = $dbUser['usr_id']; - - $user = $repository->findById($userId); - - $expected = $dbUser['usr_id']; - $actual = $user->getId(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_status_flag']; - $actual = $user->getStatus(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_username']; - $actual = $user->getUsername(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_password']; - $actual = $user->getPassword(); - $this->assertSame($expected, $actual); - } -} From c29cab2d36af0e8a190a68addacf83f90208a7da Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:22:52 -0500 Subject: [PATCH 083/301] [#.x] - removed routes interface --- src/Domain/Interfaces/RoutesInterface.php | 62 ----------------------- 1 file changed, 62 deletions(-) delete mode 100644 src/Domain/Interfaces/RoutesInterface.php diff --git a/src/Domain/Interfaces/RoutesInterface.php b/src/Domain/Interfaces/RoutesInterface.php deleted file mode 100644 index eb6d7c0..0000000 --- a/src/Domain/Interfaces/RoutesInterface.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * 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 BackedEnum; - -/** - * Interface for route enumerations - * - * @phpstan-type TMiddleware array - */ -interface RoutesInterface extends BackedEnum -{ - public const DELETE = 'delete'; - public const EVENT_BEFORE = 'before'; - public const EVENT_FINISH = 'finish'; - public const GET = 'get'; - public const POST = 'post'; - public const PUT = 'put'; - - - /** - * @return string - */ - public function endpoint(): string; - - /** - * @return string - */ - public function method(): string; - - /** - * @return TMiddleware - */ - public static function middleware(): array; - - /** - * @return string - */ - public function prefix(): string; - - /** - * @return string - */ - public function service(): string; - - /** - * @return string - */ - public function suffix(): string; -} From b538e058ad263f3605c6002cf3d94201ce04ae1c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:24:34 -0500 Subject: [PATCH 084/301] [#.x] - moved files to new locations --- .../ADR/{Domain => }/DomainInterface.php | 2 +- src/Domain/ADR/{Domain => }/Input.php | 8 +- .../ADR/{Domain => }/InputInterface.php | 2 +- src/Domain/ADR/{Domain => }/InputTypes.php | 2 +- .../{ => Components}/Constants/Dates.php | 2 +- .../{Services => Components}/Container.php | 73 +++++++++++-------- .../Enums}/Http/HttpCodesEnum.php | 6 +- .../Enums/Http}/RoutesEnum.php | 22 ++++-- .../Env/Adapters/AdapterInterface.php | 4 +- .../Env/Adapters/DotEnv.php | 6 +- .../Env/EnvFactory.php | 8 +- .../Env/EnvManager.php | 4 +- .../Env/EnvManagerTypes.php | 2 +- .../InvalidConfigurationArgumentException.php | 2 +- .../Middleware/AbstractMiddleware.php | 34 +++++---- .../Middleware/HealthMiddleware.php | 6 +- .../Middleware/NotFoundMiddleware.php | 4 +- .../Providers/ErrorHandlerProvider.php | 6 +- .../Providers/RouterProvider.php | 22 +++--- .../{ => Services}/Hello/HelloService.php | 10 +-- .../{ => Services}/User/UserGetService.php | 25 ++++--- .../ADR => }/Responder/ResponderInterface.php | 5 +- .../ADR => }/Responder/ResponderTypes.php | 14 +++- .../ContainerTest.php | 4 +- .../Enums}/Http/HttpCodesEnumTest.php | 4 +- .../Enums/Http}/RoutesEnumTest.php | 6 +- .../Env/Adapters/DotEnvTest.php | 12 +-- .../Env/EnvFactoryTest.php | 8 +- .../Env/EnvManagerTest.php | 8 +- .../Middleware/HealthMiddlewareTest.php | 9 +-- .../Middleware/NotFoundMiddlewareTest.php | 13 ++-- .../Providers/ErrorHandlerProviderTest.php | 25 +++---- .../Providers/RouterProviderTest.php | 13 ++-- .../{ => Services}/Hello/HelloServiceTest.php | 15 ++-- 34 files changed, 211 insertions(+), 175 deletions(-) rename src/Domain/ADR/{Domain => }/DomainInterface.php (93%) rename src/Domain/ADR/{Domain => }/Input.php (75%) rename src/Domain/ADR/{Domain => }/InputInterface.php (92%) rename src/Domain/ADR/{Domain => }/InputTypes.php (91%) rename src/Domain/{ => Components}/Constants/Dates.php (95%) rename src/Domain/{Services => Components}/Container.php (79%) rename src/Domain/{Services => Components/Enums}/Http/HttpCodesEnum.php (91%) rename src/Domain/{Services/Providers => Components/Enums/Http}/RoutesEnum.php (69%) rename src/Domain/{Services => Components}/Env/Adapters/AdapterInterface.php (83%) rename src/Domain/{Services => Components}/Env/Adapters/DotEnv.php (85%) rename src/Domain/{Services => Components}/Env/EnvFactory.php (83%) rename src/Domain/{Services => Components}/Env/EnvManager.php (97%) rename src/Domain/{Services => Components}/Env/EnvManagerTypes.php (91%) rename src/Domain/{ => Components}/Exceptions/InvalidConfigurationArgumentException.php (87%) rename src/Domain/{ => Components}/Middleware/AbstractMiddleware.php (55%) rename src/Domain/{ => Components}/Middleware/HealthMiddleware.php (88%) rename src/Domain/{ => Components}/Middleware/NotFoundMiddleware.php (90%) rename src/Domain/{Services => Components}/Providers/ErrorHandlerProvider.php (94%) rename src/Domain/{Services => Components}/Providers/RouterProvider.php (85%) rename src/Domain/{ => Services}/Hello/HelloService.php (79%) rename src/Domain/{ => Services}/User/UserGetService.php (60%) rename src/{Domain/ADR => }/Responder/ResponderInterface.php (78%) rename src/{Domain/ADR => }/Responder/ResponderTypes.php (53%) rename tests/Unit/Domain/{Services => Components}/ContainerTest.php (91%) rename tests/Unit/Domain/{Services => Components/Enums}/Http/HttpCodesEnumTest.php (95%) rename tests/Unit/Domain/{Services/Providers => Components/Enums/Http}/RoutesEnumTest.php (91%) rename tests/Unit/Domain/{Services => Components}/Env/Adapters/DotEnvTest.php (86%) rename tests/Unit/Domain/{Services => Components}/Env/EnvFactoryTest.php (78%) rename tests/Unit/Domain/{Services => Components}/Env/EnvManagerTest.php (94%) rename tests/Unit/Domain/{ => Components}/Middleware/HealthMiddlewareTest.php (84%) rename tests/Unit/Domain/{ => Components}/Middleware/NotFoundMiddlewareTest.php (82%) rename tests/Unit/Domain/{Services => Components}/Providers/ErrorHandlerProviderTest.php (84%) rename tests/Unit/Domain/{Services => Components}/Providers/RouterProviderTest.php (79%) rename tests/Unit/Domain/{ => Services}/Hello/HelloServiceTest.php (83%) diff --git a/src/Domain/ADR/Domain/DomainInterface.php b/src/Domain/ADR/DomainInterface.php similarity index 93% rename from src/Domain/ADR/Domain/DomainInterface.php rename to src/Domain/ADR/DomainInterface.php index d5c6a20..e06c9c0 100644 --- a/src/Domain/ADR/Domain/DomainInterface.php +++ b/src/Domain/ADR/DomainInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\ADR\Domain; +namespace Phalcon\Api\Domain\ADR; use Phalcon\Domain\Payload; diff --git a/src/Domain/ADR/Domain/Input.php b/src/Domain/ADR/Input.php similarity index 75% rename from src/Domain/ADR/Domain/Input.php rename to src/Domain/ADR/Input.php index 7d15456..95ffaed 100644 --- a/src/Domain/ADR/Domain/Input.php +++ b/src/Domain/ADR/Input.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\ADR\Domain; +namespace Phalcon\Api\Domain\ADR; -use Phalcon\Http\Request; +use Phalcon\Http\RequestInterface; /** * @phpstan-import-type TRequestQuery from InputTypes @@ -21,11 +21,11 @@ final class Input implements InputInterface { /** - * @param Request $request + * @param RequestInterface $request * * @return TRequestQuery */ - public function __invoke(Request $request): array + public function __invoke(RequestInterface $request): array { /** @var TRequestQuery $query */ $query = $request->getQuery(); diff --git a/src/Domain/ADR/Domain/InputInterface.php b/src/Domain/ADR/InputInterface.php similarity index 92% rename from src/Domain/ADR/Domain/InputInterface.php rename to src/Domain/ADR/InputInterface.php index d4a4b22..483bc1f 100644 --- a/src/Domain/ADR/Domain/InputInterface.php +++ b/src/Domain/ADR/InputInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\ADR\Domain; +namespace Phalcon\Api\Domain\ADR; use Phalcon\Http\Request; diff --git a/src/Domain/ADR/Domain/InputTypes.php b/src/Domain/ADR/InputTypes.php similarity index 91% rename from src/Domain/ADR/Domain/InputTypes.php rename to src/Domain/ADR/InputTypes.php index f85bfce..358ea99 100644 --- a/src/Domain/ADR/Domain/InputTypes.php +++ b/src/Domain/ADR/InputTypes.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\ADR\Domain; +namespace Phalcon\Api\Domain\ADR; /** * @phpstan-type THelloInput array{} diff --git a/src/Domain/Constants/Dates.php b/src/Domain/Components/Constants/Dates.php similarity index 95% rename from src/Domain/Constants/Dates.php rename to src/Domain/Components/Constants/Dates.php index 34a181d..c3f1f71 100644 --- a/src/Domain/Constants/Dates.php +++ b/src/Domain/Components/Constants/Dates.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Constants; +namespace Phalcon\Api\Domain\Components\Constants; use DateTimeImmutable; use DateTimeZone; diff --git a/src/Domain/Services/Container.php b/src/Domain/Components/Container.php similarity index 79% rename from src/Domain/Services/Container.php rename to src/Domain/Components/Container.php index d648f35..a1c9886 100644 --- a/src/Domain/Services/Container.php +++ b/src/Domain/Components/Container.php @@ -11,23 +11,25 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services; - -use Phalcon\Api\Domain\ADR\Responder\JsonResponder; -use Phalcon\Api\Domain\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Hello\HelloService; -use Phalcon\Api\Domain\Middleware\HealthMiddleware; -use Phalcon\Api\Domain\Middleware\NotFoundMiddleware; -use Phalcon\Api\Domain\Middleware\ResponseSenderMiddleware; -use Phalcon\Api\Domain\Services\Env\EnvManager; -use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Api\Domain\User\UserGetService; +namespace Phalcon\Api\Domain\Components; + +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Services\Hello\HelloService; +use Phalcon\Api\Domain\Services\User\UserGetService; +use Phalcon\Api\Responder\JsonResponder; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Di\Di; use Phalcon\Di\Service; use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\FilterFactory; use Phalcon\Http\Request; +use Phalcon\Http\Response; use Phalcon\Logger\Adapter\Stream; use Phalcon\Logger\Logger; use Phalcon\Mvc\Router; @@ -44,35 +46,38 @@ class Container extends Di public const EVENTS_MANAGER = 'eventsManager'; /** @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'; + /** @var string */ + public const SECURITY = Security::class; + /** @var string */ + public const TIME = 'time'; + /** * Services */ public const HELLO_SERVICE = HelloService::class; - /** @var string */ - public const LOGGER = 'logger'; + public const USER_GET_SERVICE = 'service.user.get'; /** * Middleware */ - public const MIDDLEWARE_HEALTH = HealthMiddleware::class; - public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; - public const MIDDLEWARE_RESPONSE_SENDER = ResponseSenderMiddleware::class; + public const MIDDLEWARE_HEALTH = HealthMiddleware::class; + public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; /** * Repositories */ - public const REPOSITORY_USER = 'repository.user'; - /** @var string */ - public const REQUEST = 'request'; + public const REPOSITORY = 'repository'; + public const REPOSITORY_TRANSPORT = TransportRepository::class; /** * Responders */ public const RESPONDER_JSON = JsonResponder::class; - /** @var string */ - public const RESPONSE = 'response'; - /** @var string */ - public const ROUTER = 'router'; - /** @var string */ - public const TIME = 'time'; - public const USER_GET_SERVICE = 'service.user.get'; public function __construct() { @@ -86,7 +91,7 @@ public function __construct() self::ROUTER => $this->getServiceRouter(), self::USER_GET_SERVICE => $this->getServiceUserGet(), - self::REPOSITORY_USER => $this->getServiceRepositoryUser(), + self::REPOSITORY => $this->getServiceRepository(), ]; parent::__construct(); @@ -186,11 +191,11 @@ function () use ($logName, $logFile) { /** * @return Service */ - private function getServiceRepositoryUser(): Service + private function getServiceRepository(): Service { return new Service( [ - 'className' => UserRepository::class, + 'className' => QueryRepository::class, 'arguments' => [ [ 'type' => 'service', @@ -230,7 +235,15 @@ private function getServiceUserGet(): Service 'arguments' => [ [ 'type' => 'service', - 'name' => self::REPOSITORY_USER, + 'name' => self::REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => self::REPOSITORY_TRANSPORT, + ], + [ + 'type' => 'service', + 'name' => self::FILTER, ], ], ] diff --git a/src/Domain/Services/Http/HttpCodesEnum.php b/src/Domain/Components/Enums/Http/HttpCodesEnum.php similarity index 91% rename from src/Domain/Services/Http/HttpCodesEnum.php rename to src/Domain/Components/Enums/Http/HttpCodesEnum.php index 2b9216f..3ccbe32 100644 --- a/src/Domain/Services/Http/HttpCodesEnum.php +++ b/src/Domain/Components/Enums/Http/HttpCodesEnum.php @@ -11,13 +11,15 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Http; +namespace Phalcon\Api\Domain\Components\Enums\Http; + +use Phalcon\Api\Domain\Components\Enums\EnumsInterface; /** * Enumeration for Http Codes. These are not only the regular HTTP codes * but also the ones generated by the application */ -enum HttpCodesEnum: int +enum HttpCodesEnum: int implements EnumsInterface { /** * Regular HTTP codes diff --git a/src/Domain/Services/Providers/RoutesEnum.php b/src/Domain/Components/Enums/Http/RoutesEnum.php similarity index 69% rename from src/Domain/Services/Providers/RoutesEnum.php rename to src/Domain/Components/Enums/Http/RoutesEnum.php index 3dc6b1c..d21fb0d 100644 --- a/src/Domain/Services/Providers/RoutesEnum.php +++ b/src/Domain/Components/Enums/Http/RoutesEnum.php @@ -11,18 +11,25 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Providers; +namespace Phalcon\Api\Domain\Components\Enums\Http; -use Phalcon\Api\Domain\Interfaces\RoutesInterface; -use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Components\Container; use function str_replace; /** - * @phpstan-import-type TMiddleware from RoutesInterface + * @phpstan-type TMiddleware array */ -enum RoutesEnum: string implements RoutesInterface +enum RoutesEnum: string { + public const EVENT_BEFORE = 'before'; + public const EVENT_FINISH = 'finish'; + + public const DELETE = 'delete'; + public const GET = 'get'; + public const POST = 'post'; + public const PUT = 'put'; + case helloGet = ''; case userGet = 'user'; @@ -51,9 +58,8 @@ public function method(): string public static function middleware(): array { return [ - Container::MIDDLEWARE_NOT_FOUND => self::EVENT_BEFORE, - Container::MIDDLEWARE_HEALTH => self::EVENT_BEFORE, - Container::MIDDLEWARE_RESPONSE_SENDER => self::EVENT_FINISH, + Container::MIDDLEWARE_NOT_FOUND => self::EVENT_BEFORE, + Container::MIDDLEWARE_HEALTH => self::EVENT_BEFORE, ]; } diff --git a/src/Domain/Services/Env/Adapters/AdapterInterface.php b/src/Domain/Components/Env/Adapters/AdapterInterface.php similarity index 83% rename from src/Domain/Services/Env/Adapters/AdapterInterface.php rename to src/Domain/Components/Env/Adapters/AdapterInterface.php index b15db06..bab4b30 100644 --- a/src/Domain/Services/Env/Adapters/AdapterInterface.php +++ b/src/Domain/Components/Env/Adapters/AdapterInterface.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Env\Adapters; +namespace Phalcon\Api\Domain\Components\Env\Adapters; -use Phalcon\Api\Domain\Services\Env\EnvManagerTypes; +use Phalcon\Api\Domain\Components\Env\EnvManagerTypes; /** * @phpstan-import-type TDotEnvOptions from EnvManagerTypes diff --git a/src/Domain/Services/Env/Adapters/DotEnv.php b/src/Domain/Components/Env/Adapters/DotEnv.php similarity index 85% rename from src/Domain/Services/Env/Adapters/DotEnv.php rename to src/Domain/Components/Env/Adapters/DotEnv.php index bd234fe..599239c 100644 --- a/src/Domain/Services/Env/Adapters/DotEnv.php +++ b/src/Domain/Components/Env/Adapters/DotEnv.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Env\Adapters; +namespace Phalcon\Api\Domain\Components\Env\Adapters; use Dotenv\Dotenv as ParentDotEnv; use Exception; -use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; -use Phalcon\Api\Domain\Services\Env\EnvManagerTypes; +use Phalcon\Api\Domain\Components\Env\EnvManagerTypes; +use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; /** * @phpstan-import-type TDotEnvOptions from EnvManagerTypes diff --git a/src/Domain/Services/Env/EnvFactory.php b/src/Domain/Components/Env/EnvFactory.php similarity index 83% rename from src/Domain/Services/Env/EnvFactory.php rename to src/Domain/Components/Env/EnvFactory.php index 84910e2..cb000ec 100644 --- a/src/Domain/Services/Env/EnvFactory.php +++ b/src/Domain/Components/Env/EnvFactory.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Env; +namespace Phalcon\Api\Domain\Components\Env; -use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; -use Phalcon\Api\Domain\Services\Env\Adapters\AdapterInterface; -use Phalcon\Api\Domain\Services\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Components\Env\Adapters\AdapterInterface; +use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; class EnvFactory { diff --git a/src/Domain/Services/Env/EnvManager.php b/src/Domain/Components/Env/EnvManager.php similarity index 97% rename from src/Domain/Services/Env/EnvManager.php rename to src/Domain/Components/Env/EnvManager.php index 6dde7a2..a39aa8a 100644 --- a/src/Domain/Services/Env/EnvManager.php +++ b/src/Domain/Components/Env/EnvManager.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Env; +namespace Phalcon\Api\Domain\Components\Env; -use Phalcon\Api\Domain\Constants\Dates; +use Phalcon\Api\Domain\Components\Constants\Dates; use function array_merge; use function getenv; diff --git a/src/Domain/Services/Env/EnvManagerTypes.php b/src/Domain/Components/Env/EnvManagerTypes.php similarity index 91% rename from src/Domain/Services/Env/EnvManagerTypes.php rename to src/Domain/Components/Env/EnvManagerTypes.php index 7819113..3acf713 100644 --- a/src/Domain/Services/Env/EnvManagerTypes.php +++ b/src/Domain/Components/Env/EnvManagerTypes.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Env; +namespace Phalcon\Api\Domain\Components\Env; /** * @phpstan-type TDotEnvOptions array{ diff --git a/src/Domain/Exceptions/InvalidConfigurationArgumentException.php b/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php similarity index 87% rename from src/Domain/Exceptions/InvalidConfigurationArgumentException.php rename to src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php index cf9c6de..8ea47de 100644 --- a/src/Domain/Exceptions/InvalidConfigurationArgumentException.php +++ b/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Exceptions; +namespace Phalcon\Api\Domain\Components\Exceptions; use InvalidArgumentException; diff --git a/src/Domain/Middleware/AbstractMiddleware.php b/src/Domain/Components/Middleware/AbstractMiddleware.php similarity index 55% rename from src/Domain/Middleware/AbstractMiddleware.php rename to src/Domain/Components/Middleware/AbstractMiddleware.php index 06c2513..a22463e 100644 --- a/src/Domain/Middleware/AbstractMiddleware.php +++ b/src/Domain/Components/Middleware/AbstractMiddleware.php @@ -11,19 +11,22 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Middleware; +namespace Phalcon\Api\Domain\Components\Middleware; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Http\Response; -use Phalcon\Api\Domain\Services\Http\ResponseTypes; +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Responder\JsonResponder; +use Phalcon\Api\Responder\ResponderTypes; +use Phalcon\Domain\Payload; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception as ResponseException; +use Phalcon\Http\ResponseInterface; use Phalcon\Mvc\Micro; use Phalcon\Mvc\Micro\MiddlewareInterface; /** - * @phpstan-import-type TData from ResponseTypes - * @phpstan-import-type TErrors from ResponseTypes + * @phpstan-import-type TData from ResponderTypes + * @phpstan-import-type TErrors from ResponderTypes */ abstract class AbstractMiddleware implements MiddlewareInterface { @@ -45,19 +48,22 @@ protected function halt( array $data = [], array $errors = [] ): void { - /** @var Response $response */ + /** @var ResponseInterface $response */ $response = $application->getSharedService(Container::RESPONSE); + /** @var JsonResponder $responder */ + $responder = $application->getService(Container::RESPONDER_JSON); $application->stop(); - $response->withCode($code, $message); + $results = [ + 'code' => $code, + 'message' => $message, + 'data' => $data, + 'errors' => $errors, + ]; - if (true === empty($errors)) { - $response->withPayloadData($data); - } else { - $response->withPayloadErrors($errors); - } + $payload = new Payload(DomainStatus::SUCCESS, $results); - $response->render()->send(); + $responder->__invoke($response, $payload); } } diff --git a/src/Domain/Middleware/HealthMiddleware.php b/src/Domain/Components/Middleware/HealthMiddleware.php similarity index 88% rename from src/Domain/Middleware/HealthMiddleware.php rename to src/Domain/Components/Middleware/HealthMiddleware.php index 898a51f..6e01eea 100644 --- a/src/Domain/Middleware/HealthMiddleware.php +++ b/src/Domain/Components/Middleware/HealthMiddleware.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Middleware; +namespace Phalcon\Api\Domain\Components\Middleware; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Middleware/NotFoundMiddleware.php b/src/Domain/Components/Middleware/NotFoundMiddleware.php similarity index 90% rename from src/Domain/Middleware/NotFoundMiddleware.php rename to src/Domain/Components/Middleware/NotFoundMiddleware.php index a5926e9..47293c2 100644 --- a/src/Domain/Middleware/NotFoundMiddleware.php +++ b/src/Domain/Components/Middleware/NotFoundMiddleware.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Middleware; +namespace Phalcon\Api\Domain\Components\Middleware; -use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Events\Event; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Services/Providers/ErrorHandlerProvider.php b/src/Domain/Components/Providers/ErrorHandlerProvider.php similarity index 94% rename from src/Domain/Services/Providers/ErrorHandlerProvider.php rename to src/Domain/Components/Providers/ErrorHandlerProvider.php index d455014..81452a9 100644 --- a/src/Domain/Services/Providers/ErrorHandlerProvider.php +++ b/src/Domain/Components/Providers/ErrorHandlerProvider.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Providers; +namespace Phalcon\Api\Domain\Components\Providers; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Logger\Logger; diff --git a/src/Domain/Services/Providers/RouterProvider.php b/src/Domain/Components/Providers/RouterProvider.php similarity index 85% rename from src/Domain/Services/Providers/RouterProvider.php rename to src/Domain/Components/Providers/RouterProvider.php index fe13b29..c17da85 100644 --- a/src/Domain/Services/Providers/RouterProvider.php +++ b/src/Domain/Components/Providers/RouterProvider.php @@ -11,23 +11,23 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Providers; +namespace Phalcon\Api\Domain\Components\Providers; -use Phalcon\Api\Domain\ADR\Action\ActionHandler; -use Phalcon\Api\Domain\ADR\Domain\DomainInterface; -use Phalcon\Api\Domain\ADR\Responder\ResponderInterface; -use Phalcon\Api\Domain\Interfaces\RoutesInterface; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Http\Response; +use Phalcon\Api\Action\ActionHandler; +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; +use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Events\Manager as EventsManager; -use Phalcon\Http\Request; +use Phalcon\Http\RequestInterface; +use Phalcon\Http\ResponseInterface; use Phalcon\Mvc\Micro; use Phalcon\Mvc\Micro\Collection; /** - * @phpstan-import-type TMiddleware from RoutesInterface + * @phpstan-import-type TMiddleware from RoutesEnum */ class RouterProvider implements ServiceProviderInterface { @@ -77,9 +77,9 @@ private function attachMiddleware( */ private function attachRoutes(Micro $application): void { - /** @var Request $request */ + /** @var RequestInterface $request */ $request = $application->getService(Container::REQUEST); - /** @var Response $response */ + /** @var ResponseInterface $response */ $response = $application->getService(Container::RESPONSE); /** @var ResponderInterface $responder */ $responder = $application->getService(Container::RESPONDER_JSON); diff --git a/src/Domain/Hello/HelloService.php b/src/Domain/Services/Hello/HelloService.php similarity index 79% rename from src/Domain/Hello/HelloService.php rename to src/Domain/Services/Hello/HelloService.php index caf9fe0..4e8565b 100644 --- a/src/Domain/Hello/HelloService.php +++ b/src/Domain/Services/Hello/HelloService.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Hello; +namespace Phalcon\Api\Domain\Services\Hello; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\ADR\Domain\DomainInterface; -use Phalcon\Api\Domain\ADR\Domain\InputTypes; -use Phalcon\Api\Domain\Constants\Dates; +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Domain\Payload; use function date; @@ -36,7 +36,7 @@ public function __invoke(array $input): Payload return new Payload( DomainStatus::SUCCESS, [ - 'results' => [ + 'data' => [ "Hello World!!! - " . date(Dates::DATE_TIME_FORMAT), ], ] diff --git a/src/Domain/User/UserGetService.php b/src/Domain/Services/User/UserGetService.php similarity index 60% rename from src/Domain/User/UserGetService.php rename to src/Domain/Services/User/UserGetService.php index 227b168..21f2e02 100644 --- a/src/Domain/User/UserGetService.php +++ b/src/Domain/Services/User/UserGetService.php @@ -11,14 +11,18 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\User; +namespace Phalcon\Api\Domain\Services\User; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\ADR\Domain\DomainInterface; -use Phalcon\Api\Domain\ADR\Domain\InputTypes; -use Phalcon\Api\Domain\DataSource\User\UserRepository; +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Domain\Payload; +use Phalcon\Filter\Filter; + use function abs; /** @@ -27,7 +31,9 @@ final readonly class UserGetService implements DomainInterface { public function __construct( - private UserRepository $userRepository + private QueryRepository $repository, + private TransportRepository $transport, + private Filter $filter ) { } @@ -38,19 +44,20 @@ public function __construct( */ public function __invoke(array $input): Payload { - $userId = abs((int)($input['userId'] ?? 0)); + $userId = $this->filter->absint($input['userId'] ?? 0); /** * Success */ if ($userId > 0) { - $user = $this->userRepository->findById($userId); + $dbUser = $this->repository->user()->findById($userId); + $user = $this->transport->newUser($dbUser); if (true !== $user->isEmpty()) { return new Payload( DomainStatus::SUCCESS, [ - 'results' => $user->toArray(), + 'data' => $user->toArray(), ] ); } @@ -62,7 +69,7 @@ public function __invoke(array $input): Payload return new Payload( DomainStatus::NOT_FOUND, [ - 'results' => [ + 'errors' => [ 'Record(s) not found', ], ] diff --git a/src/Domain/ADR/Responder/ResponderInterface.php b/src/Responder/ResponderInterface.php similarity index 78% rename from src/Domain/ADR/Responder/ResponderInterface.php rename to src/Responder/ResponderInterface.php index a458a05..7b5342a 100644 --- a/src/Domain/ADR/Responder/ResponderInterface.php +++ b/src/Responder/ResponderInterface.php @@ -11,16 +11,15 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\ADR\Responder; +namespace Phalcon\Api\Responder; -use Phalcon\Api\Domain\Services\Http\Response; use Phalcon\Domain\Payload; use Phalcon\Http\ResponseInterface; interface ResponderInterface { public function __invoke( - Response $response, + ResponseInterface $response, Payload $payload ): ResponseInterface; } diff --git a/src/Domain/ADR/Responder/ResponderTypes.php b/src/Responder/ResponderTypes.php similarity index 53% rename from src/Domain/ADR/Responder/ResponderTypes.php rename to src/Responder/ResponderTypes.php index 7a895ee..282fbe3 100644 --- a/src/Domain/ADR/Responder/ResponderTypes.php +++ b/src/Responder/ResponderTypes.php @@ -11,13 +11,25 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\ADR\Responder; +namespace Phalcon\Api\Responder; /** * @phpstan-type TResultItem array * @phpstan-type TResult array{ * result: array * } + * @phpstan-type TData array|array{} + * @phpstan-type TErrors array>|array{} + * @phpstan-type TResponsePayload array{ + * data: TData, + * errors: TErrors, + * meta: array{ + * code: int, + * hash: string, + * message: string, + * timestamp: string + * } + * } */ final class ResponderTypes { diff --git a/tests/Unit/Domain/Services/ContainerTest.php b/tests/Unit/Domain/Components/ContainerTest.php similarity index 91% rename from tests/Unit/Domain/Services/ContainerTest.php rename to tests/Unit/Domain/Components/ContainerTest.php index 073e821..c40a728 100644 --- a/tests/Unit/Domain/Services/ContainerTest.php +++ b/tests/Unit/Domain/Components/ContainerTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services; +namespace Phalcon\Api\Tests\Unit\Domain\Components; -use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\Filter; diff --git a/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php b/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php similarity index 95% rename from tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php rename to tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php index 40d6d06..51b342d 100644 --- a/tests/Unit/Domain/Services/Http/HttpCodesEnumTest.php +++ b/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Http; +namespace Phalcon\Api\Tests\Unit\Domain\Components\Enums\Http; -use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php b/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php similarity index 91% rename from tests/Unit/Domain/Services/Providers/RoutesEnumTest.php rename to tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php index 43c35fa..5f13f7c 100644 --- a/tests/Unit/Domain/Services/Providers/RoutesEnumTest.php +++ b/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Components\Enums\Http; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Providers\RoutesEnum; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php similarity index 86% rename from tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php rename to tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php index 458b147..60099b1 100644 --- a/tests/Unit/Domain/Services/Env/Adapters/DotEnvTest.php +++ b/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Env\Adapters; +namespace Phalcon\Api\Tests\Unit\Domain\Components\Env\Adapters; -use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; -use Phalcon\Api\Domain\Services\Env\Adapters\DotEnv; -use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; use Phalcon\Api\Tests\AbstractUnitTestCase; final class DotEnvTest extends AbstractUnitTestCase @@ -82,9 +82,9 @@ public function testLoadSuccess(): void $this->assertSame($expected, $actualArray); } - protected function setUp(): void + public function setUp(): void { $this->envFile = EnvManager::appPath() - . '/tests/Fixtures/Domain/Services/Env/'; + . '/tests/Fixtures/Domain/Components/Env/'; } } diff --git a/tests/Unit/Domain/Services/Env/EnvFactoryTest.php b/tests/Unit/Domain/Components/Env/EnvFactoryTest.php similarity index 78% rename from tests/Unit/Domain/Services/Env/EnvFactoryTest.php rename to tests/Unit/Domain/Components/Env/EnvFactoryTest.php index 57319a7..90aa664 100644 --- a/tests/Unit/Domain/Services/Env/EnvFactoryTest.php +++ b/tests/Unit/Domain/Components/Env/EnvFactoryTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Env; +namespace Phalcon\Api\Tests\Unit\Domain\Components\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\Components\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Components\Env\EnvFactory; +use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; use Phalcon\Api\Tests\AbstractUnitTestCase; final class EnvFactoryTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Services/Env/EnvManagerTest.php b/tests/Unit/Domain/Components/Env/EnvManagerTest.php similarity index 94% rename from tests/Unit/Domain/Services/Env/EnvManagerTest.php rename to tests/Unit/Domain/Components/Env/EnvManagerTest.php index 7af51a7..3aa7864 100644 --- a/tests/Unit/Domain/Services/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Components/Env/EnvManagerTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Env; +namespace Phalcon\Api\Tests\Unit\Domain\Components\Env; -use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; use ReflectionClass; @@ -81,7 +81,7 @@ public function testGetFromDotEnvLoad(): void $_ENV = [ 'APP_ENV_ADAPTER' => 'dotenv', 'APP_ENV_FILE_PATH' => EnvManager::appPath() - . '/tests/Fixtures/Domain/Services/Env/', + . '/tests/Fixtures/Domain/Components/Env/', ]; $values = [ @@ -112,7 +112,7 @@ public function testGetFromDotEnvLoad(): void $this->assertSame($expected, $actual); } - protected function setUp(): void + public function setUp(): void { $ref = new ReflectionClass(EnvManager::class); $ref->setStaticPropertyValue('isLoaded', false); diff --git a/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php similarity index 84% rename from tests/Unit/Domain/Middleware/HealthMiddlewareTest.php rename to tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php index 4d652cb..2fb0cfb 100644 --- a/tests/Unit/Domain/Middleware/HealthMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; -use Phalcon\Api\Domain\Middleware\HealthMiddleware; -use Phalcon\Api\Domain\Services\Container; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -27,8 +27,7 @@ final class HealthMiddlewareTest extends AbstractUnitTestCase { public function testCall(): void { - $container = new Container(); - $application = new Micro($container); + $application = new Micro($this->container); $middleware = new HealthMiddleware(); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); diff --git a/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/NotFoundMiddlewareTest.php similarity index 82% rename from tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php rename to tests/Unit/Domain/Components/Middleware/NotFoundMiddlewareTest.php index 3c167e8..146fac8 100644 --- a/tests/Unit/Domain/Middleware/NotFoundMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/NotFoundMiddlewareTest.php @@ -11,11 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; -use Phalcon\Api\Domain\Middleware\NotFoundMiddleware; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Events\Event; use Phalcon\Mvc\Micro; @@ -29,8 +28,7 @@ final class NotFoundMiddlewareTest extends AbstractUnitTestCase { public function testBeforeNotFound(): void { - $container = new Container(); - $application = new Micro($container); + $application = new Micro($this->container); $middleware = new NotFoundMiddleware(); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); @@ -58,8 +56,7 @@ public function testBeforeNotFound(): void public function testCall(): void { - $container = new Container(); - $application = new Micro($container); + $application = new Micro($this->container); $middleware = new NotFoundMiddleware(); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); diff --git a/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php similarity index 84% rename from tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php rename to tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php index 9db3fd2..71c0118 100644 --- a/tests/Unit/Domain/Services/Providers/ErrorHandlerProviderTest.php +++ b/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Env\EnvManager; -use Phalcon\Api\Domain\Services\Providers\ErrorHandlerProvider; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Components\Providers\ErrorHandlerProvider; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; use ReflectionClass; @@ -33,9 +33,8 @@ final class ErrorHandlerProviderTest extends AbstractUnitTestCase { public function testCheckRegistration(): void { - $container = new Container(); $now = hrtime(true); - $container->set( + $this->container->set( Container::TIME, function () use ($now) { return $now; @@ -44,7 +43,7 @@ function () use ($now) { ); $provider = new ErrorHandlerProvider(); - $provider->register($container); + $provider->register($this->container); $expected = date_default_timezone_get(); $actual = EnvManager::appTimezone(); @@ -63,9 +62,8 @@ function () use ($now) { public function testRegisterSetsHandlersAndLogs(): void { - $container = new Container(); $now = hrtime(true); - $container->set( + $this->container->set( Container::TIME, function () use ($now) { return $now; @@ -74,7 +72,7 @@ function () use ($now) { ); $provider = new ErrorHandlerProvider(); - $provider->register($container); + $provider->register($this->container); $message = uniqid('msg-'); // Trigger an error @@ -102,9 +100,8 @@ public function testRegisterShutdown(): void $_ENV['APP_ENV'] = 'development'; $_ENV['APP_LOG_LEVEL'] = 2; - $container = new Container(); $now = hrtime(true); - $container->set( + $this->container->set( Container::TIME, function () use ($now) { return $now; @@ -113,13 +110,13 @@ function () use ($now) { ); $provider = new ErrorHandlerProvider(); - $provider->register($container); + $provider->register($this->container); // Directly call shutdown for coverage and checking if it works $reflection = new ReflectionClass($provider); $method = $reflection->getMethod('onShutdown'); $method->setAccessible(true); - $shutdown = $method->invoke($provider, $container); + $shutdown = $method->invoke($provider, $this->container); restore_error_handler(); diff --git a/tests/Unit/Domain/Services/Providers/RouterProviderTest.php b/tests/Unit/Domain/Components/Providers/RouterProviderTest.php similarity index 79% rename from tests/Unit/Domain/Services/Providers/RouterProviderTest.php rename to tests/Unit/Domain/Components/Providers/RouterProviderTest.php index 77810d6..1ae63f4 100644 --- a/tests/Unit/Domain/Services/Providers/RouterProviderTest.php +++ b/tests/Unit/Domain/Components/Providers/RouterProviderTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Providers\RouterProvider; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Providers\RouterProvider; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; @@ -22,12 +22,11 @@ final class RouterProviderTest extends AbstractUnitTestCase { public function testCheckRegistration(): void { - $container = new Container(); - $application = new Micro($container); - $container->setShared(Container::APPLICATION, $application); + $application = new Micro($this->container); + $this->container->setShared(Container::APPLICATION, $application); $provider = new RouterProvider(); - $provider->register($container); + $provider->register($this->container); $router = $application->getRouter(); $routes = $router->getRoutes(); diff --git a/tests/Unit/Domain/Hello/HelloServiceTest.php b/tests/Unit/Domain/Services/Hello/HelloServiceTest.php similarity index 83% rename from tests/Unit/Domain/Hello/HelloServiceTest.php rename to tests/Unit/Domain/Services/Hello/HelloServiceTest.php index 64082ac..0ea190b 100644 --- a/tests/Unit/Domain/Hello/HelloServiceTest.php +++ b/tests/Unit/Domain/Services/Hello/HelloServiceTest.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Hello; +namespace Phalcon\Api\Tests\Unit\Domain\Services\Hello; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Hello\HelloService; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Env\EnvManager; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Services\Hello\HelloService; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -62,9 +62,8 @@ public function testDispatch(): void public function testService(): void { - $container = new Container(); /** @var HelloService $service */ - $service = $container->get(Container::HELLO_SERVICE); + $service = $this->container->get(Container::HELLO_SERVICE); $payload = $service->__invoke([]); @@ -73,10 +72,10 @@ public function testService(): void $this->assertSame($expected, $actual); $actual = $payload->getResult(); - $this->assertArrayHasKey('results', $actual); + $this->assertArrayHasKey('data', $actual); $expected = 'Hello World!!! - '; - $actual = $actual['results'][0]; + $actual = $actual['data'][0]; $this->assertStringContainsString($expected, $actual); } } From 891594cc6d2d17e92c312726142da92e89f56706 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:24:57 -0500 Subject: [PATCH 085/301] [#.x] - adjusted test migration with new fields --- .../Domain/Migrations/UsersMigration.php | 91 +++++++++-- tests/Unit/Domain/User/UserServiceTest.php | 149 ------------------ 2 files changed, 75 insertions(+), 165 deletions(-) delete mode 100644 tests/Unit/Domain/User/UserServiceTest.php diff --git a/tests/Fixtures/Domain/Migrations/UsersMigration.php b/tests/Fixtures/Domain/Migrations/UsersMigration.php index e164b4e..85d59ff 100644 --- a/tests/Fixtures/Domain/Migrations/UsersMigration.php +++ b/tests/Fixtures/Domain/Migrations/UsersMigration.php @@ -20,30 +20,89 @@ final class UsersMigration extends AbstractMigration protected string $table = 'co_users'; public function insert( - ?int $id = null, - int $status = 0, - ?string $username = null, - ?string $password = null, - ) { - $sql = "INSERT INTO {$this->table} ( - usr_id, usr_status_flag, usr_username, usr_password + ?int $id, + int $status, + string $email, + string $password, + string $namePrefix, + string $nameFirst, + string $nameMiddle, + string $nameLast, + string $nameSuffix, + string $issuer, + string $tokenPassword, + string $tokenId, + string $preferences, + string $createdDate, + int $createdUserId, + string $updatedDate, + int $updatedUserId + ): int { + $sql = "INSERT INTO $this->table ( + usr_id, + usr_status_flag, + usr_email, + usr_password, + usr_name_prefix, + usr_name_first, + usr_name_middle, + usr_name_last, + usr_name_suffix, + usr_issuer, + usr_token_password, + usr_token_id, + usr_preferences, + usr_created_date, + usr_created_usr_id, + usr_updated_date, + usr_updated_usr_id ) VALUES ( - :id, :status, :username, :password + :id, + :status, + :email, + :password, + :namePrefix, + :nameFirst, + :nameMiddle, + :nameLast, + :nameSuffix, + :issuer, + :tokenPassword, + :tokenId, + :preferences, + :createdDate, + :createdUserId, + :updatedDate, + :updatedUserId )"; - $stmt = $this->connection->prepare($sql); - $params = [ - ':id' => $id, - ':status' => $status, - ':username' => $username, - ':password' => $password, + + $stmt = $this->connection->prepare($sql); + $params = [ + ':id' => $id, + ':status' => $status, + ':email' => $email, + ':password' => $password, + ':namePrefix' => $namePrefix, + ':nameFirst' => $nameFirst, + ':nameMiddle' => $nameMiddle, + ':nameLast' => $nameLast, + ':nameSuffix' => $nameSuffix, + ':issuer' => $issuer, + ':tokenPassword' => $tokenPassword, + ':tokenId' => $tokenId, + ':preferences' => $preferences, + ':createdDate' => $createdDate, + ':createdUserId' => $createdUserId, + ':updatedDate' => $updatedDate, + ':updatedUserId' => $updatedUserId, ]; - $result = $stmt->execute($params); + $result = $stmt->execute($params); if (!$result) { Assert::fail( "Failed to insert id [#$id] into table [$this->table]" ); } - return $result; + return (int)$this->connection->lastInsertId(); } } diff --git a/tests/Unit/Domain/User/UserServiceTest.php b/tests/Unit/Domain/User/UserServiceTest.php deleted file mode 100644 index 85933d7..0000000 --- a/tests/Unit/Domain/User/UserServiceTest.php +++ /dev/null @@ -1,149 +0,0 @@ - - * - * 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\User; - -use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\User\UserGetService; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; -use Phalcon\DataMapper\Pdo\Connection; -use PHPUnit\Framework\Attributes\BackupGlobals; - -#[BackupGlobals(true)] -final class UserServiceTest extends AbstractUnitTestCase -{ -// public function testDispatch(): void -// { -// $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); -// $_SERVER = [ -// 'REQUEST_METHOD' => 'GET', -// 'REQUEST_TIME_FLOAT' => $time, -// 'REQUEST_URI' => '/', -// ]; -// -// ob_start(); -// require_once EnvManager::appPath('public/index.php'); -// $response = ob_get_clean(); -// -// $contents = json_decode($response, true); -// -// restore_error_handler(); -// -// $this->assertArrayHasKey('data', $contents); -// $this->assertArrayHasKey('errors', $contents); -// -// $data = $contents['data']; -// $errors = $contents['errors']; -// -// $expected = []; -// $actual = $errors; -// $this->assertSame($expected, $actual); -// -// $expected = 'Hello World!!! - '; -// $actual = $data[0]; -// $this->assertStringContainsString($expected, $actual); -// } -// - public function testServiceEmptyUserId(): void - { - $container = new Container(); - /** @var UserGetService $service */ - $service = $container->get(Container::USER_GET_SERVICE); - - $payload = $service->__invoke([]); - - $expected = DomainStatus::NOT_FOUND; - $actual = $payload->getStatus(); - $this->assertSame($expected, $actual); - - $actual = $payload->getResult(); - $this->assertArrayHasKey('results', $actual); - - $expected = 'Record(s) not found'; - $actual = $actual['results'][0]; - $this->assertStringContainsString($expected, $actual); - } - - public function testServiceWithUserId(): void - { - $container = new Container(); - /** @var Connection $connection */ - $connection = $container->getShared(Container::CONNECTION); - /** @var UserGetService $service */ - $service = $container->get(Container::USER_GET_SERVICE); - - $migration = new UsersMigration($connection); - $this->setConnection($connection); - $dbUser = $this->getNewUser($migration); - $userId = $dbUser['usr_id']; - - $payload = $service->__invoke( - [ - 'userId' => $userId, - ] - ); - - $expected = DomainStatus::SUCCESS; - $actual = $payload->getStatus(); - $this->assertSame($expected, $actual); - - $actual = $payload->getResult(); - $this->assertArrayHasKey('results', $actual); - - $user = $actual['results']; - $key = array_key_first($user); - $user = $user[$key]; - - $expected = $dbUser['usr_id']; - $actual = $user['id']; - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_status_flag']; - $actual = $user['status']; - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_username']; - $actual = $user['username']; - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_password']; - $actual = $user['password']; - $this->assertSame($expected, $actual); - } - - public function testServiceWrongUserId(): void - { - $container = new Container(); - /** @var UserGetService $service */ - $service = $container->get(Container::USER_GET_SERVICE); - - $payload = $service->__invoke( - [ - 'userId' => 999999, - ] - ); - - $expected = DomainStatus::NOT_FOUND; - $actual = $payload->getStatus(); - $this->assertSame($expected, $actual); - - $actual = $payload->getResult(); - $this->assertArrayHasKey('results', $actual); - - $expected = 'Record(s) not found'; - $actual = $actual['results'][0]; - $this->assertStringContainsString($expected, $actual); - } -} From 5ab667f769e03f24ca44ef881ee2702003f32eac Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:25:28 -0500 Subject: [PATCH 086/301] [#.x] - new setup for tests with container; removed unnecessary method --- tests/AbstractUnitTestCase.php | 99 ++++++++++++++++--- .../Domain/Migrations/AbstractMigration.php | 8 -- 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 3abc5b3..617c9f1 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -13,7 +13,11 @@ namespace Phalcon\Api\Tests; +use Faker\Factory; use PDO; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\DataMapper\Pdo\Connection; use PHPUnit\Framework\TestCase; @@ -22,8 +26,23 @@ abstract class AbstractUnitTestCase extends TestCase { + /** + * @var Connection|null + */ protected ?Connection $connection = null; + /** + * @var Container + */ + protected Container $container; + + public function setUp(): void + { + parent::setUp(); + + $this->container = new Container(); + } + /** * @param string $fileName * @param string $stream @@ -50,6 +69,19 @@ public function assertNotInDatabase(string $table, array $criteria = []): void $this->assertEmpty($records); } + public function getConnection(): Connection + { + if (null === $this->connection) { + /** @var Connection $connection */ + $connection = $this->container->getShared(Container::CONNECTION); + + $this->connection = $connection; + } + + return $this->connection; + } + + /** * @param UsersMigration $migration * @param array $fields @@ -58,22 +90,67 @@ public function assertNotInDatabase(string $table, array $criteria = []): void */ public function getNewUser(UsersMigration $migration, array $fields = []): array { - $status = $fields['usr_status_flag'] ?? 1; - $username = $fields['usr_username'] ?? uniqid('name-'); - $password = $fields['usr_password'] ?? $this->getStrongPassword(); - - $migration->insert(null, $status, $username, $password); + $userData = $this->getNewUserData($fields); + $userId = $migration->insert( + null, + $userData['usr_status_flag'], + $userData['usr_email'], + $userData['usr_password'], + $userData['usr_name_prefix'], + $userData['usr_name_first'], + $userData['usr_name_middle'], + $userData['usr_name_last'], + $userData['usr_name_suffix'], + $userData['usr_issuer'], + $userData['usr_token_password'], + $userData['usr_token_id'], + $userData['usr_preferences'], + $userData['usr_created_date'], + $userData['usr_created_usr_id'], + $userData['usr_updated_date'], + $userData['usr_updated_usr_id'] + ); $dbUser = $this->getFromDatabase( 'co_users', [ - 'usr_username' => $username, + 'usr_id' => $userId, ] ); return $dbUser[0]; } + public function getNewUserData(array $fields = []): array + { + $faker = Factory::create(); + $password = $fields['usr_password'] ?? $this->getStrongPassword(); + /** @var Security $security */ + $security = $this->container->get(Container::SECURITY); + + $password = $security->hash($password); + + return [ + 'usr_id' => 0, + 'usr_status_flag' => $fields['usr_status_flag'] ?? 1, + 'usr_email' => $fields['usr_email'] ?? $faker->email, + 'usr_password' => $password, + 'usr_name_prefix' => $fields['usr_name_prefix'] ?? $faker->title(), + 'usr_name_first' => $fields['usr_name_first'] ?? $faker->firstName(), + 'usr_name_middle' => $fields['usr_name_middle'] ?? $faker->firstName(), + 'usr_name_last' => $fields['usr_name_last'] ?? $faker->lastName(), + 'usr_name_suffix' => $fields['usr_name_suffix'] ?? $faker->suffix(), + 'usr_issuer' => $fields['usr_issuer'] ?? $faker->url(), + 'usr_token_password' => $fields['usr_token_password'] ?? $this->getStrongPassword(), + 'usr_token_id' => $fields['usr_token_id'] ?? $this->getStrongPassword(), + 'usr_preferences' => $fields['usr_preferences'] ?? '', + 'usr_created_date' => $fields['usr_created_date'] ?? $faker->date(Dates::DATE_TIME_FORMAT), + 'usr_created_usr_id' => $fields['usr_created_usr_id'] ?? 0, + 'usr_updated_date' => $fields['usr_updated_date'] ?? $faker->date(Dates::DATE_TIME_FORMAT), + 'usr_updated_usr_id' => $fields['usr_updated_usr_id'] ?? 0, + ]; + } + /** * Return a long series of strings to be used as a password * @@ -84,16 +161,6 @@ public function getStrongPassword(): string return substr(base64_encode(random_bytes(512)), 0, 128); } - /** - * @param Connection $connection - * - * @return void - */ - public function setConnection(Connection $connection): void - { - $this->connection = $connection; - } - /** * @param string $table * @param array $criteria diff --git a/tests/Fixtures/Domain/Migrations/AbstractMigration.php b/tests/Fixtures/Domain/Migrations/AbstractMigration.php index b4d39b7..06dad6e 100644 --- a/tests/Fixtures/Domain/Migrations/AbstractMigration.php +++ b/tests/Fixtures/Domain/Migrations/AbstractMigration.php @@ -32,12 +32,4 @@ public function clear(): int { return $this->connection->exec('DELETE FROM ' . $this->table); } - - /** - * @return string - */ - public function getTable(): string - { - return $this->table; - } } From c174d8351e58a93fc3109fbdeb84340687645b00 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:25:49 -0500 Subject: [PATCH 087/301] [#.x] - adjusted user transport and stan types --- .../DataSource/User/UserTransport.php | 135 ++++++++++++++++++ .../Components/DataSource/User/UserTypes.php | 61 ++++++++ src/Domain/DataSource/User/UserTransport.php | 77 ---------- src/Domain/DataSource/User/UserTypes.php | 33 ----- 4 files changed, 196 insertions(+), 110 deletions(-) create mode 100644 src/Domain/Components/DataSource/User/UserTransport.php create mode 100644 src/Domain/Components/DataSource/User/UserTypes.php delete mode 100644 src/Domain/DataSource/User/UserTransport.php delete mode 100644 src/Domain/DataSource/User/UserTypes.php diff --git a/src/Domain/Components/DataSource/User/UserTransport.php b/src/Domain/Components/DataSource/User/UserTransport.php new file mode 100644 index 0000000..98f71eb --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserTransport.php @@ -0,0 +1,135 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; + +/** + * The domain representation of a user. + * + * @method int getId() + * @method int getTenantId() + * @method string getStatus() + * @method string getEmail() + * @method string getPassword() + * @method string getNamePrefix() + * @method string getNameFirst() + * @method string getNameMiddle() + * @method string getNameLast() + * @method string getNameSuffix() + * @method string getIssuer() + * @method string getTokenPassword() + * @method string getTokenId() + * @method string getPreferences() + * @method string getCreatedDate() + * @method int|null getCreatedUserId() + * @method string getUpdatedDate() + * @method int|null getUpdatedUserId() + * + * @phpstan-import-type TUserRecord from UserTypes + * @phpstan-import-type TUserTransport from UserTypes + */ +final class UserTransport +{ + /** @var TUserTransport */ + private array $store; + + /** + * @param TUserRecord $input + */ + public function __construct(array $input) + { + $this->store = [ + 'id' => (int)($input['usr_id'] ?? 0), + 'status' => (int)($input['usr_status_flag'] ?? 0), + 'email' => (string)($input['usr_email'] ?? ''), + 'password' => (string)($input['usr_password'] ?? ''), + 'namePrefix' => (string)($input['usr_name_prefix'] ?? ''), + 'nameFirst' => (string)($input['usr_name_first'] ?? ''), + 'nameMiddle' => (string)($input['usr_name_middle'] ?? ''), + 'nameLast' => (string)($input['usr_name_last'] ?? ''), + 'nameSuffix' => (string)($input['usr_name_suffix'] ?? ''), + 'issuer' => (string)($input['usr_issuer'] ?? ''), + 'tokenPassword' => (string)($input['usr_token_password'] ?? ''), + 'tokenId' => (string)($input['usr_token_id'] ?? ''), + 'preferences' => (string)($input['usr_preferences'] ?? ''), + 'createdDate' => (string)($input['usr_created_date'] ?? ''), + 'createdUserId' => (int)($input['usr_created_usr_id'] ?? 0), + 'updatedDate' => (string)($input['usr_updated_date'] ?? ''), + 'updatedUserId' => (int)($input['usr_updated_usr_id'] ?? 0), + ]; + + $this->store['fullName'] = $this->getFullName(); + } + + /** + * @param string $name + * @param array $arguments + * + * @return int|string + */ + public function __call(string $name, array $arguments): int | string + { + return match ($name) { + 'getId' => $this->store['id'], + 'getStatus' => $this->store['status'], + 'getEmail' => $this->store['email'], + 'getPassword' => $this->store['password'], + 'getNamePrefix' => $this->store['namePrefix'], + 'getNameFirst' => $this->store['nameFirst'], + 'getNameMiddle' => $this->store['nameMiddle'], + 'getNameLast' => $this->store['nameLast'], + 'getNameSuffix' => $this->store['nameSuffix'], + 'getIssuer' => $this->store['issuer'], + 'getTokenPassword' => $this->store['tokenPassword'], + 'getTokenId' => $this->store['tokenId'], + 'getPreferences' => $this->store['preferences'], + 'getCreatedDate' => $this->store['createdDate'], + 'getCreatedUserId' => $this->store['createdUserId'], + 'getUpdatedDate' => $this->store['updatedDate'], + 'getUpdatedUserId' => $this->store['updatedUserId'], + default => throw new InvalidConfigurationArgumentException( + 'The ' . $name . ' method is not supported. [' + . json_encode($arguments) . ']', + ), + }; + } + + /** + * @return string + */ + public function getFullName(): string + { + return trim( + $this->getNameLast() + . ', ' + . $this->getNameFirst() + . ' ' + . $this->getNameMiddle() + ); + } + + public function isEmpty(): bool + { + return 0 === $this->store['id']; + } + + /** + * @return array + */ + public function toArray(): array + { + return [$this->store['id'] => $this->store]; + } +} diff --git a/src/Domain/Components/DataSource/User/UserTypes.php b/src/Domain/Components/DataSource/User/UserTypes.php new file mode 100644 index 0000000..d9b6bf2 --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserTypes.php @@ -0,0 +1,61 @@ + + * + * 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\Components\DataSource\User; + +/** + * @phpstan-type TUserDbRecord array{ + * usr_id: int, + * usr_status_flag: int, + * usr_email: string, + * usr_password: string, + * usr_name_prefix: string, + * usr_name_first: string, + * usr_name_middle: string, + * usr_name_last: string, + * usr_name_suffix: string, + * usr_issuer: string, + * usr_token_password: string, + * usr_token_id: string, + * usr_preferences: string, + * usr_created_date: string, + * usr_created_usr_id: int, + * usr_updated_date: string, + * usr_updated_usr_id: int, + * } + * + * @phpstan-type TUserRecord array{}|TUserDbRecord + * + * @phpstan-type TUserTransport array{ + * id: int, + * status: int, + * email: string, + * password: string, + * namePrefix: string, + * nameFirst: string, + * nameMiddle: string, + * nameLast: string, + * nameSuffix: string, + * issuer: string, + * tokenPassword: string, + * tokenId: string, + * preferences: string, + * createdDate: string, + * createdUserId: int, + * updatedDate: string, + * updatedUserId: int, + * } + */ +final class UserTypes +{ +} diff --git a/src/Domain/DataSource/User/UserTransport.php b/src/Domain/DataSource/User/UserTransport.php deleted file mode 100644 index 6652a21..0000000 --- a/src/Domain/DataSource/User/UserTransport.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * 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\DataSource\User; - -use Phalcon\Api\Domain\Exceptions\InvalidConfigurationArgumentException; - -/** - * @method int getId() - * @method int getStatus() - * @method string getUsername() - * @method string getPassword() - * - * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserTransport from UserTypes - */ -final class UserTransport -{ - /** @var TUserTransport */ - private array $store; - - /** - * @param TUserRecord $input - */ - public function __construct(array $input) - { - $this->store = [ - 'id' => (int)($input['usr_id'] ?? 0), - 'status' => (int)($input['usr_status_flag'] ?? 0), - 'username' => (string)($input['usr_username'] ?? ''), - 'password' => (string)($input['usr_password'] ?? ''), - ]; - } - - /** - * @param string $name - * @param array $arguments - * - * @return int|string - */ - public function __call(string $name, array $arguments): int | string - { - return match ($name) { - 'getId' => $this->store['id'], - 'getStatus' => $this->store['status'], - 'getUsername' => $this->store['username'], - 'getPassword' => $this->store['password'], - default => throw new InvalidConfigurationArgumentException( - 'The ' . $name . ' method is not supported. [' - . json_encode($arguments) . ']', - ), - }; - } - - public function isEmpty(): bool - { - return 0 === $this->store['id']; - } - - /** - * @return array - */ - public function toArray(): array - { - return [$this->store['id'] => $this->store]; - } -} diff --git a/src/Domain/DataSource/User/UserTypes.php b/src/Domain/DataSource/User/UserTypes.php deleted file mode 100644 index 711373a..0000000 --- a/src/Domain/DataSource/User/UserTypes.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * 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\DataSource\User; - -/** - * @phpstan-type TUserRecord array{}|array{ - * usr_id: int, - * usr_status_flag: int, - * usr_username: string, - * usr_password: string - * } - * - * @phpstan-type TUserTransport array{ - * id: int, - * status: int, - * username: string, - * password: string - * } - */ -final class UserTypes -{ -} From 72e8f62af054ab843698a6d34fc14a1f307691ec Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:26:10 -0500 Subject: [PATCH 088/301] [#.x] - added more tests --- .../Domain/Services/User/UserServiceTest.php | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 tests/Unit/Domain/Services/User/UserServiceTest.php diff --git a/tests/Unit/Domain/Services/User/UserServiceTest.php b/tests/Unit/Domain/Services/User/UserServiceTest.php new file mode 100644 index 0000000..b01b1ea --- /dev/null +++ b/tests/Unit/Domain/Services/User/UserServiceTest.php @@ -0,0 +1,209 @@ + + * + * 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\User; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Services\User\UserGetService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class UserServiceTest extends AbstractUnitTestCase +{ + public function testDispatch(): void + { + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'REQUEST_URI' => '/user', + ]; + + $_GET = [ + 'userId' => $userId, + ]; + + ob_start(); + require_once EnvManager::appPath('public/index.php'); + $response = ob_get_clean(); + + $contents = json_decode($response, true); + + restore_error_handler(); + + $this->assertArrayHasKey('data', $contents); + $this->assertArrayHasKey('errors', $contents); + + $data = $contents['data']; + $errors = $contents['errors']; + + $expected = []; + $actual = $errors; + $this->assertSame($expected, $actual); + + $user = $transport->newUser($dbUser); + $expected = $user->toArray(); + $actual = $data; + $this->assertSame($expected, $actual); + } + + public function testServiceEmptyUserId(): void + { + /** @var UserGetService $service */ + $service = $this->container->get(Container::USER_GET_SERVICE); + + $payload = $service->__invoke([]); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = 'Record(s) not found'; + $actual = $actual['errors'][0]; + $this->assertStringContainsString($expected, $actual); + } + + public function testServiceWithUserId(): void + { + /** @var UserGetService $service */ + $service = $this->container->get(Container::USER_GET_SERVICE); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + + $payload = $service->__invoke( + [ + 'userId' => $userId, + ] + ); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $user = $actual['data']; + $key = array_key_first($user); + $user = $user[$key]; + + $expected = $dbUser['usr_id']; + $actual = $user['id']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_status_flag']; + $actual = $user['status']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_email']; + $actual = $user['email']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_password']; + $actual = $user['password']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_prefix']; + $actual = $user['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_first']; + $actual = $user['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_middle']; + $actual = $user['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_last']; + $actual = $user['nameLast']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_suffix']; + $actual = $user['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_issuer']; + $actual = $user['issuer']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_token_password']; + $actual = $user['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_token_id']; + $actual = $user['tokenId']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_preferences']; + $actual = $user['preferences']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_date']; + $actual = $user['createdDate']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_usr_id']; + $actual = $user['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_date']; + $actual = $user['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_usr_id']; + $actual = $user['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testServiceWrongUserId(): void + { + /** @var UserGetService $service */ + $service = $this->container->get(Container::USER_GET_SERVICE); + + $payload = $service->__invoke( + [ + 'userId' => 999999, + ] + ); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = 'Record(s) not found'; + $actual = $actual['errors'][0]; + $this->assertStringContainsString($expected, $actual); + } +} From 33a346a2a5d2f3ad4ca15caf75978be6b96694d7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:26:19 -0500 Subject: [PATCH 089/301] [#.x] - removed obsolete file --- tests/Fixtures/Domain/Services/Env/.env | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 tests/Fixtures/Domain/Services/Env/.env diff --git a/tests/Fixtures/Domain/Services/Env/.env b/tests/Fixtures/Domain/Services/Env/.env deleted file mode 100644 index b707f0d..0000000 --- a/tests/Fixtures/Domain/Services/Env/.env +++ /dev/null @@ -1,4 +0,0 @@ -SAMPLE_STRING=sample_value -SAMPLE_INT=1 -SAMPLE_TRUE=true -SAMPLE_FALSE=false From d78255bc3f6c3d34ffc3cf84684325728c58095d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 08:26:33 -0500 Subject: [PATCH 090/301] [#.x] - namespace changes after refactoring --- public/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/index.php b/public/index.php index 5906089..9d75c6d 100644 --- a/public/index.php +++ b/public/index.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use Phalcon\Api\Domain\Services\Container; -use Phalcon\Api\Domain\Services\Providers\ErrorHandlerProvider; -use Phalcon\Api\Domain\Services\Providers\RouterProvider; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Providers\ErrorHandlerProvider; +use Phalcon\Api\Domain\Components\Providers\RouterProvider; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Mvc\Micro; From 6ef9b360cf24c4b6bb3d5af0d413152b0d1afcee Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 12:59:19 -0500 Subject: [PATCH 091/301] [#.x] - phpcs --- .../20250908190433_add_users_table.php | 10 +- ...7133633_add_more_fields_to_users_table.php | 121 +++++++++--------- resources/docker/config/php/php.ini | 24 ++-- src/Domain/Components/Container.php | 30 ++--- .../Components/Enums/Http/RoutesEnum.php | 3 +- src/Domain/Services/User/UserGetService.php | 4 - src/Responder/JsonResponder.php | 6 +- tests/AbstractUnitTestCase.php | 17 +-- .../Domain/Migrations/UsersMigration.php | 8 +- tests/Unit/Action/ActionHandlerTest.php | 6 +- .../DataSource/TransportRepositoryTest.php | 5 +- .../DataSource/User/UserRepositoryTest.php | 5 +- .../Components/Encryption/SecurityTest.php | 2 - .../Components/Env/Adapters/DotEnvTest.php | 12 +- .../Domain/Components/Env/EnvManagerTest.php | 14 +- .../Middleware/HealthMiddlewareTest.php | 1 - .../Providers/ErrorHandlerProviderTest.php | 6 +- .../Domain/Services/User/UserServiceTest.php | 9 +- tests/Unit/Responder/JsonResponderTest.php | 1 - 19 files changed, 132 insertions(+), 152 deletions(-) diff --git a/resources/db/migrations/20250908190433_add_users_table.php b/resources/db/migrations/20250908190433_add_users_table.php index 3d9c66d..5b20178 100644 --- a/resources/db/migrations/20250908190433_add_users_table.php +++ b/resources/db/migrations/20250908190433_add_users_table.php @@ -6,6 +6,11 @@ final class AddUsersTable extends AbstractMigration { + public function down(): void + { + $this->table('co_users')->drop()->save(); + } + public function up(): void { $table = $this->table( @@ -49,9 +54,4 @@ public function up(): void ->save() ; } - - public function down(): void - { - $this->table('co_users')->drop()->save(); - } } diff --git a/resources/db/migrations/20251017133633_add_more_fields_to_users_table.php b/resources/db/migrations/20251017133633_add_more_fields_to_users_table.php index bf8b7f0..adfb0c8 100644 --- a/resources/db/migrations/20251017133633_add_more_fields_to_users_table.php +++ b/resources/db/migrations/20251017133633_add_more_fields_to_users_table.php @@ -6,6 +6,52 @@ final class AddMoreFieldsToUsersTable extends AbstractMigration { + public function down(): void + { + $table = $this->table('co_users'); + $table + ->removeIndexByName('i_email_x_status') + ->removeIndexByName('i_id_x_status') + ->removeIndexByName('i_token_id') + ->removeIndexByName('i_email') + ->save() + ; + + $table + ->removeColumn('usr_email') + ->removeColumn('usr_name_prefix') + ->removeColumn('usr_name_first') + ->removeColumn('usr_name_middle') + ->removeColumn('usr_name_last') + ->removeColumn('usr_name_suffix') + ->removeColumn('usr_issuer') + ->removeColumn('usr_token_password') + ->removeColumn('usr_token_id') + ->removeColumn('usr_preferences') + ->removeColumn('usr_created_date') + ->removeColumn('usr_created_usr_id') + ->removeColumn('usr_updated_date') + ->removeColumn('usr_updated_usr_id') + ->save() + ; + + $table + ->addColumn( + 'usr_username', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + 'after' => 'usr_status_flag', + ] + ) + ->addIndex('usr_status_flag') + ->addIndex('usr_username') + ->save() + ; + } + public function up(): void { $table = $this->table('co_users'); @@ -24,8 +70,8 @@ public function up(): void 'string', [ 'limit' => 128, - 'null' => false, - 'after' => 'usr_status_flag' + 'null' => false, + 'after' => 'usr_status_flag', ] ) ->addColumn( @@ -77,31 +123,31 @@ public function up(): void 'usr_issuer', 'string', [ - 'limit' => 128, - 'null' => false, + 'limit' => 128, + 'null' => false, ] ) ->addColumn( 'usr_token_password', 'string', [ - 'limit' => 128, - 'null' => false, + 'limit' => 128, + 'null' => false, ] ) ->addColumn( 'usr_token_id', 'string', [ - 'limit' => 128, - 'null' => false, + 'limit' => 128, + 'null' => false, ] ) ->addColumn( 'usr_preferences', 'text', [ - 'null' => true + 'null' => true, ] ) ->addColumn( @@ -116,7 +162,7 @@ public function up(): void 'usr_created_usr_id', 'biginteger', [ - 'null' => false, + 'null' => false, 'default' => 0, ] ) @@ -132,7 +178,7 @@ public function up(): void 'usr_updated_usr_id', 'biginteger', [ - 'null' => false, + 'null' => false, 'default' => 0, ] ) @@ -142,7 +188,7 @@ public function up(): void 'usr_status_flag', ], [ - 'name' => 'i_email_x_status' + 'name' => 'i_email_x_status', ] ) ->addIndex( @@ -151,16 +197,16 @@ public function up(): void 'usr_status_flag', ], [ - 'name' => 'i_id_x_status' + 'name' => 'i_id_x_status', ] ) ->addIndex( [ - 'usr_token_id' + 'usr_token_id', ], [ 'unique' => true, - 'name' => 'i_token_id' + 'name' => 'i_token_id', ] ) ->addIndex( @@ -169,53 +215,10 @@ public function up(): void ], [ 'unique' => true, - 'name' => 'i_email' + 'name' => 'i_email', ] ) ->save() ; } - - public function down(): void - { - $table = $this->table('co_users'); - $table - ->removeIndexByName('i_email_x_status') - ->removeIndexByName('i_id_x_status') - ->removeIndexByName('i_token_id') - ->removeIndexByName('i_email') - ->save(); - - $table - ->removeColumn('usr_email') - ->removeColumn('usr_name_prefix') - ->removeColumn('usr_name_first') - ->removeColumn('usr_name_middle') - ->removeColumn('usr_name_last') - ->removeColumn('usr_name_suffix') - ->removeColumn('usr_issuer') - ->removeColumn('usr_token_password') - ->removeColumn('usr_token_id') - ->removeColumn('usr_preferences') - ->removeColumn('usr_created_date') - ->removeColumn('usr_created_usr_id') - ->removeColumn('usr_updated_date') - ->removeColumn('usr_updated_usr_id') - ->save(); - - $table - ->addColumn( - 'usr_username', - 'string', - [ - 'limit' => 128, - 'null' => false, - 'default' => '', - 'after' => 'usr_status_flag', - ] - ) - ->addIndex('usr_status_flag') - ->addIndex('usr_username') - ->save(); - } } diff --git a/resources/docker/config/php/php.ini b/resources/docker/config/php/php.ini index 3be8e2d..7ab0cf5 100644 --- a/resources/docker/config/php/php.ini +++ b/resources/docker/config/php/php.ini @@ -1,13 +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 +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/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index a1c9886..fea369d 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -15,7 +15,6 @@ use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; @@ -46,24 +45,12 @@ class Container extends Di public const EVENTS_MANAGER = 'eventsManager'; /** @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'; - /** @var string */ - public const SECURITY = Security::class; - /** @var string */ - public const TIME = 'time'; - /** * Services */ - public const HELLO_SERVICE = HelloService::class; - public const USER_GET_SERVICE = 'service.user.get'; + public const HELLO_SERVICE = HelloService::class; + /** @var string */ + public const LOGGER = 'logger'; /** * Middleware */ @@ -74,10 +61,21 @@ class Container extends Di */ public const REPOSITORY = 'repository'; public const REPOSITORY_TRANSPORT = TransportRepository::class; + /** @var string */ + public const REQUEST = 'request'; /** * Responders */ public const RESPONDER_JSON = JsonResponder::class; + /** @var string */ + public const RESPONSE = 'response'; + /** @var string */ + public const ROUTER = 'router'; + /** @var string */ + public const SECURITY = Security::class; + /** @var string */ + public const TIME = 'time'; + public const USER_GET_SERVICE = 'service.user.get'; public function __construct() { diff --git a/src/Domain/Components/Enums/Http/RoutesEnum.php b/src/Domain/Components/Enums/Http/RoutesEnum.php index d21fb0d..db20eb3 100644 --- a/src/Domain/Components/Enums/Http/RoutesEnum.php +++ b/src/Domain/Components/Enums/Http/RoutesEnum.php @@ -22,10 +22,9 @@ */ enum RoutesEnum: string { + public const DELETE = 'delete'; public const EVENT_BEFORE = 'before'; public const EVENT_FINISH = 'finish'; - - public const DELETE = 'delete'; public const GET = 'get'; public const POST = 'post'; public const PUT = 'put'; diff --git a/src/Domain/Services/User/UserGetService.php b/src/Domain/Services/User/UserGetService.php index 21f2e02..cb79dfa 100644 --- a/src/Domain/Services/User/UserGetService.php +++ b/src/Domain/Services/User/UserGetService.php @@ -18,13 +18,9 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Domain\Payload; - use Phalcon\Filter\Filter; -use function abs; - /** * @phpstan-import-type TUserInput from InputTypes */ diff --git a/src/Responder/JsonResponder.php b/src/Responder/JsonResponder.php index 7d7a7d2..36ea2ff 100644 --- a/src/Responder/JsonResponder.php +++ b/src/Responder/JsonResponder.php @@ -31,7 +31,7 @@ final class JsonResponder implements ResponderInterface { /** * @param ResponseInterface $response - * @param Payload $payload + * @param Payload $payload * * @return ResponseInterface * @throws BaseException @@ -52,7 +52,7 @@ public function __invoke( /** @var TData $data */ $data = $result['data'] ?? []; /** @var TErrors $errors */ - $errors = $result['errors'] ?? []; + $errors = $result['errors'] ?? []; $statusMessage = true === empty($errors) ? 'success' : 'error'; $content = [ @@ -101,6 +101,4 @@ private function calculateMeta(array $content): array return [$content, $eTag]; } - - } diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 617c9f1..32cd9a0 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -22,8 +22,6 @@ use Phalcon\DataMapper\Pdo\Connection; use PHPUnit\Framework\TestCase; -use function uniqid; - abstract class AbstractUnitTestCase extends TestCase { /** @@ -36,13 +34,6 @@ abstract class AbstractUnitTestCase extends TestCase */ protected Container $container; - public function setUp(): void - { - parent::setUp(); - - $this->container = new Container(); - } - /** * @param string $fileName * @param string $stream @@ -81,7 +72,6 @@ public function getConnection(): Connection return $this->connection; } - /** * @param UsersMigration $migration * @param array $fields @@ -161,6 +151,13 @@ public function getStrongPassword(): string return substr(base64_encode(random_bytes(512)), 0, 128); } + public function setUp(): void + { + parent::setUp(); + + $this->container = new Container(); + } + /** * @param string $table * @param array $criteria diff --git a/tests/Fixtures/Domain/Migrations/UsersMigration.php b/tests/Fixtures/Domain/Migrations/UsersMigration.php index 85d59ff..8d7cb31 100644 --- a/tests/Fixtures/Domain/Migrations/UsersMigration.php +++ b/tests/Fixtures/Domain/Migrations/UsersMigration.php @@ -38,7 +38,7 @@ public function insert( string $updatedDate, int $updatedUserId ): int { - $sql = "INSERT INTO $this->table ( + $sql = "INSERT INTO $this->table ( usr_id, usr_status_flag, usr_email, @@ -76,8 +76,8 @@ public function insert( :updatedUserId )"; - $stmt = $this->connection->prepare($sql); - $params = [ + $stmt = $this->connection->prepare($sql); + $params = [ ':id' => $id, ':status' => $status, ':email' => $email, @@ -96,7 +96,7 @@ public function insert( ':updatedDate' => $updatedDate, ':updatedUserId' => $updatedUserId, ]; - $result = $stmt->execute($params); + $result = $stmt->execute($params); if (!$result) { Assert::fail( "Failed to insert id [#$id] into table [$this->table]" diff --git a/tests/Unit/Action/ActionHandlerTest.php b/tests/Unit/Action/ActionHandlerTest.php index 14b257d..7ba4ae9 100644 --- a/tests/Unit/Action/ActionHandlerTest.php +++ b/tests/Unit/Action/ActionHandlerTest.php @@ -32,9 +32,9 @@ final class ActionHandlerTest extends AbstractUnitTestCase public function testInvoke(): void { /** @var RequestInterface $request */ - $request = $this->container->getShared(Container::REQUEST); + $request = $this->container->getShared(Container::REQUEST); /** @var ResponseInterface $response */ - $response = $this->container->getShared(Container::RESPONSE); + $response = $this->container->getShared(Container::RESPONSE); /** @var ResponderInterface $responder */ $responder = $this->container->getShared(Container::RESPONDER_JSON); @@ -42,7 +42,7 @@ public function testInvoke(): void 'key' => uniqid('key-'), 'data' => [ uniqid('data-'), - ] + ], ]; $_GET = $getData; diff --git a/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php b/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php index dbf9083..90b6120 100644 --- a/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php +++ b/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php @@ -14,10 +14,8 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\TransportRepository; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; final class TransportRepositoryTest extends AbstractUnitTestCase { @@ -31,7 +29,7 @@ public function testNewUser(): void $this->assertTrue($actual); $dbUser = $this->getNewUserData(); - $user = $transport->newUser($dbUser); + $user = $transport->newUser($dbUser); $fullName = trim( $dbUser['usr_name_last'] @@ -137,6 +135,5 @@ public function testNewUser(): void $expected = $fullName; $actual = $user->getFullName(); $this->assertSame($expected, $actual); - } } diff --git a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php b/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php index 6e0f09f..1443a5e 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php @@ -15,11 +15,8 @@ use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; -use Phalcon\DataMapper\Pdo\Connection; final class UserRepositoryTest extends AbstractUnitTestCase { @@ -52,7 +49,7 @@ public function testFindById(): void $this->assertEmpty($repositoryUser); $migrationUser = $this->getNewUser($migration); - $userId = $migrationUser['usr_id']; + $userId = $migrationUser['usr_id']; $repositoryUser = $repository->user()->findById($userId); diff --git a/tests/Unit/Domain/Components/Encryption/SecurityTest.php b/tests/Unit/Domain/Components/Encryption/SecurityTest.php index d0350c5..be1d7df 100644 --- a/tests/Unit/Domain/Components/Encryption/SecurityTest.php +++ b/tests/Unit/Domain/Components/Encryption/SecurityTest.php @@ -14,10 +14,8 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Encryption; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; final class SecurityTest extends AbstractUnitTestCase { diff --git a/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php index 60099b1..b12044a 100644 --- a/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php +++ b/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php @@ -22,6 +22,12 @@ final class DotEnvTest extends AbstractUnitTestCase { private string $envFile; + public function setUp(): void + { + $this->envFile = EnvManager::appPath() + . '/tests/Fixtures/Domain/Components/Env/'; + } + public function testLoadExceptionForEmptyFilePath(): void { $this->expectException(InvalidConfigurationArgumentException::class); @@ -81,10 +87,4 @@ public function testLoadSuccess(): void $this->assertSame($expected, $actualArray); } - - public function setUp(): void - { - $this->envFile = EnvManager::appPath() - . '/tests/Fixtures/Domain/Components/Env/'; - } } diff --git a/tests/Unit/Domain/Components/Env/EnvManagerTest.php b/tests/Unit/Domain/Components/Env/EnvManagerTest.php index 3aa7864..0d13280 100644 --- a/tests/Unit/Domain/Components/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Components/Env/EnvManagerTest.php @@ -21,6 +21,13 @@ #[BackupGlobals(true)] final class EnvManagerTest extends AbstractUnitTestCase { + public function setUp(): void + { + $ref = new ReflectionClass(EnvManager::class); + $ref->setStaticPropertyValue('isLoaded', false); + $ref->setStaticPropertyValue('settings', []); + } + public function testAppEnvReturnsDefault(): void { $expected = 'development'; @@ -111,11 +118,4 @@ public function testGetFromDotEnvLoad(): void $actual = EnvManager::get('SAMPLE_FALSE'); $this->assertSame($expected, $actual); } - - public function setUp(): void - { - $ref = new ReflectionClass(EnvManager::class); - $ref->setStaticPropertyValue('isLoaded', false); - $ref->setStaticPropertyValue('settings', []); - } } diff --git a/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php index 2fb0cfb..153a256 100644 --- a/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php @@ -13,7 +13,6 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; -use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; diff --git a/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php index 71c0118..b3f2343 100644 --- a/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php +++ b/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php @@ -33,7 +33,7 @@ final class ErrorHandlerProviderTest extends AbstractUnitTestCase { public function testCheckRegistration(): void { - $now = hrtime(true); + $now = hrtime(true); $this->container->set( Container::TIME, function () use ($now) { @@ -62,7 +62,7 @@ function () use ($now) { public function testRegisterSetsHandlersAndLogs(): void { - $now = hrtime(true); + $now = hrtime(true); $this->container->set( Container::TIME, function () use ($now) { @@ -100,7 +100,7 @@ public function testRegisterShutdown(): void $_ENV['APP_ENV'] = 'development'; $_ENV['APP_LOG_LEVEL'] = 2; - $now = hrtime(true); + $now = hrtime(true); $this->container->set( Container::TIME, function () use ($now) { diff --git a/tests/Unit/Domain/Services/User/UserServiceTest.php b/tests/Unit/Domain/Services/User/UserServiceTest.php index b01b1ea..513f82d 100644 --- a/tests/Unit/Domain/Services/User/UserServiceTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceTest.php @@ -16,7 +16,6 @@ use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Domain\Services\User\UserGetService; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -29,7 +28,7 @@ final class UserServiceTest extends AbstractUnitTestCase public function testDispatch(): void { /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); @@ -64,7 +63,7 @@ public function testDispatch(): void $actual = $errors; $this->assertSame($expected, $actual); - $user = $transport->newUser($dbUser); + $user = $transport->newUser($dbUser); $expected = $user->toArray(); $actual = $data; $this->assertSame($expected, $actual); @@ -95,8 +94,8 @@ public function testServiceWithUserId(): void $service = $this->container->get(Container::USER_GET_SERVICE); $migration = new UsersMigration($this->getConnection()); - $dbUser = $this->getNewUser($migration); - $userId = $dbUser['usr_id']; + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; $payload = $service->__invoke( [ diff --git a/tests/Unit/Responder/JsonResponderTest.php b/tests/Unit/Responder/JsonResponderTest.php index fd052bf..a31b6b8 100644 --- a/tests/Unit/Responder/JsonResponderTest.php +++ b/tests/Unit/Responder/JsonResponderTest.php @@ -180,5 +180,4 @@ public function testSuccess(): void $actual = $headers['Content-Type']; $this->assertSame($expected, $actual); } - } From 18eb6b13535bf3edff9e9539eb5827d084b4fb1a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 13:00:56 -0500 Subject: [PATCH 092/301] [#.x] - changed class to use the interface for responder --- src/Domain/Components/Middleware/AbstractMiddleware.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Domain/Components/Middleware/AbstractMiddleware.php b/src/Domain/Components/Middleware/AbstractMiddleware.php index a22463e..edf464b 100644 --- a/src/Domain/Components/Middleware/AbstractMiddleware.php +++ b/src/Domain/Components/Middleware/AbstractMiddleware.php @@ -15,7 +15,7 @@ use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Responder\JsonResponder; +use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Responder\ResponderTypes; use Phalcon\Domain\Payload; use Phalcon\Events\Exception as EventsException; @@ -50,7 +50,7 @@ protected function halt( ): void { /** @var ResponseInterface $response */ $response = $application->getSharedService(Container::RESPONSE); - /** @var JsonResponder $responder */ + /** @var ResponderInterface $responder */ $responder = $application->getService(Container::RESPONDER_JSON); $application->stop(); From 50fd7c2089bcb50bf7be44b950d5ff01b0894ff4 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 18 Oct 2025 13:03:12 -0500 Subject: [PATCH 093/301] [#.x] - added .env file for tests --- tests/Fixtures/Domain/Components/Env/.env | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/Fixtures/Domain/Components/Env/.env diff --git a/tests/Fixtures/Domain/Components/Env/.env b/tests/Fixtures/Domain/Components/Env/.env new file mode 100644 index 0000000..b707f0d --- /dev/null +++ b/tests/Fixtures/Domain/Components/Env/.env @@ -0,0 +1,4 @@ +SAMPLE_STRING=sample_value +SAMPLE_INT=1 +SAMPLE_TRUE=true +SAMPLE_FALSE=false From a28329917eba342a9edb34b84ddff1d1143c8a7b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:52:33 -0500 Subject: [PATCH 094/301] [#.x] - removed helloService --- src/Domain/Services/Hello/HelloService.php | 45 ----------- .../Services/Hello/HelloServiceTest.php | 81 ------------------- 2 files changed, 126 deletions(-) delete mode 100644 src/Domain/Services/Hello/HelloService.php delete mode 100644 tests/Unit/Domain/Services/Hello/HelloServiceTest.php diff --git a/src/Domain/Services/Hello/HelloService.php b/src/Domain/Services/Hello/HelloService.php deleted file mode 100644 index 4e8565b..0000000 --- a/src/Domain/Services/Hello/HelloService.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * 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\Hello; - -use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Domain\Payload; - -use function date; - -/** - * @phpstan-import-type THelloInput from InputTypes - */ -final class HelloService implements DomainInterface -{ - /** - * @param THelloInput $input - * - * @return Payload - */ - public function __invoke(array $input): Payload - { - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => [ - "Hello World!!! - " . date(Dates::DATE_TIME_FORMAT), - ], - ] - ); - } -} diff --git a/tests/Unit/Domain/Services/Hello/HelloServiceTest.php b/tests/Unit/Domain/Services/Hello/HelloServiceTest.php deleted file mode 100644 index 0ea190b..0000000 --- a/tests/Unit/Domain/Services/Hello/HelloServiceTest.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * 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\Hello; - -use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Services\Hello\HelloService; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use PHPUnit\Framework\Attributes\BackupGlobals; - -use function ob_get_clean; -use function ob_start; -use function restore_error_handler; -use function time; - -#[BackupGlobals(true)] -final class HelloServiceTest extends AbstractUnitTestCase -{ - public function testDispatch(): void - { - $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); - $_SERVER = [ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_TIME_FLOAT' => $time, - 'REQUEST_URI' => '/', - ]; - - ob_start(); - require_once EnvManager::appPath('public/index.php'); - $response = ob_get_clean(); - - $contents = json_decode($response, true); - - restore_error_handler(); - - $this->assertArrayHasKey('data', $contents); - $this->assertArrayHasKey('errors', $contents); - - $data = $contents['data']; - $errors = $contents['errors']; - - $expected = []; - $actual = $errors; - $this->assertSame($expected, $actual); - - $expected = 'Hello World!!! - '; - $actual = $data[0]; - $this->assertStringContainsString($expected, $actual); - } - - public function testService(): void - { - /** @var HelloService $service */ - $service = $this->container->get(Container::HELLO_SERVICE); - - $payload = $service->__invoke([]); - - $expected = DomainStatus::SUCCESS; - $actual = $payload->getStatus(); - $this->assertSame($expected, $actual); - - $actual = $payload->getResult(); - $this->assertArrayHasKey('data', $actual); - - $expected = 'Hello World!!! - '; - $actual = $actual['data'][0]; - $this->assertStringContainsString($expected, $actual); - } -} From 0b5c2e38afdd32da1cb20ec38311b97d3920fcb1 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:52:49 -0500 Subject: [PATCH 095/301] [#.x] - removed processIsolation --- phpunit.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 174c263..f3f7469 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,7 +4,6 @@ bootstrap="./phpunit.php" cacheDirectory="tests/_output/.phpunit.result.cache" colors="true" - processIsolation="true" > From 1c997cbc93fdf093e5af0ce30b2a527c994825e4 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:53:03 -0500 Subject: [PATCH 096/301] [#.x] - added new Http Codes --- .../Components/Enums/Http/HttpCodesEnum.php | 65 ++++++++++--- .../Enums/Http/HttpCodesEnumTest.php | 92 ++++++++++++++++++- 2 files changed, 140 insertions(+), 17 deletions(-) diff --git a/src/Domain/Components/Enums/Http/HttpCodesEnum.php b/src/Domain/Components/Enums/Http/HttpCodesEnum.php index 3ccbe32..cdb07e7 100644 --- a/src/Domain/Components/Enums/Http/HttpCodesEnum.php +++ b/src/Domain/Components/Enums/Http/HttpCodesEnum.php @@ -32,16 +32,34 @@ enum HttpCodesEnum: int implements EnumsInterface /** * Application specific codes */ - case AppMalformedPayload = 3400; - case AppRecordsNotFound = 3401; - case AppResourceNotFound = 3402; - case AppUnauthorized = 3403; - case AppInvalidArguments = 3409; + case AppMalformedPayload = 3400; + case AppRecordsNotFound = 3401; + case AppResourceNotFound = 3402; + case AppUnauthorized = 3403; + case AppCannotConnectToDatabase = 3404; + case AppCannotCreateDatabaseRecord = 3405; + case AppCannotDeleteDatabaseRecord = 3406; + case AppCannotUpdateDatabaseRecord = 3407; + case AppIncorrectCredentials = 3408; + case AppInvalidArguments = 3409; + case AppTokenInvalidAudience = 3410; + case AppTokenInvalidCannotDecodeContent = 3411; + case AppTokenInvalidIdentifier = 3412; + case AppTokenInvalidIssuer = 3413; + case AppTokenInvalidStructure = 3414; + case AppTokenInvalidTenant = 3415; + case AppTokenInvalidUser = 3416; + case AppTokenNotPresent = 3417; + case AppTokenNotValid = 3418; + case AppTokenSignatureNotValid = 3419; + case AppTokenUnsupportedHeaderFound = 3420; + case AppValidationFailed = 3421; + /** * Returns an array with the value as key and text as value * - * @return array + * @return array */ public function error(): array { @@ -56,15 +74,32 @@ public function error(): array public function text(): string { return match ($this) { - self::OK => 'OK', - self::BadRequest => 'Bad Request', - self::NotFound => 'Not Found', - self::AppMalformedPayload => 'Malformed payload', - self::AppRecordsNotFound => 'Record(s) not found', - self::AppResourceNotFound => 'Resource not found', - self::Unauthorized, - self::AppUnauthorized => 'Unauthorized', - self::AppInvalidArguments => 'Invalid arguments provided', + HttpCodesEnum::OK => 'OK', + HttpCodesEnum::BadRequest => 'Bad Request', + HttpCodesEnum::NotFound => 'Not Found', + HttpCodesEnum::AppMalformedPayload => 'Malformed payload', + HttpCodesEnum::AppRecordsNotFound => 'Record(s) not found', + HttpCodesEnum::AppResourceNotFound => 'Resource not found', + HttpCodesEnum::Unauthorized, + HttpCodesEnum::AppUnauthorized => 'Unauthorized', + HttpCodesEnum::AppCannotConnectToDatabase => 'Failure connecting to the database', + HttpCodesEnum::AppCannotCreateDatabaseRecord => 'Cannot create database record: ', + HttpCodesEnum::AppCannotDeleteDatabaseRecord => 'Cannot delete database record: ', + HttpCodesEnum::AppCannotUpdateDatabaseRecord => 'Cannot update database record: ', + HttpCodesEnum::AppIncorrectCredentials => 'Incorrect credentials', + HttpCodesEnum::AppInvalidArguments => 'Invalid arguments provided', + HttpCodesEnum::AppTokenInvalidAudience => 'Invalid Token [Audience]', + HttpCodesEnum::AppTokenInvalidCannotDecodeContent => 'Invalid Token [Cannot decode content]', + HttpCodesEnum::AppTokenInvalidIdentifier => 'Invalid Token [Identifier]', + HttpCodesEnum::AppTokenInvalidIssuer => 'Invalid Token [Issuer]', + HttpCodesEnum::AppTokenInvalidStructure => 'Invalid Token [Token structure]', + HttpCodesEnum::AppTokenInvalidTenant => 'Invalid Token [Tenant]', + HttpCodesEnum::AppTokenInvalidUser => 'Invalid Token [User]', + HttpCodesEnum::AppTokenNotPresent => 'Invalid Token [Token not present]', + HttpCodesEnum::AppTokenNotValid => 'Invalid Token [verification - Token not valid]', + HttpCodesEnum::AppTokenSignatureNotValid => 'Invalid Token [verification - Signature not valid]', + HttpCodesEnum::AppTokenUnsupportedHeaderFound => 'Invalid Token [Unsupported header]', + HttpCodesEnum::AppValidationFailed => 'Validation error [%s]', }; } } diff --git a/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php b/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php index 51b342d..88f0e0b 100644 --- a/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php +++ b/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php @@ -19,6 +19,9 @@ final class HttpCodesEnumTest extends AbstractUnitTestCase { + /** + * @return array[] + */ public static function getExamples(): array { return [ @@ -62,17 +65,102 @@ public static function getExamples(): array 3403, 'Unauthorized', ], + [ + HttpCodesEnum::AppCannotConnectToDatabase, + 3404, + 'Failure connecting to the database', + ], + [ + HttpCodesEnum::AppCannotCreateDatabaseRecord, + 3405, + 'Cannot create database record: ', + ], + [ + HttpCodesEnum::AppCannotDeleteDatabaseRecord, + 3406, + 'Cannot delete database record: ', + ], + [ + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + 3407, + 'Cannot update database record: ', + ], + [ + HttpCodesEnum::AppIncorrectCredentials, + 3408, + 'Incorrect credentials', + ], [ HttpCodesEnum::AppInvalidArguments, 3409, 'Invalid arguments provided', ], + [ + HttpCodesEnum::AppTokenInvalidAudience, + 3410, + 'Invalid Token [Audience]', + ], + [ + HttpCodesEnum::AppTokenInvalidCannotDecodeContent, + 3411, + 'Invalid Token [Cannot decode content]', + ], + [ + HttpCodesEnum::AppTokenInvalidIdentifier, + 3412, + 'Invalid Token [Identifier]', + ], + [ + HttpCodesEnum::AppTokenInvalidIssuer, + 3413, + 'Invalid Token [Issuer]', + ], + [ + HttpCodesEnum::AppTokenInvalidStructure, + 3414, + 'Invalid Token [Token structure]', + ], + [ + HttpCodesEnum::AppTokenInvalidTenant, + 3415, + 'Invalid Token [Tenant]', + ], + [ + HttpCodesEnum::AppTokenInvalidUser, + 3416, + 'Invalid Token [User]', + ], + [ + HttpCodesEnum::AppTokenNotPresent, + 3417, + 'Invalid Token [Token not present]', + ], + [ + HttpCodesEnum::AppTokenNotValid, + 3418, + 'Invalid Token [verification - Token not valid]', + ], + [ + HttpCodesEnum::AppTokenSignatureNotValid, + 3419, + 'Invalid Token [verification - Signature not valid]', + ], + [ + HttpCodesEnum::AppTokenUnsupportedHeaderFound, + 3420, + 'Invalid Token [Unsupported header]', + ], + [ + HttpCodesEnum::AppValidationFailed, + 3421, + 'Validation error [%s]', + ], ]; } public function testCheckCount(): void { - $expected = 9; + $expected = 26; $actual = HttpCodesEnum::cases(); $this->assertCount($expected, $actual); } @@ -82,7 +170,7 @@ public function testCheckItems( HttpCodesEnum $element, int $value, string $text - ) { + ): void { $expected = $value; $actual = $element->value; $this->assertSame($expected, $actual); From a8b21968cccac424bfbf3516e5d72697df18506a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:53:21 -0500 Subject: [PATCH 097/301] [#.x] - refactored EnvManager --- src/Domain/Components/Env/Adapters/DotEnv.php | 2 +- src/Domain/Components/Env/EnvManager.php | 107 +++++++----------- .../Components/Env/Adapters/DotEnvTest.php | 8 +- .../Domain/Components/Env/EnvManagerTest.php | 61 ++++++---- 4 files changed, 91 insertions(+), 87 deletions(-) diff --git a/src/Domain/Components/Env/Adapters/DotEnv.php b/src/Domain/Components/Env/Adapters/DotEnv.php index 599239c..4793353 100644 --- a/src/Domain/Components/Env/Adapters/DotEnv.php +++ b/src/Domain/Components/Env/Adapters/DotEnv.php @@ -41,7 +41,7 @@ public function load(array $options): array ); } - $dotenv = ParentDotEnv::createImmutable($filePath); + $dotenv = ParentDotEnv::createMutable($filePath); $dotenv->load(); /** @var TSettings $env */ diff --git a/src/Domain/Components/Env/EnvManager.php b/src/Domain/Components/Env/EnvManager.php index a39aa8a..086a40a 100644 --- a/src/Domain/Components/Env/EnvManager.php +++ b/src/Domain/Components/Env/EnvManager.php @@ -14,6 +14,7 @@ namespace Phalcon\Api\Domain\Components\Env; use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Support\Collection; use function array_merge; use function getenv; @@ -21,24 +22,22 @@ /** * @phpstan-import-type TSettings from EnvManagerTypes */ -class EnvManager +class EnvManager extends Collection { - private static bool $isLoaded = false; + public function __construct() + { + parent::__construct(); - /** - * @var TSettings - */ - private static array $settings = []; + $this->load(); + } /** * @return string */ - public static function appEnv(): string + public function appEnv(): string { - self::load(); - /** @var string $appEnv */ - $appEnv = self::get('APP_ENV', 'development'); + $appEnv = $this->get('APP_ENV', 'development'); return (string)$appEnv; } @@ -46,14 +45,12 @@ public static function appEnv(): string /** * @return int */ - public static function appLogLevel(): int + public function appLogLevel(): int { - self::load(); + /** @var int $logLevel */ + $logLevel = $this->get('APP_LOG_LEVEL', 1, 'int'); - /** @var int|string $logLevel */ - $logLevel = self::get('APP_LOG_LEVEL', 1); - - return (int)$logLevel; + return $logLevel; } /** @@ -61,7 +58,7 @@ public static function appLogLevel(): int * * @return string */ - public static function appPath(string $path = ''): string + public function appPath(string $path = ''): string { return dirname(__DIR__, 4) . ($path ? DIRECTORY_SEPARATOR . $path : $path); @@ -70,76 +67,58 @@ public static function appPath(string $path = ''): string /** * @return string */ - public static function appTimezone(): string + public function appTimezone(): string { - self::load(); - /** @var string $timezone */ - $timezone = self::get('APP_TIMEZONE', Dates::DATE_TIME_ZONE); + $timezone = $this->get('APP_TIMEZONE', Dates::DATE_TIME_ZONE); return (string)$timezone; } /** - * @param string $key - * @param bool|int|string|null $defaultValue - * - * @return bool|int|string|null + * @return void */ - public static function get( - string $key, - bool | int | string | null $defaultValue = null - ): bool | int | string | null { - self::load(); + public function load(): void + { + $envFactory = new EnvFactory(); + $options = self::getOptions(); + $adapter = $options['adapter']; - return self::$settings[$key] ?? $defaultValue; + $envs = array_merge(getenv(), $_ENV); + /** @var TSettings $options */ + $options = $envFactory->newInstance($adapter)->load($options); + /** @var TSettings $envs */ + $envs = array_merge($envs, $options); + + $settings = array_map( + function ($value) { + return match ($value) { + 'true' => true, + 'false' => false, + default => $value, + }; + }, + $envs + ); + + $this->clear(); + $this->init($settings); } /** * @return array */ - private static function getOptions(): array + private function getOptions(): array { $envs = array_merge(getenv(), $_ENV); /** @var string $adapter */ $adapter = $envs['APP_ENV_ADAPTER'] ?? 'dotenv'; /** @var string $filePath */ - $filePath = $envs['APP_ENV_FILE_PATH'] ?? self::appPath(); + $filePath = $envs['APP_ENV_FILE_PATH'] ?? $this->appPath(); return [ 'adapter' => $adapter, 'filePath' => $filePath, ]; } - - /** - * @return void - */ - 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); - /** @var TSettings $options */ - $options = $envFactory->newInstance($adapter)->load($options); - /** @var TSettings $envs */ - $envs = array_merge($envs, $options); - - self::$settings = array_map( - function ($value) { - return match ($value) { - 'true' => true, - 'false' => false, - default => $value, - }; - }, - $envs - ); - } - } } diff --git a/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php index b12044a..5d40f00 100644 --- a/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php +++ b/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php @@ -13,6 +13,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Env\Adapters; +use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; @@ -24,7 +25,12 @@ final class DotEnvTest extends AbstractUnitTestCase public function setUp(): void { - $this->envFile = EnvManager::appPath() + parent::setUp(); + + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + + $this->envFile = $env->appPath() . '/tests/Fixtures/Domain/Components/Env/'; } diff --git a/tests/Unit/Domain/Components/Env/EnvManagerTest.php b/tests/Unit/Domain/Components/Env/EnvManagerTest.php index 0d13280..1e8f32c 100644 --- a/tests/Unit/Domain/Components/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Components/Env/EnvManagerTest.php @@ -13,25 +13,21 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Env; +use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; -use ReflectionClass; #[BackupGlobals(true)] final class EnvManagerTest extends AbstractUnitTestCase { - public function setUp(): void - { - $ref = new ReflectionClass(EnvManager::class); - $ref->setStaticPropertyValue('isLoaded', false); - $ref->setStaticPropertyValue('settings', []); - } - public function testAppEnvReturnsDefault(): void { + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $expected = 'development'; - $actual = EnvManager::appEnv(); + $actual = $env->appEnv(); $this->assertSame($expected, $actual); } @@ -39,15 +35,21 @@ public function testAppEnvReturnsValue(): void { $_ENV = ['APP_ENV' => 'production']; + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $expected = 'production'; - $actual = EnvManager::appEnv(); + $actual = $env->appEnv(); $this->assertSame($expected, $actual); } public function testAppLogLevelReturnsDefault(): void { + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $expected = 1; - $actual = EnvManager::appLogLevel(); + $actual = $env->appLogLevel(); $this->assertSame($expected, $actual); } @@ -55,22 +57,31 @@ public function testAppLogLevelReturnsValue(): void { $_ENV = ['APP_LOG_LEVEL' => 5]; + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $expected = 5; - $actual = EnvManager::appLogLevel(); + $actual = $env->appLogLevel(); $this->assertSame($expected, $actual); } public function testAppPathReturnsRoot(): void { + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $expected = dirname(__DIR__, 5); - $actual = EnvManager::appPath(); + $actual = $env->appPath(); $this->assertSame($expected, $actual); } public function testAppTimezoneReturnsDefault(): void { + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $expected = 'UTC'; - $actual = EnvManager::appTimezone(); + $actual = $env->appTimezone(); $this->assertSame($expected, $actual); } @@ -78,19 +89,27 @@ public function testAppTimezoneReturnsValue(): void { $_ENV = ['APP_TIMEZONE' => 'America/Los_Angeles']; + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $expected = 'America/Los_Angeles'; - $actual = EnvManager::appTimezone(); + $actual = $env->appTimezone(); $this->assertSame($expected, $actual); } public function testGetFromDotEnvLoad(): void { + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $_ENV = [ 'APP_ENV_ADAPTER' => 'dotenv', - 'APP_ENV_FILE_PATH' => EnvManager::appPath() + 'APP_ENV_FILE_PATH' => $env->appPath() . '/tests/Fixtures/Domain/Components/Env/', ]; + $env->load(); + $values = [ 'SAMPLE_STRING' => 'sample_value', 'SAMPLE_INT' => '1', @@ -99,23 +118,23 @@ public function testGetFromDotEnvLoad(): void ]; $expected = 'default_value'; - $actual = EnvManager::get('NON_EXISTENT', 'default_value'); + $actual = $env->get('NON_EXISTENT', 'default_value'); $this->assertSame($expected, $actual); $expected = $values['SAMPLE_STRING']; - $actual = EnvManager::get('SAMPLE_STRING'); + $actual = $env->get('SAMPLE_STRING'); $this->assertSame($expected, $actual); $expected = $values['SAMPLE_INT']; - $actual = EnvManager::get('SAMPLE_INT'); + $actual = $env->get('SAMPLE_INT'); $this->assertSame($expected, $actual); $expected = $values['SAMPLE_TRUE']; - $actual = EnvManager::get('SAMPLE_TRUE'); + $actual = $env->get('SAMPLE_TRUE'); $this->assertSame($expected, $actual); $expected = $values['SAMPLE_FALSE']; - $actual = EnvManager::get('SAMPLE_FALSE'); + $actual = $env->get('SAMPLE_FALSE'); $this->assertSame($expected, $actual); } } From 35421cfc3f1a9ac169541ca6d6728cc9b4dc20e5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:53:45 -0500 Subject: [PATCH 098/301] [#.x] - added Cache class --- src/Domain/Components/Cache/Cache.php | 92 +++++++++++++++++++ .../Providers/CacheDataProviderTest.php | 28 ++++++ 2 files changed, 120 insertions(+) create mode 100644 src/Domain/Components/Cache/Cache.php create mode 100644 tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php diff --git a/src/Domain/Components/Cache/Cache.php b/src/Domain/Components/Cache/Cache.php new file mode 100644 index 0000000..6a2f963 --- /dev/null +++ b/src/Domain/Components/Cache/Cache.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\Domain\Components\Cache; + +use DateTimeImmutable; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Cache\Cache as PhalconCache; +use Psr\SimpleCache\InvalidArgumentException; + +use function sha1; + +class Cache extends PhalconCache +{ + /** @var int */ + public const CACHE_LIFETIME_DAY = 86400; + /** @var int */ + public const CACHE_LIFETIME_HOUR = 3600; + /** + * Cache Timeouts + */ + /** @var int */ + public const CACHE_LIFETIME_MINUTE = 60; + /** @var int */ + public const CACHE_LIFETIME_MONTH = 2592000; + /** + * Default token expiry - 4 hours + */ + /** @var int */ + public const CACHE_TOKEN_EXPIRY = 14400; + /** + * Cache masks + */ + /** @var string */ + private const MASK_TOKEN_USER = 'tk-%s-%s'; + + /** + * @param UserTransport $domainUser + * @param string $token + * + * @return string + */ + public function getCacheTokenKey(UserTransport $domainUser, string $token): string + { + return sprintf( + self::MASK_TOKEN_USER, + $domainUser->getId(), + sha1($token) + ); + } + + /** + * @param EnvManager $env + * @param UserTransport $domainUser + * @param string $token + * + * @return bool + * @throws InvalidArgumentException + */ + public function storeTokenInCache( + EnvManager $env, + UserTransport $domainUser, + string $token + ): bool { + $cacheKey = $this->getCacheTokenKey($domainUser, $token); + /** @var int $expiration */ + $expiration = $env->get('TOKEN_EXPIRATION', self::CACHE_TOKEN_EXPIRY, 'int'); + $expirationDate = (new DateTimeImmutable()) + ->modify('+' . $expiration . ' seconds') + ->format(Dates::DATE_TIME_FORMAT) + ; + + $payload = [ + 'token' => $token, + 'expiry' => $expirationDate, + ]; + + return $this->set($cacheKey, $payload, $expiration); + } +} diff --git a/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php b/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php new file mode 100644 index 0000000..041baea --- /dev/null +++ b/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php @@ -0,0 +1,28 @@ + + * + * 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\Components\Providers; + +use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class CacheDataProviderTest extends AbstractUnitTestCase +{ + public function testCheckRegistration(): void + { + $expected = Cache::class; + $actual = $this->container->get(Container::CACHE); + $this->assertInstanceOf($expected, $actual); + } +} From 04619f34702d4a4e5bad047a89be3de15e4ac33f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:54:05 -0500 Subject: [PATCH 099/301] [#.x] - added exception trait and new exception --- .../Components/Exceptions/ExceptionTrait.php | 28 +++++++++++++++++++ .../InvalidConfigurationArgumentException.php | 1 + .../Exceptions/TokenValidationException.php | 21 ++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/Domain/Components/Exceptions/ExceptionTrait.php create mode 100644 src/Domain/Components/Exceptions/TokenValidationException.php diff --git a/src/Domain/Components/Exceptions/ExceptionTrait.php b/src/Domain/Components/Exceptions/ExceptionTrait.php new file mode 100644 index 0000000..1323f88 --- /dev/null +++ b/src/Domain/Components/Exceptions/ExceptionTrait.php @@ -0,0 +1,28 @@ + + * + * 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\Components\Exceptions; + +trait ExceptionTrait +{ + /** + * @param string $message + * @param int $code + * + * @return self + */ + public static function new(string $message, int $code = 0): self + { + return new self($message, $code); + } +} diff --git a/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php b/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php index 8ea47de..fe2f4a0 100644 --- a/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php +++ b/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php @@ -17,4 +17,5 @@ class InvalidConfigurationArgumentException extends InvalidArgumentException { + use ExceptionTrait; } diff --git a/src/Domain/Components/Exceptions/TokenValidationException.php b/src/Domain/Components/Exceptions/TokenValidationException.php new file mode 100644 index 0000000..1acb086 --- /dev/null +++ b/src/Domain/Components/Exceptions/TokenValidationException.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\Components\Exceptions; + +use InvalidArgumentException; + +class TokenValidationException extends InvalidArgumentException +{ + use ExceptionTrait; +} From 8a8b41faf07efc71f243192b265eb872b4449fbf Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:54:35 -0500 Subject: [PATCH 100/301] [#.x] - added auth post login service --- src/Domain/Services/Auth/LoginPostService.php | 126 +++++++++++++++ .../Services/Auth/LoginPostServiceTest.php | 143 ++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 src/Domain/Services/Auth/LoginPostService.php create mode 100644 tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Services/Auth/LoginPostService.php new file mode 100644 index 0000000..306005f --- /dev/null +++ b/src/Domain/Services/Auth/LoginPostService.php @@ -0,0 +1,126 @@ + + * + * 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\Auth; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Domain\Payload; +use Phalcon\Filter\Filter; + +/** + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TLoginInput from InputTypes + */ +final readonly class LoginPostService implements DomainInterface +{ + public function __construct( + private QueryRepository $repository, + private TransportRepository $transport, + private Cache $cache, + private EnvManager $env, + private JWTToken $jwtToken, + private Filter $filter, + private Security $security, + ) { + } + + /** + * @param TLoginInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + /** + * Get email and password from the input and sanitize them + */ + $email = (string)($input['email'] ?? ''); + $password = (string)($input['password'] ?? ''); + $email = $this->filter->string($email); + $password = $this->filter->string($password); + + /** + * Check if email or password are empty + */ + if (true === empty($email) || true === empty($password)) { + return $this->getUnauthorizedPayload(); + } + + /** + * Find the user in the database + */ + $dbUser = $this->repository->user()->findByEmail($email); + $dbUserId = (int)($dbUser['usr_id'] ?? 0); + $dbPassword = $dbUser['usr_password'] ?? ''; + + /** + * Check if the user exists and if the password matches + */ + if ( + $dbUserId < 1 || + true !== $this->security->verify($password, $dbPassword) + ) { + return $this->getUnauthorizedPayload(); + } + + /** + * Get a new token for this user + */ + /** @var TUserDbRecord $dbUser $token */ + $token = $this->jwtToken->getForUser($dbUser); + $domainUser = $this->transport->newUser($dbUser); + $results = $this->transport->newLoginUser( + $domainUser, + $token, + ); + + /** + * Store the token in cache + */ + $this->cache->storeTokenInCache($this->env, $domainUser, $token); + /** + * Send the payload back + */ + return new Payload( + DomainStatus::SUCCESS, + [ + 'data' => $results, + ] + ); + } + + /** + * @return Payload + */ + private function getUnauthorizedPayload(): Payload + { + return new Payload( + DomainStatus::UNAUTHORIZED, + [ + 'errors' => [ + HttpCodesEnum::AppIncorrectCredentials->error(), + ], + ] + ); + } +} diff --git a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php new file mode 100644 index 0000000..c37c0b9 --- /dev/null +++ b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php @@ -0,0 +1,143 @@ + + * + * 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\Auth; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Services\Auth\LoginPostService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class LoginPostServiceTest extends AbstractUnitTestCase +{ + public function testServiceEmptyCredentials(): void + { + /** @var LoginPostService $service */ + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + + $payload = $service->__invoke([]); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppIncorrectCredentials->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testServiceWithCredentials(): void + { + /** @var LoginPostService $service */ + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + $migration = new UsersMigration($this->getConnection()); + + /** + * Setting the password to something we know + */ + $password = 'password'; + + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; + $payload = [ + 'email' => $email, + 'password' => $password, + ]; + + $payload = $service->__invoke($payload); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey('authenticated', $data); + $this->assertArrayHasKey('user', $data); + $this->assertArrayHasKey('jwt', $data); + + $authenticated = $data['authenticated']; + $user = $data['user']; + $jwt = $data['jwt']; + + $actual = $authenticated; + $this->assertTrue($actual); + + $expected = $dbUser['usr_id']; + $actual = $user['id']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_email']; + $actual = $user['email']; + $this->assertSame($expected, $actual); + + $expected = trim( + $dbUser['usr_name_last'] + . ', ' + . $dbUser['usr_name_first'] + . ' ' + . $dbUser['usr_name_middle'] + ); + + $actual = $user['name']; + $this->assertSame($expected, $actual); + + $this->assertArrayHasKey('token', $jwt); +// $this->assertArrayHasKey('refreshToken', $jwt); + + $this->assertNotEmpty($jwt['token']); +// $this->assertNotEmpty($jwt['refreshToken']); + } + + public function testServiceWrongCredentials(): void + { + /** @var LoginPostService $service */ + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + $migration = new UsersMigration($this->getConnection()); + + $password = 'password'; + + /** + * Issue a wrong password + */ + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; + $payload = [ + 'email' => $email, + 'password' => $password . '2', + ]; + + $payload = $service->__invoke($payload); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppIncorrectCredentials->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } +} From 5ed412947e007cbff0e334adf787efd98b9f34f6 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:54:51 -0500 Subject: [PATCH 101/301] [#.x] - added new route and adjusted tests --- .../Components/Enums/Http/RoutesEnum.php | 20 +++++++++++-------- .../Components/Enums/Http/RoutesEnumTest.php | 10 +++++----- .../Providers/RouterProviderTest.php | 4 ++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Domain/Components/Enums/Http/RoutesEnum.php b/src/Domain/Components/Enums/Http/RoutesEnum.php index db20eb3..3c6fcac 100644 --- a/src/Domain/Components/Enums/Http/RoutesEnum.php +++ b/src/Domain/Components/Enums/Http/RoutesEnum.php @@ -29,8 +29,8 @@ enum RoutesEnum: string public const POST = 'post'; public const PUT = 'put'; - case helloGet = ''; - case userGet = 'user'; + case authLoginPost = 'auth/login'; + case userGet = 'user'; /** * @return string @@ -46,8 +46,8 @@ public function endpoint(): string public function method(): string { return match ($this) { - self::helloGet, - self::userGet => self::GET, + self::authLoginPost => self::POST, + self::userGet => self::GET, }; } @@ -57,8 +57,12 @@ public function method(): string public static function middleware(): array { return [ - Container::MIDDLEWARE_NOT_FOUND => self::EVENT_BEFORE, - Container::MIDDLEWARE_HEALTH => self::EVENT_BEFORE, + Container::MIDDLEWARE_NOT_FOUND => self::EVENT_BEFORE, + Container::MIDDLEWARE_HEALTH => self::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_PRESENCE => self::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE => self::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_USER => self::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_CLAIMS => self::EVENT_BEFORE, ]; } @@ -73,8 +77,8 @@ public function prefix(): string public function service(): string { return match ($this) { - self::helloGet => Container::HELLO_SERVICE, - self::userGet => Container::USER_GET_SERVICE, + self::authLoginPost => Container::AUTH_LOGIN_POST_SERVICE, + self::userGet => Container::USER_GET_SERVICE, }; } diff --git a/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php b/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php index 5f13f7c..c8057ea 100644 --- a/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php +++ b/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php @@ -24,11 +24,11 @@ public static function getExamples(): array { return [ [ - RoutesEnum::helloGet, - '', - '/', - RoutesEnum::GET, - Container::HELLO_SERVICE, + RoutesEnum::authLoginPost, + 'auth/login', + '/auth/login', + RoutesEnum::POST, + Container::AUTH_LOGIN_POST_SERVICE, ], [ RoutesEnum::userGet, diff --git a/tests/Unit/Domain/Components/Providers/RouterProviderTest.php b/tests/Unit/Domain/Components/Providers/RouterProviderTest.php index 1ae63f4..28fa717 100644 --- a/tests/Unit/Domain/Components/Providers/RouterProviderTest.php +++ b/tests/Unit/Domain/Components/Providers/RouterProviderTest.php @@ -33,8 +33,8 @@ public function testCheckRegistration(): void $data = [ [ - 'method' => 'GET', - 'pattern' => '/', + 'method' => 'POST', + 'pattern' => '/auth/login', ], [ 'method' => 'GET', From 1f3b256bfad2a0bdb1933d6d5db675bb2de6fda4 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:55:12 -0500 Subject: [PATCH 102/301] [#.x] - added JTW class, tests and enum --- src/Domain/Components/Encryption/JWTToken.php | 209 ++++++++++++++++++ .../Components/Enums/Common/JWTEnum.php | 41 ++++ .../Components/Encryption/JWTTokenTest.php | 144 ++++++++++++ 3 files changed, 394 insertions(+) create mode 100644 src/Domain/Components/Encryption/JWTToken.php create mode 100644 src/Domain/Components/Enums/Common/JWTEnum.php create mode 100644 tests/Unit/Domain/Components/Encryption/JWTTokenTest.php diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Components/Encryption/JWTToken.php new file mode 100644 index 0000000..3134e4a --- /dev/null +++ b/src/Domain/Components/Encryption/JWTToken.php @@ -0,0 +1,209 @@ + + * + * 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\Components\Encryption; + +use DateTimeImmutable; +use InvalidArgumentException; +use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; +use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +use Phalcon\Encryption\Security\JWT\Builder; +use Phalcon\Encryption\Security\JWT\Signer\Hmac; +use Phalcon\Encryption\Security\JWT\Token\Parser; +use Phalcon\Encryption\Security\JWT\Token\Token; +use Phalcon\Encryption\Security\JWT\Validator; +use Phalcon\Support\Helper\Json\Decode; + +/** + * @phpstan-import-type TUserRecord from UserTypes + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-type TValidatorErrors array{}|array + */ +final class JWTToken +{ + /** + * @var Parser|null + */ + private ?Parser $parser = null; + + public function __construct( + private readonly EnvManager $env + ) { + } + + /** + * Returns the string token + * + * @param TUserDbRecord $user + * + * @return string + */ + public function getForUser(array $user): string + { + /** @var int $expiration */ + $expiration = $this->env->get( + 'TOKEN_EXPIRATION', + Cache::CACHE_TOKEN_EXPIRY, + 'int' + ); + + $now = new DateTimeImmutable(); + $expiresAt = $now->modify('+' . $expiration . ' seconds'); + + return $this->generateTokenForUser($user, $now, $expiresAt); + } + + /** + * Return the JWT Token object + * + * @param string $token + * + * @return Token + */ + public function getObject(string $token): Token + { + try { + if (null === $this->parser) { + $this->parser = new Parser(new Decode()); + } + + $tokenObject = $this->parser->parse($token); + } catch (InvalidArgumentException $ex) { + throw TokenValidationException::new($ex->getMessage()); + } + + return $tokenObject; + } + + /** + * @param QueryRepository $repository + * @param Token $token + * + * @return TUserRecord + */ + public function getUser( + QueryRepository $repository, + Token $token, + ): array { + /** @var string $issuer */ + $issuer = $token->getClaims()->get(JWTEnum::Issuer->value); + /** @var string $tokenId */ + $tokenId = $token->getClaims()->get(JWTEnum::Id->value); + /** @var string $userId */ + $userId = $token->getClaims()->get(JWTEnum::UserId->value); + + $criteria = [ + 'usr_id' => $userId, + 'usr_status_flag' => FlagsEnum::Active->value, + 'usr_issuer' => $issuer, + 'usr_token_id' => $tokenId, + ]; + + /** @var TUserRecord $user */ + $user = $repository->user()->findOneBy($criteria); + + return $user; + } + + /** + * @param Token $tokenObject + * @param UserTransport $user + * + * @return TValidatorErrors + */ + public function validate( + Token $tokenObject, + UserTransport $user + ): array { + $validator = new Validator($tokenObject); + $signer = new Hmac(); + $now = new DateTimeImmutable(); + + $validator + ->validateId($user->getTokenId()) + ->validateAudience($this->getTokenAudience()) + ->validateIssuer($user->getIssuer()) + ->validateNotBefore($now->getTimestamp()) + ->validateIssuedAt($now->getTimestamp()) + ->validateExpiration($now->getTimestamp()) + ->validateSignature($signer, $user->getTokenPassword()) + ->validateClaim(JWTEnum::UserId->value, $user->getId()) + ; + + /** @var TValidatorErrors $errors */ + $errors = $validator->getErrors(); + + return $errors; + } + + /** + * @param TUserDbRecord $user + * @param DateTimeImmutable $now + * @param DateTimeImmutable $expiresAt + * @param bool $isRefresh + * + * @return string + */ + private function generateTokenForUser( + array $user, + DateTimeImmutable $now, + DateTimeImmutable $expiresAt, + bool $isRefresh = true + ): string { + $tokenBuilder = new Builder(new Hmac()); + /** @var string $issuer */ + $issuer = $user['usr_issuer']; + /** @var string $tokenPassword */ + $tokenPassword = $user['usr_token_password']; + /** @var string $tokenId */ + $tokenId = $user['usr_token_id']; + /** @var string $userId */ + $userId = $user['usr_id']; + + $tokenObject = $tokenBuilder + ->setIssuer($issuer) + ->setAudience($this->getTokenAudience()) + ->setId($tokenId) + ->setNotBefore($now->getTimestamp()) + ->setIssuedAt($now->getTimestamp()) + ->setExpirationTime($expiresAt->getTimestamp()) + ->setPassphrase($tokenPassword) + ->addClaim(JWTEnum::UserId->value, $userId) + ->addClaim(JWTEnum::Refresh->value, $isRefresh) + ->getToken() + ; + + return $tokenObject->getToken(); + } + + + /** + * @return string + */ + private function getTokenAudience(): string + { + /** @var string $audience */ + $audience = $this->env->get( + 'TOKEN_AUDIENCE', + 'https://rest-api.phalcon.io' + ); + + return $audience; + } +} diff --git a/src/Domain/Components/Enums/Common/JWTEnum.php b/src/Domain/Components/Enums/Common/JWTEnum.php new file mode 100644 index 0000000..c37ff35 --- /dev/null +++ b/src/Domain/Components/Enums/Common/JWTEnum.php @@ -0,0 +1,41 @@ + + * + * 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\Components\Enums\Common; + +enum JWTEnum: string +{ + /** + * Headers + */ + case Type = 'typ'; + case Algo = 'alg'; + case ContentType = 'cty'; + + /** + * Claims + */ + case Audience = 'aud'; + case ExpirationTime = 'exp'; + case Id = 'jti'; + case IssuedAt = 'iat'; + case Issuer = 'iss'; + case NotBefore = 'nbf'; + case Subject = 'sub'; + + /** + * Custom claims + */ + case UserId = 'uid'; + case Refresh = 'rid'; +} diff --git a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php b/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php new file mode 100644 index 0000000..476ff38 --- /dev/null +++ b/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php @@ -0,0 +1,144 @@ + + * + * 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\Components\Encryption; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Encryption\Security\JWT\Token\Token; + +use function sleep; + +final class JWTTokenTest extends AbstractUnitTestCase +{ + private JWTToken $jwtToken; + + public function setUp(): void + { + parent::setUp(); + + $this->jwtToken = $this->container->get(Container::JWT_TOKEN); + } + + public function testGetForUserReturnsTokenString(): void + { + $user = $this->getUserData(); + + $token = $this->jwtToken->getForUser($user); + $this->assertIsString($token); + $this->assertNotEmpty($token); + } + + public function testGetObjectReturnsPlainToken(): void + { + $user = $this->getUserData(); + + $tokenString = $this->jwtToken->getForUser($user); + + $plain = $this->jwtToken->getObject($tokenString); + $this->assertInstanceOf(Token::class, $plain); + } + + public function testGetObjectThrowsOnCannotDecodeContent(): void + { + $this->expectException(TokenValidationException::class); + $this->expectExceptionMessage('Invalid Header (not an array)'); + + // Simulate exception by calling with invalid token + $this->jwtToken->getObject('invalid.token.content'); + } + + public function testGetObjectThrowsOnInvalidTokenStructure(): void + { + $this->expectException(TokenValidationException::class); + + // This will throw, as the structure is invalid + $this->jwtToken->getObject('abc.def.ghi'); + } + + public function testGetUserReturnsUserArray(): void + { + $user = $this->getUserData(); + + $tokenString = $this->jwtToken->getForUser($user); + $plain = $this->jwtToken->getObject($tokenString); + + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'findOneBy', + ] + ) + ->getMock() + ; + + $userRepository->expects($this->once()) + ->method('findOneBy') + ->willReturn($user) + ; + + $mockRepository = $this + ->getMockBuilder(QueryRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'user', + ] + ) + ->getMock() + ; + $mockRepository->expects($this->once()) + ->method('user') + ->willReturn($userRepository) + ; + + $result = $this->jwtToken->getUser($mockRepository, $plain); + $this->assertEquals($user, $result); + } + + public function testValidateSuccess(): void + { + $user = $this->getUserData(); + + $tokenString = $this->jwtToken->getForUser($user); + $plain = $this->jwtToken->getObject($tokenString); + $userTransport = new UserTransport($user); + + sleep(1); + + $actual = $this->jwtToken->validate($plain, $userTransport); + + $this->assertSame([], $actual); + } + + /** + * @return array + */ + private function getUserData(): array + { + $user = $this->getNewUserData(); + $user['usr_id'] = 2; + $user['usr_token_id'] = $this->getStrongPassword(); + $user['usr_issuer'] = 'issuer'; + $user['usr_token_password'] = $this->getStrongPassword(); + + return $user; + } +} From 0dbc02db0a1b365c3e1f45c5fb42972eda38ee79 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:55:34 -0500 Subject: [PATCH 103/301] [#.x] - corrected env references --- .../Providers/ErrorHandlerProvider.php | 35 ++++++++++----- .../Providers/ErrorHandlerProviderTest.php | 45 +++++++++++++------ 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/Domain/Components/Providers/ErrorHandlerProvider.php b/src/Domain/Components/Providers/ErrorHandlerProvider.php index 81452a9..7cf3b31 100644 --- a/src/Domain/Components/Providers/ErrorHandlerProvider.php +++ b/src/Domain/Components/Providers/ErrorHandlerProvider.php @@ -31,11 +31,15 @@ class ErrorHandlerProvider implements ServiceProviderInterface { public function register(DiInterface $container): void { + /** @var EnvManager $env */ + $env = $container->getShared(Container::ENV); /** @var Logger $logger */ $logger = $container->getShared(Container::LOGGER); + /** @var int $time */ + $time = $container->getShared(Container::TIME); - date_default_timezone_set(EnvManager::appTimezone()); - $errors = 'development' === EnvManager::appEnv() ? 'On' : 'Off'; + date_default_timezone_set($env->appTimezone()); + $errors = 'development' === $env->appEnv() ? 'On' : 'Off'; ini_set('display_errors', $errors); error_reporting(E_ALL); @@ -57,20 +61,31 @@ function (int $number, string $message, string $file, int $line) use ($logger) { } ); - register_shutdown_function([$this, 'onShutdown'], $container); + register_shutdown_function( + [$this, 'onShutdown'], + $logger, + $env, + $time + ); } - protected function onShutdown(DiInterface $container): bool - { - /** @var Logger $logger */ - $logger = $container->getShared(Container::LOGGER); - /** @var int $time */ - $time = $container->getShared(Container::TIME); + /** + * @param Logger $logger + * @param EnvManager $env + * @param int $time + * + * @return bool + */ + protected function onShutdown( + Logger $logger, + EnvManager $env, + int $time + ): bool { $memory = memory_get_usage() / 1000000; $execution = hrtime(true) - $time; $execution = $execution / 1000000000; - if (EnvManager::appLogLevel() >= 1) { + if ($env->appLogLevel() >= 1) { $logger ->info( sprintf( diff --git a/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php index b3f2343..a77e9ad 100644 --- a/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php +++ b/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php @@ -17,6 +17,7 @@ use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Domain\Components\Providers\ErrorHandlerProvider; use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Logger\Logger; use PHPUnit\Framework\Attributes\BackupGlobals; use ReflectionClass; @@ -42,11 +43,14 @@ function () use ($now) { true ); + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $provider = new ErrorHandlerProvider(); $provider->register($this->container); $expected = date_default_timezone_get(); - $actual = EnvManager::appTimezone(); + $actual = $env->appTimezone(); $this->assertSame($expected, $actual); $expected = 'On'; @@ -71,6 +75,9 @@ function () use ($now) { true ); + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + $provider = new ErrorHandlerProvider(); $provider->register($this->container); @@ -81,25 +88,16 @@ function () use ($now) { restore_error_handler(); /** @var string $logName */ - $logName = EnvManager::get('LOG_FILENAME', 'rest-api'); + $logName = $env->get('LOG_FILENAME', 'rest-api'); /** @var string $logPath */ - $logPath = EnvManager::get('LOG_PATH', 'storage/logs/'); - $logFile = EnvManager::appPath($logPath) . '/' . $logName . '.log'; + $logPath = $env->get('LOG_PATH', 'storage/logs/'); + $logFile = $env->appPath($logPath) . '/' . $logName . '.log'; $this->assertFileContentsContains($logFile, $message); } public function testRegisterShutdown(): void { - /** @var string $logName */ - $logName = EnvManager::get('LOG_FILENAME', 'rest-api'); - /** @var string $logPath */ - $logPath = EnvManager::get('LOG_PATH', 'storage/logs/'); - $logFile = EnvManager::appPath($logPath) . '/' . $logName . '.log'; - - $_ENV['APP_ENV'] = 'development'; - $_ENV['APP_LOG_LEVEL'] = 2; - $now = hrtime(true); $this->container->set( Container::TIME, @@ -109,6 +107,20 @@ function () use ($now) { true ); + $_ENV['APP_ENV'] = 'development'; + $_ENV['APP_LOG_LEVEL'] = 2; + + /** @var Logger $logger */ + $logger = $this->container->getShared(Container::LOGGER); + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + + /** @var string $logName */ + $logName = $env->get('LOG_FILENAME', 'rest-api'); + /** @var string $logPath */ + $logPath = $env->get('LOG_PATH', 'storage/logs/'); + $logFile = $env->appPath($logPath) . '/' . $logName . '.log'; + $provider = new ErrorHandlerProvider(); $provider->register($this->container); @@ -116,7 +128,12 @@ function () use ($now) { $reflection = new ReflectionClass($provider); $method = $reflection->getMethod('onShutdown'); $method->setAccessible(true); - $shutdown = $method->invoke($provider, $this->container); + $shutdown = $method->invoke( + $provider, + $logger, + $env, + $now + ); restore_error_handler(); From 1568160b49b03f2fb59768066a05763d805aaa9c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:55:48 -0500 Subject: [PATCH 104/301] [#.x] - new types and added POST to input --- src/Domain/ADR/Input.php | 6 +++++- src/Domain/ADR/InputTypes.php | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Domain/ADR/Input.php b/src/Domain/ADR/Input.php index 95ffaed..ed98253 100644 --- a/src/Domain/ADR/Input.php +++ b/src/Domain/ADR/Input.php @@ -15,6 +15,8 @@ use Phalcon\Http\RequestInterface; +use function array_merge; + /** * @phpstan-import-type TRequestQuery from InputTypes */ @@ -29,7 +31,9 @@ public function __invoke(RequestInterface $request): array { /** @var TRequestQuery $query */ $query = $request->getQuery(); + /** @var TRequestQuery $post */ + $post = $request->getPost(); - return $query; + return array_merge($query, $post); } } diff --git a/src/Domain/ADR/InputTypes.php b/src/Domain/ADR/InputTypes.php index 358ea99..40b8806 100644 --- a/src/Domain/ADR/InputTypes.php +++ b/src/Domain/ADR/InputTypes.php @@ -14,7 +14,10 @@ namespace Phalcon\Api\Domain\ADR; /** - * @phpstan-type THelloInput array{} + * @phpstan-type TLoginInput array{ + * email?: string, + * password?: string + * } * @phpstan-type TUserInput array{ * userId?: int * } From 1455fe9f3f9947f5c717e24f11c0d1bbef5c4c14 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:56:02 -0500 Subject: [PATCH 105/301] [#.x] - added new middleware for tokens --- .../ValidateTokenClaimsMiddleware.php | 79 +++++++++ .../ValidateTokenPresenceMiddleware.php | 54 ++++++ .../ValidateTokenStructureMiddleware.php | 72 ++++++++ .../ValidateTokenUserMiddleware.php | 78 +++++++++ .../ValidateTokenClaimsMiddlewareTest.php | 159 ++++++++++++++++++ .../ValidateTokenPresenceMiddlewareTest.php | 90 ++++++++++ .../ValidateTokenStructureMiddlewareTest.php | 117 +++++++++++++ .../ValidateTokenUserMiddlewareTest.php | 97 +++++++++++ 8 files changed, 746 insertions(+) create mode 100644 src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php create mode 100644 src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php create mode 100644 src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php create mode 100644 src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php create mode 100644 tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php create mode 100644 tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php create mode 100644 tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php create mode 100644 tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php diff --git a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php new file mode 100644 index 0000000..f659890 --- /dev/null +++ b/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php @@ -0,0 +1,79 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Encryption\Security\JWT\Token\Token; +use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\Response\Exception; +use Phalcon\Mvc\Micro; + +/** + * Validates the token claims + */ +final class ValidateTokenClaimsMiddleware extends AbstractMiddleware +{ + /** + * @param Micro $application + * + * @return bool + * @throws EventsException + * @throws Exception + */ + public function call(Micro $application): bool + { + /** @var JWTToken $jwtToken */ + $jwtToken = $application->getSharedService(Container::JWT_TOKEN); + /** @var TransportRepository $transport */ + $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + + /** + * Get the token object + */ + /** @var Token $tokenObject */ + $tokenObject = $transport->getSessionToken(); + /** + * Our used is in the transport, so we can get it without a + * database call + */ + /** @var UserTransport $sessionUser */ + $sessionUser = $transport->getSessionUser(); + + /** + * This is where we validate everything. Even though the user + * we got from the database has been checked against the 'uid' + * claims of the token, we will still check the claims against the + * user stored in the session + */ + $errors = $jwtToken->validate($tokenObject, $sessionUser); + + if (true !== empty($errors)) { + $this->halt( + $application, + HttpCodesEnum::Unauthorized->value, + HttpCodesEnum::Unauthorized->text(), + [], + [$errors] + ); + + return false; + } + + return true; + } +} diff --git a/src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php new file mode 100644 index 0000000..dbe48ad --- /dev/null +++ b/src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php @@ -0,0 +1,54 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\Request; +use Phalcon\Http\Response\Exception; +use Phalcon\Mvc\Micro; + +final class ValidateTokenPresenceMiddleware extends AbstractMiddleware +{ + /** + * @param Micro $application + * + * @return bool + * @throws EventsException + * @throws Exception + */ + public function call(Micro $application): bool + { + /** @var Request $request */ + $request = $application->getSharedService(Container::REQUEST); + /** @var EnvManager $env */ + $env = $application->getSharedService(Container::ENV); + + if (true === $this->isEmptyBearerToken($request, $env)) { + $this->halt( + $application, + HttpCodesEnum::Unauthorized->value, + HttpCodesEnum::Unauthorized->text(), + [], + [HttpCodesEnum::AppTokenNotPresent->error()] + ); + + return false; + } + + return true; + } +} diff --git a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php new file mode 100644 index 0000000..d69eee0 --- /dev/null +++ b/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.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\Domain\Components\Middleware; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\Request; +use Phalcon\Http\Response\Exception; +use Phalcon\Mvc\Micro; + +final class ValidateTokenStructureMiddleware extends AbstractMiddleware +{ + /** + * @param Micro $application + * + * @return bool + * @throws EventsException + * @throws Exception + */ + public function call(Micro $application): bool + { + /** @var Request $request */ + $request = $application->getSharedService(Container::REQUEST); + /** @var EnvManager $env */ + $env = $application->getSharedService(Container::ENV); + /** @var JWTToken $jwtToken */ + $jwtToken = $application->getSharedService(Container::JWT_TOKEN); + + try { + $token = $jwtToken->getObject( + $this->getBearerTokenFromHeader($request, $env) + ); + } catch (TokenValidationException $ex) { + $this->halt( + $application, + HttpCodesEnum::Unauthorized->value, + HttpCodesEnum::Unauthorized->text(), + [], + [ + [$ex->getCode() => $ex->getMessage()], + ] + ); + + return false; + } + + /** + * If we are down here the token is an object and is valid + */ + /** @var TransportRepository $transport */ + $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + $transport->setSessionToken($token); + + return true; + } +} diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php new file mode 100644 index 0000000..ce2ea63 --- /dev/null +++ b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php @@ -0,0 +1,78 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Encryption\Security\JWT\Token\Token; +use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\Response\Exception; +use Phalcon\Mvc\Micro; + +/** + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TUserRecord from UserTypes + */ +final class ValidateTokenUserMiddleware extends AbstractMiddleware +{ + /** + * @param Micro $application + * + * @return bool + * @throws EventsException + * @throws Exception + */ + public function call(Micro $application): bool + { + /** @var JWTToken $jwtToken */ + $jwtToken = $application->getSharedService(Container::JWT_TOKEN); + /** @var QueryRepository $repository */ + $repository = $application->getSharedService(Container::REPOSITORY); + /** @var TransportRepository $transport */ + $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + + /** + * Get the token object + */ + /** @var Token $tokenObject */ + $tokenObject = $transport->getSessionToken(); + /** @var TUserRecord $dbUser */ + $dbUser = $jwtToken->getUser($repository, $tokenObject); + + if (true === empty($dbUser)) { + $this->halt( + $application, + HttpCodesEnum::Unauthorized->value, + HttpCodesEnum::Unauthorized->text(), + [], + [HttpCodesEnum::AppTokenInvalidUser->error()] + ); + + return false; + } + + /** + * If we are here everything is fine and we need to keep the user + * as a "session" user in the transport + */ + /** @var TUserDbRecord $dbUser */ + $transport->setSessionUser($dbUser); + + return true; + } +} diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php new file mode 100644 index 0000000..5535a13 --- /dev/null +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php @@ -0,0 +1,159 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Mvc\Micro; +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\DataProvider; + +use function array_replace; + +#[BackupGlobals(true)] +final class ValidateTokenClaimsMiddlewareTest extends AbstractUnitTestCase +{ + public static function getExamples(): array + { + return [ + [ + ['usr_token_id' => 'abcdef'], + [ + 'Validation: incorrect Id', + ], + ], + [ + ['usr_issuer' => 'abcdef'], + [ + 'Validation: incorrect issuer', + ], + ], + [ + ['algo' => 'sha256'], + [ + 'Validation: the signature does not match', + ], + ], + [ + ['audience' => 'wrong audience'], + [ + 'Validation: audience not allowed', + ], + ], + [ + ['usr_id' => 1], + [ + 'Validation: incorrect uid', + ], + ], + ]; + } + + #[DataProvider('getExamples')] + public function testValidateTokenClaimsFailure( + array $userData, + array $expectedErrors + ): void { + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $user; + + [$micro, $middleware, $jwtToken] = $this->setupTest(); + + /** + * Make the signature non valid + */ + $tokenUser = array_replace($tokenUser, $userData); + $token = $this->getUserToken($tokenUser); + $tokenObject = $jwtToken->getObject($token); + + /** + * Store the user in the session + */ + /** @var TransportRepository $transport */ + $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); + $transport->setSessionUser($user); + $transport->setSessionToken($tokenObject); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $middleware->call($micro); + $contents = ob_get_clean(); + + $contents = json_decode($contents, true); + + $data = $contents['data']; + $errors = $contents['errors']; + + $this->assertSame([], $data); + + $expected = [$expectedErrors]; + $this->assertSame($expected, $errors); + } + + public function testValidateTokenClaimsSuccess(): void + { + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $user; + + [$micro, $middleware, $jwtToken] = $this->setupTest(); + + $token = $this->getUserToken($tokenUser); + $tokenObject = $jwtToken->getObject($token); + + /** + * Store the user in the session + */ + /** @var TransportRepository $transport */ + $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); + $transport->setSessionUser($user); + $transport->setSessionToken($tokenObject); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $actual = $middleware->call($micro); + ob_get_clean(); + + $this->assertTrue($actual); + } + + /** + * @return array + */ + private function setupTest(): array + { + $micro = new Micro($this->container); + $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_CLAIMS); + $jwtToken = $this->container->getShared(Container::JWT_TOKEN); + + return [$micro, $middleware, $jwtToken]; + } +} diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php new file mode 100644 index 0000000..1bea4b3 --- /dev/null +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php @@ -0,0 +1,90 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Mvc\Micro; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class ValidateTokenPresenceMiddlewareTest extends AbstractUnitTestCase +{ + public function testValidateTokenPresenceFailure(): void + { + $micro = new Micro($this->container); + $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_PRESENCE); + + /** + * No token exception + */ + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $middleware->call($micro); + $contents = ob_get_clean(); + + $contents = json_decode($contents, true); + $data = $contents['data']; + $errors = $contents['errors']; + + $this->assertSame([], $data); + + $expected = [HttpCodesEnum::AppTokenNotPresent->error()]; + $this->assertSame($expected, $errors); + } + + public function testValidateTokenPresenceSuccess(): void + { + $micro = new Micro($this->container); + $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_PRESENCE); + + /** + * No token return + */ + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $actual = $middleware->call($micro); + ob_end_clean(); + $this->assertFalse($actual); + + /** + * Token present + */ + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer 123.456.789', + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $actual = $middleware->call($micro); + ob_end_clean(); + $this->assertTrue($actual); + } +} diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php new file mode 100644 index 0000000..7eece51 --- /dev/null +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php @@ -0,0 +1,117 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Mvc\Micro; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class ValidateTokenStructureMiddlewareTest extends AbstractUnitTestCase +{ + public function testValidateTokenStructureFailureBadSignature(): void + { + [$micro, $middleware] = $this->setupTest(); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer abcd.abcd.abcd', + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $middleware->call($micro); + $contents = ob_get_clean(); + + $contents = json_decode($contents, true); + $data = $contents['data']; + $errors = $contents['errors']; + + $this->assertSame([], $data); + + $expected = [ + ['Invalid Header (not an array)'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testValidateTokenStructureFailureNoDots(): void + { + [$micro, $middleware] = $this->setupTest(); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer abcd.abcd', + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $middleware->call($micro); + $contents = ob_get_clean(); + + $contents = json_decode($contents, true); + $data = $contents['data']; + $errors = $contents['errors']; + + $this->assertSame([], $data); + + $expected = [ + ['Invalid JWT string (dots misalignment)'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testValidateTokenStructureSuccess(): void + { + $userData = $this->getNewUserData(); + + [$micro, $middleware] = $this->setupTest(); + /** @var JWTToken $jwtToken */ + $jwtToken = $micro->getSharedService(Container::JWT_TOKEN); + + $token = $jwtToken->getForUser($userData); + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $actual = $middleware->call($micro); + ob_end_flush(); + + $this->assertTrue($actual); + } + + /** + * @return array + */ + private function setupTest(): array + { + $micro = new Micro($this->container); + $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE); + + return [$micro, $middleware]; + } +} diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php new file mode 100644 index 0000000..d991e2e --- /dev/null +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php @@ -0,0 +1,97 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Mvc\Micro; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class ValidateTokenUserMiddlewareTest extends AbstractUnitTestCase +{ + public function testValidateTokenUserFailureRecordNotFound(): void + { + [$micro, $middleware, $jwtToken] = $this->setupTest(); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + $token = $this->getUserToken($userData); + $tokenObject = $jwtToken->getObject($token); + $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); + $transport->setSessionToken($tokenObject); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $middleware->call($micro); + $contents = ob_get_clean(); + + $contents = json_decode($contents, true); + $data = $contents['data']; + $errors = $contents['errors']; + + $this->assertSame([], $data); + + $expected = [HttpCodesEnum::AppTokenInvalidUser->error()]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testValidateTokenUserSuccess(): void + { + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + + [$micro, $middleware, $jwtToken] = $this->setupTest(); + + $token = $jwtToken->getForUser($user); + $tokenObject = $jwtToken->getObject($token); + $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); + $transport->setSessionToken($tokenObject); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => '/user?userId=1234', + ]; + + ob_start(); + $actual = $middleware->call($micro); + ob_end_flush(); + + $this->assertTrue($actual); + } + + /** + * @return array + */ + private function setupTest(): array + { + $micro = new Micro($this->container); + $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_USER); + $jwtToken = $this->container->get(Container::JWT_TOKEN); + + return [$micro, $middleware, $jwtToken]; + } +} From 4f94a644dacf1d04419e07b7e162df445c5c1513 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:56:25 -0500 Subject: [PATCH 106/301] [#.x] - corrected test --- tests/Unit/Domain/Services/User/UserServiceTest.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Unit/Domain/Services/User/UserServiceTest.php b/tests/Unit/Domain/Services/User/UserServiceTest.php index 513f82d..9140759 100644 --- a/tests/Unit/Domain/Services/User/UserServiceTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceTest.php @@ -27,17 +27,21 @@ final class UserServiceTest extends AbstractUnitTestCase { public function testDispatch(): void { + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); /** @var TransportRepository $transport */ $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); $userId = $dbUser['usr_id']; + $token = $this->getUserToken($dbUser); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, 'REQUEST_URI' => '/user', ]; @@ -46,7 +50,7 @@ public function testDispatch(): void ]; ob_start(); - require_once EnvManager::appPath('public/index.php'); + require_once $env->appPath('public/index.php'); $response = ob_get_clean(); $contents = json_decode($response, true); From cd3556bb01ea1def81e16b2b07b33458be202505 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:56:35 -0500 Subject: [PATCH 107/301] [#.x] - new types for user --- src/Domain/Components/DataSource/User/UserTypes.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Domain/Components/DataSource/User/UserTypes.php b/src/Domain/Components/DataSource/User/UserTypes.php index d9b6bf2..a9825b3 100644 --- a/src/Domain/Components/DataSource/User/UserTypes.php +++ b/src/Domain/Components/DataSource/User/UserTypes.php @@ -14,6 +14,18 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; /** + * @phpstan-type TLoginResponsePayload array{ + * authenticated: true, + * user: array{ + * id: int, + * name: string, + * email: string + * }, + * jwt: array{ + * token: string + * } + * } + * * @phpstan-type TUserDbRecord array{ * usr_id: int, * usr_status_flag: int, From 81ebcd27b9220b62318943ff0871fdd5356927e7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:56:57 -0500 Subject: [PATCH 108/301] [#.x] - minor adjustments --- src/Domain/Components/DataSource/User/UserRepository.php | 2 +- src/Domain/Components/DataSource/User/UserTransport.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index c288ede..007929b 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -20,7 +20,7 @@ /** * @phpstan-import-type TUserRecord from UserTypes */ -final class UserRepository +class UserRepository { public function __construct( private readonly Connection $connection, diff --git a/src/Domain/Components/DataSource/User/UserTransport.php b/src/Domain/Components/DataSource/User/UserTransport.php index 98f71eb..98490cf 100644 --- a/src/Domain/Components/DataSource/User/UserTransport.php +++ b/src/Domain/Components/DataSource/User/UserTransport.php @@ -99,7 +99,7 @@ public function __call(string $name, array $arguments): int | string 'getCreatedUserId' => $this->store['createdUserId'], 'getUpdatedDate' => $this->store['updatedDate'], 'getUpdatedUserId' => $this->store['updatedUserId'], - default => throw new InvalidConfigurationArgumentException( + default => throw InvalidConfigurationArgumentException::new( 'The ' . $name . ' method is not supported. [' . json_encode($arguments) . ']', ), From 87ca7d98f84fa34ade23bf4e1ef2e1255be170f3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:57:13 -0500 Subject: [PATCH 109/301] [#.x] - added new services (cache, jwt etc.) --- src/Domain/Components/Container.php | 193 ++++++++++++++++++++++++---- 1 file changed, 169 insertions(+), 24 deletions(-) diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index fea369d..265f950 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -13,15 +13,22 @@ namespace Phalcon\Api\Domain\Components; +use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; -use Phalcon\Api\Domain\Services\Hello\HelloService; +use Phalcon\Api\Domain\Components\Middleware\ValidateTokenClaimsMiddleware; +use Phalcon\Api\Domain\Components\Middleware\ValidateTokenPresenceMiddleware; +use Phalcon\Api\Domain\Components\Middleware\ValidateTokenStructureMiddleware; +use Phalcon\Api\Domain\Components\Middleware\ValidateTokenUserMiddleware; +use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Domain\Services\User\UserGetService; use Phalcon\Api\Responder\JsonResponder; +use Phalcon\Cache\AdapterFactory; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Di\Di; use Phalcon\Di\Service; @@ -32,30 +39,39 @@ use Phalcon\Logger\Adapter\Stream; use Phalcon\Logger\Logger; use Phalcon\Mvc\Router; +use Phalcon\Storage\SerializerFactory; class Container extends Di { /** @var string */ public const APPLICATION = 'application'; + /** + * Services + */ + public const AUTH_LOGIN_POST_SERVICE = 'service.auth.login.post'; /** @var string */ public const CACHE = 'cache'; /** @var string */ public const CONNECTION = 'connection'; /** @var string */ + public const ENV = 'env'; + /** @var string */ public const EVENTS_MANAGER = 'eventsManager'; /** @var string */ public const FILTER = 'filter'; - /** - * Services - */ - public const HELLO_SERVICE = HelloService::class; + /** @var string */ + public const JWT_TOKEN = 'jwt.token'; /** @var string */ public const LOGGER = 'logger'; /** * Middleware */ - public const MIDDLEWARE_HEALTH = HealthMiddleware::class; - public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; + public const MIDDLEWARE_HEALTH = HealthMiddleware::class; + public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_CLAIMS = ValidateTokenClaimsMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_PRESENCE = ValidateTokenPresenceMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE = ValidateTokenStructureMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_USER = ValidateTokenUserMiddleware::class; /** * Repositories */ @@ -67,6 +83,7 @@ class Container extends Di * Responders */ public const RESPONDER_JSON = JsonResponder::class; +// public const MIDDLEWARE_VALIDATE_TOKEN_REVOKED = ValidateTokenRevokedMiddleware::class; /** @var string */ public const RESPONSE = 'response'; /** @var string */ @@ -74,27 +91,117 @@ class Container extends Di /** @var string */ public const SECURITY = Security::class; /** @var string */ - public const TIME = 'time'; - public const USER_GET_SERVICE = 'service.user.get'; + public const TIME = 'time'; + public const USER_GET_SERVICE = 'service.user.get'; public function __construct() { $this->services = [ + self::CACHE => $this->getServiceCache(), self::CONNECTION => $this->getServiceConnection(), + self::ENV => $this->getServiceEnv(), self::EVENTS_MANAGER => $this->getServiceEventsManger(), self::FILTER => $this->getServiceFilter(), + self::JWT_TOKEN => $this->getServiceJWTToken(), self::LOGGER => $this->getServiceLogger(), self::REQUEST => new Service(Request::class, true), self::RESPONSE => new Service(Response::class, true), self::ROUTER => $this->getServiceRouter(), - self::USER_GET_SERVICE => $this->getServiceUserGet(), - self::REPOSITORY => $this->getServiceRepository(), + self::REPOSITORY => $this->getServiceRepository(), + + self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthLoginPost(), + self::USER_GET_SERVICE => $this->getServiceUserGet(), ]; parent::__construct(); } + /** + * @return Service + */ + private function getServiceAuthLoginPost(): Service + { + return new Service( + [ + 'className' => LoginPostService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => self::REPOSITORY_TRANSPORT, + ], + [ + 'type' => 'service', + 'name' => self::CACHE, + ], + [ + 'type' => 'service', + 'name' => self::ENV, + ], + [ + 'type' => 'service', + 'name' => self::JWT_TOKEN, + ], + [ + 'type' => 'service', + 'name' => self::FILTER, + ], + [ + 'type' => 'service', + 'name' => self::SECURITY, + ], + ], + ] + ); + } + + /** + * @return Service + */ + private function getServiceCache(): Service + { + return new Service( + function () { + /** @var EnvManager $env */ + $env = $this->getShared(self::ENV); + + /** @var string $prefix */ + $prefix = $env->get('CACHE_PREFIX', '-rest-'); + /** @var string $host */ + $host = $env->get('CACHE_HOST', 'localhost'); + /** @var int $lifetime */ + $lifetime = $env->get('CACHE_LIFETIME', Cache::CACHE_LIFETIME_DAY, 'int'); + /** @var int $index */ + $index = $env->get('CACHE_INDEX', 0, 'int'); + /** @var int $port */ + $port = $env->get('CACHE_PORT', 6379, 'int'); + + $options = [ + 'host' => $host, + 'index' => $index, + 'lifetime' => $lifetime, + 'prefix' => $prefix, + 'port' => $port, + 'uniqueId' => $prefix, + ]; + + /** @var string $adapter */ + $adapter = $env->get('CACHE_ADAPTER', 'redis'); + + $serializerFactory = new SerializerFactory(); + $adapterFactory = new AdapterFactory($serializerFactory); + $cacheAdapter = $adapterFactory->newInstance($adapter, $options); + + return new Cache($cacheAdapter); + }, + true + ); + } + /** * @return Service */ @@ -102,17 +209,21 @@ private function getServiceConnection(): Service { return new Service( function () { + /** @var EnvManager $env */ + $env = $this->getShared(self::ENV); + /** @var string $dbName */ - $dbName = EnvManager::get('DB_NAME', 'phalcon'); + $dbName = $env->get('DB_NAME', 'phalcon'); /** @var string $host */ - $host = EnvManager::get('DB_HOST', 'rest-db'); + $host = $env->get('DB_HOST', 'rest-db'); /** @var string $password */ - $password = EnvManager::get('DB_PASS', 'secret'); - $port = (int)EnvManager::get('DB_PORT', 3306); + $password = $env->get('DB_PASS', 'secret'); + /** @var int $port */ + $port = $env->get('DB_PORT', 3306, 'int'); /** @var string $username */ - $username = EnvManager::get('DB_USER', 'root'); + $username = $env->get('DB_USER', 'root'); /** @var string $encoding */ - $encoding = EnvManager::get('DB_CHARSET', 'utf8'); + $encoding = $env->get('DB_CHARSET', 'utf8'); $queries = ['SET NAMES utf8mb4']; $dsn = sprintf( 'mysql:host=%s;port=%s;dbname=%s;charset=%s', @@ -134,6 +245,19 @@ function () { ); } + /** + * @return Service + */ + private function getServiceEnv(): Service + { + return new Service( + function () { + return new EnvManager(); + }, + true + ); + } + /** * @return Service */ @@ -166,16 +290,37 @@ function () { /** * @return Service */ - private function getServiceLogger(): Service + private function getServiceJWTToken(): Service { - /** @var string $logName */ - $logName = EnvManager::get('LOG_FILENAME', 'rest-api'); - /** @var string $logPath */ - $logPath = EnvManager::get('LOG_PATH', 'storage/logs/'); - $logFile = EnvManager::appPath($logPath) . '/' . $logName . '.log'; + return new Service( + [ + 'className' => JWTToken::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::ENV, + ], + ], + ] + ); + } + /** + * @return Service + */ + private function getServiceLogger(): Service + { return new Service( - function () use ($logName, $logFile) { + function () { + /** @var EnvManager $env */ + $env = $this->getShared(self::ENV); + + /** @var string $logName */ + $logName = $env->get('LOG_FILENAME', 'rest-api'); + /** @var string $logPath */ + $logPath = $env->get('LOG_PATH', 'storage/logs/'); + $logFile = $env->appPath($logPath) . '/' . $logName . '.log'; + return new Logger( $logName, [ From de73eab0e423e407b4c573fb537ddc1f41369bc5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:57:23 -0500 Subject: [PATCH 110/301] [#.x] - added token related methods --- .../Middleware/AbstractMiddleware.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Domain/Components/Middleware/AbstractMiddleware.php b/src/Domain/Components/Middleware/AbstractMiddleware.php index edf464b..6dfcee8 100644 --- a/src/Domain/Components/Middleware/AbstractMiddleware.php +++ b/src/Domain/Components/Middleware/AbstractMiddleware.php @@ -15,21 +15,59 @@ use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Responder\ResponderTypes; use Phalcon\Domain\Payload; use Phalcon\Events\Exception as EventsException; +use Phalcon\Http\RequestInterface; use Phalcon\Http\Response\Exception as ResponseException; use Phalcon\Http\ResponseInterface; use Phalcon\Mvc\Micro; use Phalcon\Mvc\Micro\MiddlewareInterface; +use function str_replace; + /** * @phpstan-import-type TData from ResponderTypes * @phpstan-import-type TErrors from ResponderTypes */ abstract class AbstractMiddleware implements MiddlewareInterface { + /** + * @param RequestInterface $request + * @param EnvManager $env + * + * @return string + */ + public function getBearerTokenFromHeader( + RequestInterface $request, + EnvManager $env + ): string { + /** + * For certain local environments the Authorization header is not + * recognized, as such this is here to allow a custom one through the + * environment + */ + /** @var string $header */ + $header = $env->get('API_HEADER_AUTH', 'Authorization'); + + return str_replace('Bearer ', '', $request->getHeader($header)); + } + + /** + * @param RequestInterface $request + * @param EnvManager $env + * + * @return bool + */ + public function isEmptyBearerToken( + RequestInterface $request, + EnvManager $env + ): bool { + return true === empty($this->getBearerTokenFromHeader($request, $env)); + } + /** * @param Micro $application * @param int $code From 10fa5bfd1fe58243c2fd1ccad3dd6bf96875000b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:57:40 -0500 Subject: [PATCH 111/301] [#.x] - added getToken and strengthened password generation --- tests/AbstractUnitTestCase.php | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 32cd9a0..8b5f766 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -13,13 +13,17 @@ namespace Phalcon\Api\Tests; +use DateTimeImmutable; use Faker\Factory; use PDO; use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\Encryption\Security\JWT\Builder; +use Phalcon\Encryption\Security\JWT\Signer\Hmac; use PHPUnit\Framework\TestCase; abstract class AbstractUnitTestCase extends TestCase @@ -123,7 +127,7 @@ public function getNewUserData(array $fields = []): array return [ 'usr_id' => 0, 'usr_status_flag' => $fields['usr_status_flag'] ?? 1, - 'usr_email' => $fields['usr_email'] ?? $faker->email, + 'usr_email' => $fields['usr_email'] ?? $faker->email(), 'usr_password' => $password, 'usr_name_prefix' => $fields['usr_name_prefix'] ?? $faker->title(), 'usr_name_first' => $fields['usr_name_first'] ?? $faker->firstName(), @@ -148,7 +152,59 @@ public function getNewUserData(array $fields = []): array */ public function getStrongPassword(): string { - return substr(base64_encode(random_bytes(512)), 0, 128); + $password = base64_encode(random_bytes(128)); + $special = '!@#$%^&*()-_=+[]{}|;:,.<>?/~`'; + + $passwordArray = str_split($password); + $specialArray = str_split($special); + + $result = array_merge($passwordArray, $specialArray); + shuffle($result); + + $password = implode('', $result); + + return substr($password, 0, 128); + } + + /** + * @return string + */ + public function getUserToken(array $user): string + { + $audience = $user['audience'] ?? 'https://rest-api.phalcon.io'; + $expires = $user['expires'] ?? '+60 seconds'; + $algo = $user['algo'] ?? 'sha512'; + + $tokenBuilder = new Builder(new Hmac($algo)); + /** @var non-empty-string $issuer */ + $issuer = $user['usr_issuer']; + /** @var non-empty-string $password */ + $password = $user['usr_token_password']; + /** @var non-empty-string $tokenId */ + $tokenId = $user['usr_token_id']; + /** @var non-empty-string $userId */ + $userId = $user['usr_id']; + $now = new DateTimeImmutable(); + /** + * Just in case so that the token nbf and iss are correct + */ + $now = $now->modify('-2 seconds'); + $expiresAt = $now->modify($expires); + + $tokenObject = $tokenBuilder + ->setIssuer($issuer) + ->setAudience($audience) + ->setId($tokenId) + ->setNotBefore($now->getTimestamp()) + ->setIssuedAt($now->getTimestamp()) + ->setExpirationTime($expiresAt->getTimestamp()) + ->setPassphrase($password) + ->addClaim(JWTEnum::UserId->value, $userId) + ->addClaim(JWTEnum::Refresh->value, false) + ->getToken() + ; + + return $tokenObject->getToken(); } public function setUp(): void From 578a23a65e1834355c67d722d78da1d581f3dc2c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:58:04 -0500 Subject: [PATCH 112/301] [#.x] - added imports --- src/Domain/Components/Encryption/Security.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Domain/Components/Encryption/Security.php b/src/Domain/Components/Encryption/Security.php index 1294156..78ae798 100644 --- a/src/Domain/Components/Encryption/Security.php +++ b/src/Domain/Components/Encryption/Security.php @@ -13,6 +13,9 @@ namespace Phalcon\Api\Domain\Components\Encryption; +use function password_hash; +use function password_verify; + use const PASSWORD_ARGON2I; /** From dc5a13241d46a1dd549fa2f37b139d7292ec9813 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:58:22 -0500 Subject: [PATCH 113/301] [#.x] - new session transport properties --- .../DataSource/TransportRepository.php | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/Domain/Components/DataSource/TransportRepository.php b/src/Domain/Components/DataSource/TransportRepository.php index 545b098..a7b43cd 100644 --- a/src/Domain/Components/DataSource/TransportRepository.php +++ b/src/Domain/Components/DataSource/TransportRepository.php @@ -15,12 +15,62 @@ use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Encryption\Security\JWT\Token\Token; /** + * @phpstan-import-type TLoginResponsePayload from UserTypes + * @phpstan-import-type TUserDbRecord from UserTypes * @phpstan-import-type TUserRecord from UserTypes */ -final readonly class TransportRepository +final class TransportRepository { + private ?Token $sessionToken = null; + + private ?UserTransport $sessionUser = null; + + /** + * Returns the session token. + * + * @return Token|null + */ + public function getSessionToken(): ?Token + { + return $this->sessionToken; + } + + /** + * Returns the session user. + * + * @return UserTransport|null + */ + public function getSessionUser(): ?UserTransport + { + return $this->sessionUser; + } + + /** + * @param UserTransport $user + * @param string $token + * + * @return TLoginResponsePayload + */ + public function newLoginUser( + UserTransport $user, + string $token + ): array { + return [ + 'authenticated' => true, + 'user' => [ + 'id' => $user->getId(), + 'name' => $user->getFullName(), + 'email' => $user->getEmail(), + ], + 'jwt' => [ + 'token' => $token, + ], + ]; + } + /** * @param TUserRecord $dbUser * @@ -30,4 +80,28 @@ public function newUser(array $dbUser): UserTransport { return new UserTransport($dbUser); } + + /** + * Sets the session Token + * + * @param Token $token + * + * @return void + */ + public function setSessionToken(Token $token): void + { + $this->sessionToken = $token; + } + + /** + * Populates the session user with the user data. + * + * @param TUserDbRecord $user + * + * @return void + */ + public function setSessionUser(array $user): void + { + $this->sessionUser = $this->newUser($user); + } } From 1dadc3eea28954e920e239855b8fab3a233f471d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 21:58:29 -0500 Subject: [PATCH 114/301] [#.x] - phpstan --- src/Action/ActionHandler.php | 6 ++++++ src/Domain/ADR/DomainInterface.php | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Action/ActionHandler.php b/src/Action/ActionHandler.php index e8f6491..d169ae1 100644 --- a/src/Action/ActionHandler.php +++ b/src/Action/ActionHandler.php @@ -15,10 +15,15 @@ use Phalcon\Api\Domain\ADR\DomainInterface; use Phalcon\Api\Domain\ADR\Input; +use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Http\RequestInterface; use Phalcon\Http\ResponseInterface; +/** + * @phpstan-import-type TLoginInput from InputTypes + * @phpstan-import-type TUserInput from InputTypes + */ final readonly class ActionHandler implements ActionInterface { public function __construct( @@ -32,6 +37,7 @@ public function __construct( public function __invoke(): void { $input = new Input(); + /** @var TLoginInput|TUserInput $data */ $data = $input->__invoke($this->request); $this->responder->__invoke( diff --git a/src/Domain/ADR/DomainInterface.php b/src/Domain/ADR/DomainInterface.php index e06c9c0..52be69d 100644 --- a/src/Domain/ADR/DomainInterface.php +++ b/src/Domain/ADR/DomainInterface.php @@ -16,13 +16,13 @@ use Phalcon\Domain\Payload; /** - * @phpstan-import-type THelloInput from InputTypes + * @phpstan-import-type TLoginInput from InputTypes * @phpstan-import-type TUserInput from InputTypes */ interface DomainInterface { /** - * @param THelloInput|TUserInput $input + * @param TLoginInput|TUserInput $input * * @return Payload */ From b8b37ffa12cbc29c4e62605c29eb6491d6f0573b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 26 Oct 2025 03:02:42 +0000 Subject: [PATCH 115/301] Update src/Domain/Components/DataSource/TransportRepository.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Domain/Components/DataSource/TransportRepository.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Domain/Components/DataSource/TransportRepository.php b/src/Domain/Components/DataSource/TransportRepository.php index a7b43cd..2ccd20e 100644 --- a/src/Domain/Components/DataSource/TransportRepository.php +++ b/src/Domain/Components/DataSource/TransportRepository.php @@ -22,8 +22,13 @@ * @phpstan-import-type TUserDbRecord from UserTypes * @phpstan-import-type TUserRecord from UserTypes */ +/** + * Note: The 'readonly' keyword was intentionally removed from this class. + * Properties $sessionToken and $sessionUser are mutable to support session + * management, allowing updates to the current session state. This change + * removes immutability guarantees, but is necessary for the intended use. + */ final class TransportRepository -{ private ?Token $sessionToken = null; private ?UserTransport $sessionUser = null; From 2dd49da37cbe001999a879807fecb9caa9c47485 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 26 Oct 2025 03:03:09 +0000 Subject: [PATCH 116/301] Update src/Domain/Components/DataSource/User/UserRepository.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Domain/Components/DataSource/User/UserRepository.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index 007929b..8b558d5 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -20,6 +20,8 @@ /** * @phpstan-import-type TUserRecord from UserTypes */ +// The 'final' keyword was intentionally removed from this class to allow extension for testing purposes (e.g., mocking in unit tests). +// Please avoid extending this class in production code unless absolutely necessary. class UserRepository { public function __construct( From a53936dc479e3e4e5e241e3920339f424a2d67d7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 22:03:44 -0500 Subject: [PATCH 117/301] [#.x] - fixed getOptions call --- src/Domain/Components/Env/EnvManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Components/Env/EnvManager.php b/src/Domain/Components/Env/EnvManager.php index 086a40a..94fd405 100644 --- a/src/Domain/Components/Env/EnvManager.php +++ b/src/Domain/Components/Env/EnvManager.php @@ -81,7 +81,7 @@ public function appTimezone(): string public function load(): void { $envFactory = new EnvFactory(); - $options = self::getOptions(); + $options = $this->getOptions(); $adapter = $options['adapter']; $envs = array_merge(getenv(), $_ENV); From 50881213f22c9e5126b92c24242cc2d805e302be Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 22:06:33 -0500 Subject: [PATCH 118/301] [#.x] - phpcs --- src/Domain/Components/DataSource/TransportRepository.php | 4 ++-- src/Domain/Components/DataSource/User/UserRepository.php | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Domain/Components/DataSource/TransportRepository.php b/src/Domain/Components/DataSource/TransportRepository.php index 2ccd20e..870bdf4 100644 --- a/src/Domain/Components/DataSource/TransportRepository.php +++ b/src/Domain/Components/DataSource/TransportRepository.php @@ -21,14 +21,14 @@ * @phpstan-import-type TLoginResponsePayload from UserTypes * @phpstan-import-type TUserDbRecord from UserTypes * @phpstan-import-type TUserRecord from UserTypes - */ -/** + * * Note: The 'readonly' keyword was intentionally removed from this class. * Properties $sessionToken and $sessionUser are mutable to support session * management, allowing updates to the current session state. This change * removes immutability guarantees, but is necessary for the intended use. */ final class TransportRepository +{ private ?Token $sessionToken = null; private ?UserTransport $sessionUser = null; diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index 8b558d5..a841ea0 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -19,9 +19,12 @@ /** * @phpstan-import-type TUserRecord from UserTypes + * + * The 'final' keyword was intentionally removed from this class to allow + * extension for testing purposes (e.g., mocking in unit tests). + * + * Please avoid extending this class in production code unless absolutely necessary. */ -// The 'final' keyword was intentionally removed from this class to allow extension for testing purposes (e.g., mocking in unit tests). -// Please avoid extending this class in production code unless absolutely necessary. class UserRepository { public function __construct( From e91e734df3909d142ec1e9800fa141ae10d41cd5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 22:09:50 -0500 Subject: [PATCH 119/301] [#.x] - correcting .env --- config/.env.ci | 25 +++++++++++++++++++++---- config/.env.example | 29 +++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/config/.env.ci b/config/.env.ci index 09b4fab..ff2fc18 100644 --- a/config/.env.ci +++ b/config/.env.ci @@ -1,5 +1,10 @@ +# Docker prefix PROJECT_NAME="rest" +# Environment +APP_ENV_ADAPTER="dotenv" +APP_ENV_FILE_PATH="./" + # Mariadb DB_HOST="127.0.0.1" DB_PORT=3306 @@ -8,9 +13,21 @@ DB_PASS="secret" DB_NAME="phalcon" DB_CHARSET="utf8" -# Redis -DATA_REDIS_HOST="app-cache" -DATA_REDIS_PORT=6379 -DATA_REDIS_NAME="0" +# Cache +CACHE_ADAPTER="redis" +CACHE_HOST="127.0.0.1" +CACHE_PORT=6379 +CACHE_INDEX="0" +CACHE_LIFETIME=86400 +CACHE_PREFIX="-rest-" + +# Logger +LOG_FILENAME="rest-api" +LOG_PATH="storage/logs" +# Code Coverage XDEBUG_MODE=coverage + +# JWT +TOKEN_EXPIRATION=14400 +TOKEN_AUDIENCE="https://rest-api.phalcon.io" diff --git a/config/.env.example b/config/.env.example index 37fccc8..b0aa5a4 100644 --- a/config/.env.example +++ b/config/.env.example @@ -1,16 +1,33 @@ +# Docker prefix PROJECT_NAME="rest" +# Environment +APP_ENV_ADAPTER="dotenv" +APP_ENV_FILE_PATH="./" + # Mariadb -DB_HOST="app-db" +DB_HOST="rest-db" DB_PORT=3306 DB_USER="root" DB_PASS="secret" DB_NAME="phalcon" -DB_CHARSET= "utf8" +DB_CHARSET="utf8" + +# Cache +CACHE_ADAPTER="redis" +CACHE_HOST="rest-cache" +CACHE_PORT=6379 +CACHE_INDEX="0" +CACHE_LIFETIME=86400 +CACHE_PREFIX="-rest-" -# Redis -DATA_REDIS_HOST="app-cache" -DATA_REDIS_PORT=6379 -DATA_REDIS_NAME="0" +# Logger +LOG_FILENAME="rest-api" +LOG_PATH="storage/logs" +# Code Coverage XDEBUG_MODE=coverage + +# JWT +TOKEN_EXPIRATION=14400 +TOKEN_AUDIENCE="https://rest-api.phalcon.io" From b35db4386d581c8acc74b9d0633b043df1669c90 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 25 Oct 2025 22:12:45 -0500 Subject: [PATCH 120/301] [#.x] - added redis service --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0affd28..300d307 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,6 +93,11 @@ jobs: MYSQL_DATABASE: phalcon MYSQL_PASSWORD: secret + redis: + image: redis:8-alpine + ports: + - "6379:6379" + steps: - uses: actions/checkout@v4 From 897027e98a14c4714b5264b1db4891a299da5e17 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 28 Oct 2025 11:22:40 -0500 Subject: [PATCH 121/301] [#.x] - updating readme --- README.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/README.md b/README.md index 65bea64..2f77ef5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,107 @@ # REST API with Phalcon v6 A REST API developed with Phalcon v6 + +The directory structure for this projects follows the recommendations of [pds/skeleton][pds_skeleton] + +# Folders +The folders contain: + +- `bin`: empty for now, we might use it later on +- `config`: .env configuration files for CI and example +- `docs`: documentation (TODO) +- `public`: entry point of the application where `index.php` lives +- `resources`: stores database migrations and docker files for local development +- `src`: source code of the project +- `storage`: various storage data such as application logs +- `tests`: tests + +# Code organization +The application follows the [ADR pattern][adr_pattern] where the application is split into an `Action` layer, the `Domain` layer and a `Responder` layer. + +## Action +The folder (under `src/`) contains the handler that is responsible for receiving data from the Domain and injecting it to the Responder so that the response can be generated. It also collects all the Input supplied by the user and injects it into the Domain for further processing. + +## Domain + +The Domain is organized in folders based on their use. The `Components` folder contains components that are essential to the operation of the application, while the `Services` folder contains classes that map to endpoints of the application + +### Container + +The application uses the `Phalcon\Di\Di` container with minimal components lazy loaded. Each non "core" component is also registered there (i.e. domain services, responder etc.) and all necessary dependencies are injected based on the service definitions. + +Additionally there are two `Providers` that are also registered in the DI container for further functionality. The `ErrorHandlerProvider` which caters for the starting up/shut down of the application and error logging, and the very important `RoutesProvider` which handles registering all the routes that the application serves. + +### Enums + +There are several enumerations present in the application. Those help with common values for tasks. For example the `FlagsEnum` holds the values for the `co_users.usr_status_flag` field. We could certainly introduce a lookup table in the database for "status" and hold the values there, joining it to the `co_users` table with a lookup table. However, this will introduce an extra join in our query which will inevitable reduce performance. Since the `FlagsEnum` can keep the various statuses, we keep everything in code instead of the database. Thorough tests for enumerations ensure that if a change is made in the future, tests will fail, so that database integrity can be kept. + +The `RoutesEnum` holds the various routes of the application. Every route is represented by a specific element in the enumeration and the relevant prefix/suffix are calculated for each endpoint. Also, each endpoint is mapped to a particular service, registered in the DI container, so that the action handler can invoke it when the route is matched. + +Finally, the `RoutesEnum` also holds the middleware array, which defines their execution and the "hook" they will execute in (before/after). + +### Middleware + +There are several middleware registered for this application and they are being executed one after another (order matters) before the action is executed. As a result, the application will stop executing if an error occurs, or if certain validations fail. + +The middleware execution order is defined in the `RoutesEnum`. The available middleware is: + +- [NotFoundMiddleware.php](src/Domain/Components/Middleware/NotFoundMiddleware.php) +- [HealthMiddleware.php](src/Domain/Components/Middleware/HealthMiddleware.php) +- [ValidateTokenClaimsMiddleware.php](src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php) +- [ValidateTokenPresenceMiddleware.php](src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php) +- [ValidateTokenRevokedMiddleware.php](src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php) +- [ValidateTokenStructureMiddleware.php](src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php) +- [ValidateTokenUserMiddleware.php](src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php) + + + +**NotFoundMiddleware** + +Checks if the route has been matched. If not, it will return a `Resource Not Found` payload + + +**HealthMiddleware** + +Invoked when the `/health` endpoint is called and returns a `OK` payload + +**ValidateTokenPresenceMiddleware** + +Checks if a JWT token is present in the `Authorization` header. If not, an error is returned + +**ValidateTokenStructureMiddleware** + +Gets the JWT token and checks if it can be parsed. If not, an error is returned + +**ValidateTokenUserMiddleware** + +Gets the userId from the JWT token, along with other information, and tries to match it with a user in the database. If the user is not found, an error is returned + +**ValidateTokenClaimsMiddleware** + +Checks all the claims of the JWT token to ensure that it validates. For instance, this checks the token validity (expired, not before), the claims, etc. If a validation error happens, then an error is returned. + +**ValidateTokenRevokedMiddleware** + +Checks if the token has been revoked. If it has, an error is returned + + +## Responder +The responder is responsible for constructing the response with the desired output, and emitting it back to the caller. For the moment we have only implemented a JSON response with a specified array as the payload to be sent back. + +The responder receives the outcome of the Domain, by means of a `Payload` object. The object contains all the data necessary to inject in the response. + +### Response payload + +The application responds always with a specific JSON payload. The payload contains the following nodes: +- `data` - contains any data that are returned back (can be empty) +- `errors` - contains any errors occurred (can be empty) +- `meta` - array of information regarding the payload + - `code` - the application code returned + - `hash` - a `sha1` hash of the `data`, `errors` and timestamp + - `message` - `success` or `error` + - `timestamp` - the time in UTC format + + +[adr_pattern]: https://github.com/pmjones/adr +[pds_skeleton]: https://github.com/php-pds/skeleton From 4344bf30467ec66b13d9c57d7416ca305e171bc3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:46:18 -0600 Subject: [PATCH 122/301] [#.x] - new routes for user crud and auth --- .../Components/Enums/Http/RoutesEnum.php | 67 +++++++++++++---- .../Components/Enums/Http/RoutesEnumTest.php | 74 +++++++++++++++++-- .../Providers/RouterProviderTest.php | 20 +++++ 3 files changed, 142 insertions(+), 19 deletions(-) diff --git a/src/Domain/Components/Enums/Http/RoutesEnum.php b/src/Domain/Components/Enums/Http/RoutesEnum.php index 3c6fcac..e28552c 100644 --- a/src/Domain/Components/Enums/Http/RoutesEnum.php +++ b/src/Domain/Components/Enums/Http/RoutesEnum.php @@ -20,17 +20,29 @@ /** * @phpstan-type TMiddleware array */ -enum RoutesEnum: string +enum RoutesEnum: int { - public const DELETE = 'delete'; + /** + * Methods + */ + public const DELETE = 'delete'; + /** + * Events + */ public const EVENT_BEFORE = 'before'; public const EVENT_FINISH = 'finish'; - public const GET = 'get'; - public const POST = 'post'; - public const PUT = 'put'; + public const GET = 'get'; + public const POST = 'post'; + public const PUT = 'put'; + + case authLoginPost = 11; + case authLogoutPost = 12; + case authRefreshPost = 13; - case authLoginPost = 'auth/login'; - case userGet = 'user'; + case userDelete = 21; + case userGet = 22; + case userPost = 23; + case userPut = 24; /** * @return string @@ -46,8 +58,13 @@ public function endpoint(): string public function method(): string { return match ($this) { - self::authLoginPost => self::POST, - self::userGet => self::GET, + self::authLoginPost, + self::authLogoutPost, + self::authRefreshPost, + self::userPost => self::POST, + self::userDelete => self::DELETE, + self::userGet => self::GET, + self::userPut => self::PUT, }; } @@ -63,6 +80,7 @@ public static function middleware(): array Container::MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE => self::EVENT_BEFORE, Container::MIDDLEWARE_VALIDATE_TOKEN_USER => self::EVENT_BEFORE, Container::MIDDLEWARE_VALIDATE_TOKEN_CLAIMS => self::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_REVOKED => self::EVENT_BEFORE, ]; } @@ -71,14 +89,29 @@ public static function middleware(): array */ public function prefix(): string { - return '/' . str_replace('-', '/', $this->value); + $endpoint = match ($this) { + self::authLoginPost, + self::authLogoutPost, + self::authRefreshPost => 'auth', + self::userDelete, + self::userGet, + self::userPost, + self::userPut => 'user', + }; + + return '/' . str_replace('-', '/', $endpoint); } public function service(): string { return match ($this) { - self::authLoginPost => Container::AUTH_LOGIN_POST_SERVICE, - self::userGet => Container::USER_GET_SERVICE, + self::authLoginPost => Container::AUTH_LOGIN_POST_SERVICE, + self::authLogoutPost => Container::AUTH_LOGOUT_POST_SERVICE, + self::authRefreshPost => Container::AUTH_REFRESH_POST_SERVICE, + self::userDelete => Container::USER_DELETE_SERVICE, + self::userGet => Container::USER_GET_SERVICE, + self::userPost => Container::USER_POST_SERVICE, + self::userPut => Container::USER_PUT_SERVICE, }; } @@ -87,6 +120,14 @@ public function service(): string */ public function suffix(): string { - return ''; + return match ($this) { + self::authLoginPost => '/login', + self::authLogoutPost => '/logout', + self::authRefreshPost => '/refresh', + self::userDelete, + self::userGet, + self::userPost, + self::userPut => '', + }; } } diff --git a/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php b/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php index c8057ea..42592d4 100644 --- a/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php +++ b/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php @@ -25,24 +25,66 @@ public static function getExamples(): array return [ [ RoutesEnum::authLoginPost, - 'auth/login', + '/auth', + '/login', '/auth/login', RoutesEnum::POST, Container::AUTH_LOGIN_POST_SERVICE, ], + [ + RoutesEnum::authLogoutPost, + '/auth', + '/logout', + '/auth/logout', + RoutesEnum::POST, + Container::AUTH_LOGOUT_POST_SERVICE, + ], + [ + RoutesEnum::authRefreshPost, + '/auth', + '/refresh', + '/auth/refresh', + RoutesEnum::POST, + Container::AUTH_REFRESH_POST_SERVICE, + ], + [ + RoutesEnum::userDelete, + '/user', + '', + '/user', + RoutesEnum::DELETE, + Container::USER_DELETE_SERVICE, + ], [ RoutesEnum::userGet, - 'user', + '/user', + '', '/user', RoutesEnum::GET, Container::USER_GET_SERVICE, ], + [ + RoutesEnum::userPost, + '/user', + '', + '/user', + RoutesEnum::POST, + Container::USER_POST_SERVICE, + ], + [ + RoutesEnum::userPut, + '/user', + '', + '/user', + RoutesEnum::PUT, + Container::USER_PUT_SERVICE, + ], ]; } public function testCheckCount(): void { - $expected = 2; + $expected = 7; $actual = RoutesEnum::cases(); $this->assertCount($expected, $actual); } @@ -50,13 +92,18 @@ public function testCheckCount(): void #[DataProvider('getExamples')] public function testCheckItems( RoutesEnum $element, - string $value, + string $prefix, + string $suffix, string $endpoint, string $method, string $service ) { - $expected = $value; - $actual = $element->value; + $expected = $prefix; + $actual = $element->prefix(); + $this->assertSame($expected, $actual); + + $expected = $suffix; + $actual = $element->suffix(); $this->assertSame($expected, $actual); $expected = $endpoint; @@ -71,4 +118,19 @@ public function testCheckItems( $actual = $element->service(); $this->assertSame($expected, $actual); } + + public function testMiddleware(): void + { + $expected = [ + Container::MIDDLEWARE_NOT_FOUND => RoutesEnum::EVENT_BEFORE, + Container::MIDDLEWARE_HEALTH => RoutesEnum::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_PRESENCE => RoutesEnum::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE => RoutesEnum::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_USER => RoutesEnum::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_CLAIMS => RoutesEnum::EVENT_BEFORE, + Container::MIDDLEWARE_VALIDATE_TOKEN_REVOKED => RoutesEnum::EVENT_BEFORE, + ]; + $actual = RoutesEnum::middleware(); + $this->assertSame($expected, $actual); + } } diff --git a/tests/Unit/Domain/Components/Providers/RouterProviderTest.php b/tests/Unit/Domain/Components/Providers/RouterProviderTest.php index 28fa717..0429c1b 100644 --- a/tests/Unit/Domain/Components/Providers/RouterProviderTest.php +++ b/tests/Unit/Domain/Components/Providers/RouterProviderTest.php @@ -36,10 +36,30 @@ public function testCheckRegistration(): void 'method' => 'POST', 'pattern' => '/auth/login', ], + [ + 'method' => 'POST', + 'pattern' => '/auth/logout', + ], + [ + 'method' => 'POST', + 'pattern' => '/auth/refresh', + ], + [ + 'method' => 'DELETE', + 'pattern' => '/user', + ], [ 'method' => 'GET', 'pattern' => '/user', ], + [ + 'method' => 'POST', + 'pattern' => '/user', + ], + [ + 'method' => 'PUT', + 'pattern' => '/user', + ], [ 'method' => 'GET', 'pattern' => '/health', From c2aeec38456187b0114ef71d9594d104420c2869 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:46:36 -0600 Subject: [PATCH 123/301] [#.x] - new validate token revoked middleware --- .../ValidateTokenRevokedMiddleware.php | 67 ++++++++++ .../ValidateTokenRevokedMiddlewareTest.php | 119 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php create mode 100644 tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php diff --git a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php new file mode 100644 index 0000000..81fddca --- /dev/null +++ b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php @@ -0,0 +1,67 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Http\RequestInterface; +use Phalcon\Mvc\Micro; + +final class ValidateTokenRevokedMiddleware extends AbstractMiddleware +{ + /** + * @param Micro $application + * + * @return bool + */ + public function call(Micro $application): bool + { + /** @var RequestInterface $request */ + $request = $application->getSharedService(Container::REQUEST); + /** @var Cache $cache */ + $cache = $application->getSharedService(Container::CACHE); + /** @var EnvManager $env */ + $env = $application->getSharedService(Container::ENV); + /** @var TransportRepository $userTransport */ + $userTransport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + + /** @var UserTransport $user */ + $user = $userTransport->getSessionUser(); + + /** + * Get the token object + */ + $token = $this->getBearerTokenFromHeader($request, $env); + $cacheKey = $cache->getCacheTokenKey($user, $token); + $exists = $cache->has($cacheKey); + + if (true !== $exists) { + $this->halt( + $application, + HttpCodesEnum::Unauthorized->value, + HttpCodesEnum::Unauthorized->text(), + [], + [HttpCodesEnum::AppTokenNotValid->error()] + ); + + return false; + } + + return true; + } +} diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php new file mode 100644 index 0000000..6c65fdd --- /dev/null +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php @@ -0,0 +1,119 @@ + + * + * 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\Components\Middleware; + +use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Mvc\Micro; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class ValidateTokenRevokedMiddlewareTest extends AbstractUnitTestCase +{ + public function testValidateTokenRevokedFailureInvalidToken(): void + { + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $user; + + [$micro, $middleware] = $this->setupTest(); + + $token = $this->getUserToken($tokenUser); + + /** + * Store the user in the session + */ + /** @var UserTransport $userRepository */ + $userRepository = $micro->getSharedService(Container::REPOSITORY_TRANSPORT); + $userRepository->setSessionUser($user); + + // There is no entry in the cache for this token, so this should fail. + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => '/user?id=1234', + ]; + + ob_start(); + $middleware->call($micro); + $contents = ob_get_clean(); + + $contents = json_decode($contents, true); + + $data = $contents['data']; + $errors = $contents['errors']; + + $this->assertSame([], $data); + + $expected = [HttpCodesEnum::AppTokenNotValid->error()]; + $this->assertSame($expected, $errors); + } + + public function testValidateTokenRevokedSuccess(): void + { + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $user; + + [$micro, $middleware] = $this->setupTest(); + + $token = $this->getUserToken($tokenUser); + + /** + * Store the user in the session + */ + /** @var UserTransport $userRepository */ + $userRepository = $micro->getSharedService(Container::REPOSITORY_TRANSPORT); + $userRepository->setSessionUser($user); + /** @var Cache $cache */ + $cache = $micro->getSharedService(Container::CACHE); + $sessionUser = $userRepository->getSessionUser(); + $cacheKey = $cache->getCacheTokenKey($sessionUser, $token); + $payload = [ + 'token' => $token, + ]; + $actual = $cache->set($cacheKey, $payload, 2); + $this->assertTrue($actual); + + // There is no entry in the cache for this token, so this should fail. + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => '/user?id=1234', + ]; + + $contents = $middleware->call($micro); + + $this->assertTrue($contents); + } + + /** + * @return array + */ + private function setupTest(): array + { + $micro = new Micro($this->container); + $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_REVOKED); + + return [$micro, $middleware]; + } +} From 3ea800dedd384536a51eae86d89aec44f59b8dcf Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:47:17 -0600 Subject: [PATCH 124/301] [#.x] - docblocks and minor corrections --- tests/AbstractUnitTestCase.php | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 8b5f766..23fbede 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -26,6 +26,10 @@ use Phalcon\Encryption\Security\JWT\Signer\Hmac; use PHPUnit\Framework\TestCase; +use function base64_encode; +use function random_bytes; +use function substr; + abstract class AbstractUnitTestCase extends TestCase { /** @@ -50,6 +54,12 @@ public function assertFileContentsContains(string $fileName, string $stream): vo $this->assertStringContainsString($stream, $contents); } + /** + * @param string $table + * @param array $criteria + * + * @return void + */ public function assertInDatabase(string $table, array $criteria = []): void { $records = $this->getFromDatabase($table, $criteria); @@ -57,6 +67,12 @@ public function assertInDatabase(string $table, array $criteria = []): void $this->assertNotEmpty($records); } + /** + * @param string $table + * @param array $criteria + * + * @return void + */ public function assertNotInDatabase(string $table, array $criteria = []): void { $records = $this->getFromDatabase($table, $criteria); @@ -64,6 +80,11 @@ public function assertNotInDatabase(string $table, array $criteria = []): void $this->assertEmpty($records); } + /** + * @param Connection $connection + * + * @return void + */ public function getConnection(): Connection { if (null === $this->connection) { @@ -115,12 +136,17 @@ public function getNewUser(UsersMigration $migration, array $fields = []): array return $dbUser[0]; } + /** + * @param array $fields + * + * @return array + */ public function getNewUserData(array $fields = []): array { $faker = Factory::create(); $password = $fields['usr_password'] ?? $this->getStrongPassword(); /** @var Security $security */ - $security = $this->container->get(Container::SECURITY); + $security = $this->container->getShared(Container::SECURITY); $password = $security->hash($password); @@ -235,10 +261,10 @@ protected function getFromDatabase( if (!empty($where)) { $sql .= ' WHERE ' . implode(' AND ', $where); } - $stmt = $this->connection?->prepare($sql); - $stmt?->execute($params); - $records = $stmt?->fetchAll(PDO::FETCH_ASSOC); - return $records; + $stmt = $this->getConnection()->prepare($sql); + $stmt->execute($params); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); } } From 1649bac20539d62950f18e2804a21e80af0d29af Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:47:45 -0600 Subject: [PATCH 125/301] [#.x] - adding support for refresh token --- .../DataSource/TransportRepository.php | 7 ++- src/Domain/Components/Encryption/JWTToken.php | 63 ++++++++++++------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/Domain/Components/DataSource/TransportRepository.php b/src/Domain/Components/DataSource/TransportRepository.php index 870bdf4..c7edf02 100644 --- a/src/Domain/Components/DataSource/TransportRepository.php +++ b/src/Domain/Components/DataSource/TransportRepository.php @@ -56,12 +56,14 @@ public function getSessionUser(): ?UserTransport /** * @param UserTransport $user * @param string $token + * @param string $refreshToken * * @return TLoginResponsePayload */ public function newLoginUser( UserTransport $user, - string $token + string $token, + string $refreshToken ): array { return [ 'authenticated' => true, @@ -71,7 +73,8 @@ public function newLoginUser( 'email' => $user->getEmail(), ], 'jwt' => [ - 'token' => $token, + 'token' => $token, + 'refreshToken' => $refreshToken, ], ]; } diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Components/Encryption/JWTToken.php index 3134e4a..c4c067b 100644 --- a/src/Domain/Components/Encryption/JWTToken.php +++ b/src/Domain/Components/Encryption/JWTToken.php @@ -32,10 +32,13 @@ /** * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TUserTokenDbRecord from UserTypes * @phpstan-type TValidatorErrors array{}|array + * + * Removed the final declaration so that this class can be mocked. This + * class should not be extended */ -final class JWTToken +class JWTToken { /** * @var Parser|null @@ -50,23 +53,13 @@ public function __construct( /** * Returns the string token * - * @param TUserDbRecord $user + * @param TUserTokenDbRecord $user * * @return string */ public function getForUser(array $user): string { - /** @var int $expiration */ - $expiration = $this->env->get( - 'TOKEN_EXPIRATION', - Cache::CACHE_TOKEN_EXPIRY, - 'int' - ); - - $now = new DateTimeImmutable(); - $expiresAt = $now->modify('+' . $expiration . ' seconds'); - - return $this->generateTokenForUser($user, $now, $expiresAt); + return $this->generateTokenForUser($user); } /** @@ -91,6 +84,18 @@ public function getObject(string $token): Token return $tokenObject; } + /** + * Returns the string token + * + * @param TUserTokenDbRecord $user + * + * @return string + */ + public function getRefreshForUser(array $user): string + { + return $this->generateTokenForUser($user, true); + } + /** * @param QueryRepository $repository * @param Token $token @@ -122,6 +127,8 @@ public function getUser( } /** + * Returns an array with the validation errors for this token + * * @param Token $tokenObject * @param UserTransport $user * @@ -153,19 +160,31 @@ public function validate( } /** - * @param TUserDbRecord $user - * @param DateTimeImmutable $now - * @param DateTimeImmutable $expiresAt - * @param bool $isRefresh + * Returns the string token + * + * @param TUserTokenDbRecord $user + * @param bool $isRefresh * * @return string */ private function generateTokenForUser( array $user, - DateTimeImmutable $now, - DateTimeImmutable $expiresAt, - bool $isRefresh = true + bool $isRefresh = false ): string { + /** @var int $expiration */ + $expiration = $this->env->get( + 'TOKEN_EXPIRATION', + Cache::CACHE_TOKEN_EXPIRY, + 'int' + ); + + $now = new DateTimeImmutable(); + $expiresAt = $now->modify('+' . $expiration . ' seconds'); + /** + * This is to ensure that the token is valid the minute we issue it + */ + $now = $now->modify('-1 second'); + $tokenBuilder = new Builder(new Hmac()); /** @var string $issuer */ $issuer = $user['usr_issuer']; @@ -194,6 +213,8 @@ private function generateTokenForUser( /** + * Returns the default audience for the tokens + * * @return string */ private function getTokenAudience(): string From aa12d78b2cd411eb24f3f297dea72194640afb95 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:48:15 -0600 Subject: [PATCH 126/301] [#.x] - Changing exception throwing --- src/Domain/Components/Env/Adapters/DotEnv.php | 2 +- src/Domain/Components/Env/EnvFactory.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Domain/Components/Env/Adapters/DotEnv.php b/src/Domain/Components/Env/Adapters/DotEnv.php index 4793353..0133a82 100644 --- a/src/Domain/Components/Env/Adapters/DotEnv.php +++ b/src/Domain/Components/Env/Adapters/DotEnv.php @@ -35,7 +35,7 @@ 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( + throw InvalidConfigurationArgumentException::new( 'The .env file does not exist at the specified path: ' . (string)$filePath ); diff --git a/src/Domain/Components/Env/EnvFactory.php b/src/Domain/Components/Env/EnvFactory.php index cb000ec..4ce3ba1 100644 --- a/src/Domain/Components/Env/EnvFactory.php +++ b/src/Domain/Components/Env/EnvFactory.php @@ -29,7 +29,7 @@ public function newInstance(string $name, mixed ...$parameters): AdapterInterfac $adapters = $this->getAdapters(); if (true !== isset($this->instances[$name])) { if (true !== isset($adapters[$name])) { - throw new InvalidConfigurationArgumentException( + throw InvalidConfigurationArgumentException::new( 'Service ' . $name . ' is not registered' ); } From 962f02f92bd03af54d1d46ea78a2af933d0fc25e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:48:36 -0600 Subject: [PATCH 127/301] [#.x] - minor corrections --- .../Components/Middleware/ValidateTokenUserMiddleware.php | 2 +- .../Middleware/ValidateTokenClaimsMiddlewareTest.php | 4 ++-- .../Middleware/ValidateTokenPresenceMiddlewareTest.php | 6 +++--- .../Middleware/ValidateTokenStructureMiddlewareTest.php | 6 +++--- .../Middleware/ValidateTokenUserMiddlewareTest.php | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php index ce2ea63..9720a05 100644 --- a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php @@ -52,7 +52,7 @@ public function call(Micro $application): bool /** @var Token $tokenObject */ $tokenObject = $transport->getSessionToken(); /** @var TUserRecord $dbUser */ - $dbUser = $jwtToken->getUser($repository, $tokenObject); + $dbUser = $jwtToken->getUser($repository, $tokenObject); if (true === empty($dbUser)) { $this->halt( diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php index 5535a13..453701a 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php @@ -93,7 +93,7 @@ public function testValidateTokenClaimsFailure( 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); @@ -135,7 +135,7 @@ public function testValidateTokenClaimsSuccess(): void 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php index 1bea4b3..bbc0517 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php @@ -34,7 +34,7 @@ public function testValidateTokenPresenceFailure(): void $_SERVER = [ 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); @@ -63,7 +63,7 @@ public function testValidateTokenPresenceSuccess(): void $_SERVER = [ 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); @@ -79,7 +79,7 @@ public function testValidateTokenPresenceSuccess(): void 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, 'HTTP_AUTHORIZATION' => 'Bearer 123.456.789', - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php index 7eece51..539aa48 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php @@ -31,7 +31,7 @@ public function testValidateTokenStructureFailureBadSignature(): void 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, 'HTTP_AUTHORIZATION' => 'Bearer abcd.abcd.abcd', - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); @@ -60,7 +60,7 @@ public function testValidateTokenStructureFailureNoDots(): void 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, 'HTTP_AUTHORIZATION' => 'Bearer abcd.abcd', - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); @@ -94,7 +94,7 @@ public function testValidateTokenStructureSuccess(): void 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php index d991e2e..827d519 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php @@ -38,7 +38,7 @@ public function testValidateTokenUserFailureRecordNotFound(): void 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); @@ -73,7 +73,7 @@ public function testValidateTokenUserSuccess(): void 'REQUEST_METHOD' => 'GET', 'REQUEST_TIME_FLOAT' => $time, 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, - 'REQUEST_URI' => '/user?userId=1234', + 'REQUEST_URI' => '/user?id=1234', ]; ob_start(); From 0774788c78602d4b3dbae09aba0f4584f9c9536e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:48:50 -0600 Subject: [PATCH 128/301] [#.x] - new types for users/input --- src/Domain/ADR/InputTypes.php | 67 ++++++++++++++++++- .../Components/DataSource/User/UserTypes.php | 47 ++++++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/Domain/ADR/InputTypes.php b/src/Domain/ADR/InputTypes.php index 40b8806..b68da4d 100644 --- a/src/Domain/ADR/InputTypes.php +++ b/src/Domain/ADR/InputTypes.php @@ -18,10 +18,75 @@ * email?: string, * password?: string * } + * + * @phpstan-type TLogoutInput array{ + * token?: string + * } + * + * @phpstan-type TRefreshInput array{ + * token?: string + * } + * * @phpstan-type TUserInput array{ - * userId?: int + * id?: int, + * status?: int, + * email?: string, + * password?: string, + * namePrefix?: string, + * nameFirst?: string, + * nameMiddle?: string, + * nameLast?: string, + * nameSuffix?: string, + * issuer?: string, + * tokenPassword?: string, + * tokenId?: string, + * preferences?: string, + * createdDate?: string, + * createdUserId?: int, + * updatedDate?: string, + * updatedUserId?: int, * } + * + * @phpstan-type TUserSanitizedInsertInput array{ + * status: int, + * email: string, + * password: string, + * namePrefix: string, + * nameFirst: string, + * nameMiddle: string, + * nameLast: string, + * nameSuffix: string, + * issuer: string, + * tokenPassword: string, + * tokenId: string, + * preferences: string, + * createdDate: string, + * createdUserId: int, + * updatedDate: string, + * updatedUserId: int, + * } + * + * @phpstan-type TUserSanitizedUpdateInput array{ + * id: int, + * status: int, + * email: string, + * password: string, + * namePrefix: string, + * nameFirst: string, + * nameMiddle: string, + * nameLast: string, + * nameSuffix: string, + * issuer: string, + * tokenPassword: string, + * tokenId: string, + * preferences: string, + * updatedDate: string, + * updatedUserId: int, + * } + * * @phpstan-type TRequestQuery array + * + * @phpstan-type TValidationErrors array> */ final class InputTypes { diff --git a/src/Domain/Components/DataSource/User/UserTypes.php b/src/Domain/Components/DataSource/User/UserTypes.php index a9825b3..d42f2e4 100644 --- a/src/Domain/Components/DataSource/User/UserTypes.php +++ b/src/Domain/Components/DataSource/User/UserTypes.php @@ -22,7 +22,8 @@ * email: string * }, * jwt: array{ - * token: string + * token: string, + * refreshToken: string, * } * } * @@ -46,6 +47,13 @@ * usr_updated_usr_id: int, * } * + * @phpstan-type TUserTokenDbRecord array{ + * usr_id: int, + * usr_issuer: string, + * usr_token_password: string, + * usr_token_id: string + * } + * * @phpstan-type TUserRecord array{}|TUserDbRecord * * @phpstan-type TUserTransport array{ @@ -67,6 +75,43 @@ * updatedDate: string, * updatedUserId: int, * } + * + * @phpstan-type TUserInsert array{ + * status: int, + * email: string, + * password: string, + * namePrefix: string, + * nameFirst: string, + * nameMiddle: string, + * nameLast: string, + * nameSuffix: string, + * issuer: string, + * tokenPassword: string, + * tokenId: string, + * preferences: string, + * createdDate: string, + * createdUserId: int, + * updatedDate: string, + * updatedUserId: int, + * } + * + * @phpstan-type TUserUpdate array{ + * id: int, + * status: int, + * email: string, + * password: string, + * namePrefix: string, + * nameFirst: string, + * nameMiddle: string, + * nameLast: string, + * nameSuffix: string, + * issuer: string, + * tokenPassword: string, + * tokenId: string, + * preferences: string, + * updatedDate: string, + * updatedUserId: int, + * } */ final class UserTypes { From 5cec63803d977c46ba0352adcdaf1166f308d5da Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:49:40 -0600 Subject: [PATCH 129/301] [#.x] - refactoring constructor --- .../Services/Auth/AbstractAuthService.php | 61 +++++++++++++++++++ src/Domain/Services/Auth/LoginPostService.php | 56 +++++------------ .../Services/Auth/LoginPostServiceTest.php | 4 +- 3 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 src/Domain/Services/Auth/AbstractAuthService.php diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Services/Auth/AbstractAuthService.php new file mode 100644 index 0000000..3094540 --- /dev/null +++ b/src/Domain/Services/Auth/AbstractAuthService.php @@ -0,0 +1,61 @@ + + * + * 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\Auth; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Domain\Payload; +use Phalcon\Filter\Filter; + +/** + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TLoginInput from InputTypes + * @phpstan-import-type TValidationErrors from InputTypes + */ +abstract class AbstractAuthService implements DomainInterface +{ + public function __construct( + protected readonly QueryRepository $repository, + protected readonly TransportRepository $transport, + protected readonly Cache $cache, + protected readonly EnvManager $env, + protected readonly JWTToken $jwtToken, + protected readonly Filter $filter, + protected readonly Security $security, + ) { + } + + /** + * @param TValidationErrors $errors + * + * @return Payload + */ + protected function getUnauthorizedPayload(array $errors): Payload + { + return new Payload( + DomainStatus::UNAUTHORIZED, + [ + 'errors' => $errors, + ] + ); + } +} diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Services/Auth/LoginPostService.php index 306005f..d6f9a4f 100644 --- a/src/Domain/Services/Auth/LoginPostService.php +++ b/src/Domain/Services/Auth/LoginPostService.php @@ -14,36 +14,17 @@ namespace Phalcon\Api\Domain\Services\Auth; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\ADR\DomainInterface; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Domain\Payload; -use Phalcon\Filter\Filter; /** * @phpstan-import-type TUserDbRecord from UserTypes * @phpstan-import-type TLoginInput from InputTypes */ -final readonly class LoginPostService implements DomainInterface +final class LoginPostService extends AbstractAuthService { - public function __construct( - private QueryRepository $repository, - private TransportRepository $transport, - private Cache $cache, - private EnvManager $env, - private JWTToken $jwtToken, - private Filter $filter, - private Security $security, - ) { - } - /** * @param TLoginInput $input * @@ -63,7 +44,9 @@ public function __invoke(array $input): Payload * Check if email or password are empty */ if (true === empty($email) || true === empty($password)) { - return $this->getUnauthorizedPayload(); + return $this->getUnauthorizedPayload( + [HttpCodesEnum::AppIncorrectCredentials->error()] + ); } /** @@ -80,24 +63,30 @@ public function __invoke(array $input): Payload $dbUserId < 1 || true !== $this->security->verify($password, $dbPassword) ) { - return $this->getUnauthorizedPayload(); + return $this->getUnauthorizedPayload( + [HttpCodesEnum::AppIncorrectCredentials->error()] + ); } /** * Get a new token for this user */ - /** @var TUserDbRecord $dbUser $token */ - $token = $this->jwtToken->getForUser($dbUser); - $domainUser = $this->transport->newUser($dbUser); - $results = $this->transport->newLoginUser( + /** @var TUserDbRecord $dbUser */ + $token = $this->jwtToken->getForUser($dbUser); + $refreshToken = $this->jwtToken->getRefreshForUser($dbUser); + $domainUser = $this->transport->newUser($dbUser); + $results = $this->transport->newLoginUser( $domainUser, $token, + $refreshToken ); /** * Store the token in cache */ $this->cache->storeTokenInCache($this->env, $domainUser, $token); + $this->cache->storeTokenInCache($this->env, $domainUser, $refreshToken); + /** * Send the payload back */ @@ -108,19 +97,4 @@ public function __invoke(array $input): Payload ] ); } - - /** - * @return Payload - */ - private function getUnauthorizedPayload(): Payload - { - return new Payload( - DomainStatus::UNAUTHORIZED, - [ - 'errors' => [ - HttpCodesEnum::AppIncorrectCredentials->error(), - ], - ] - ); - } } diff --git a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php index c37c0b9..2049fb1 100644 --- a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php @@ -103,10 +103,10 @@ public function testServiceWithCredentials(): void $this->assertSame($expected, $actual); $this->assertArrayHasKey('token', $jwt); -// $this->assertArrayHasKey('refreshToken', $jwt); + $this->assertArrayHasKey('refreshToken', $jwt); $this->assertNotEmpty($jwt['token']); -// $this->assertNotEmpty($jwt['refreshToken']); + $this->assertNotEmpty($jwt['refreshToken']); } public function testServiceWrongCredentials(): void From 3dd2c5b9707eab4333207d25e1411e8080993d11 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:49:53 -0600 Subject: [PATCH 130/301] [#.x] - added put in the vars --- src/Domain/ADR/Input.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Domain/ADR/Input.php b/src/Domain/ADR/Input.php index ed98253..079057e 100644 --- a/src/Domain/ADR/Input.php +++ b/src/Domain/ADR/Input.php @@ -33,7 +33,9 @@ public function __invoke(RequestInterface $request): array $query = $request->getQuery(); /** @var TRequestQuery $post */ $post = $request->getPost(); + /** @var TRequestQuery $put */ + $put = $request->getPut(); - return array_merge($query, $post); + return array_merge($query, $post, $put); } } From a50c0d08432564c359ef4ff589a5c3bbfb52ce51 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:50:21 -0600 Subject: [PATCH 131/301] [#.x] - abstracting constructor --- .../Services/User/AbstractUserService.php | 31 +++++++++++++++++++ src/Domain/Services/User/UserGetService.php | 15 ++------- 2 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 src/Domain/Services/User/AbstractUserService.php diff --git a/src/Domain/Services/User/AbstractUserService.php b/src/Domain/Services/User/AbstractUserService.php new file mode 100644 index 0000000..f6d5c84 --- /dev/null +++ b/src/Domain/Services/User/AbstractUserService.php @@ -0,0 +1,31 @@ + + * + * 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\User; + +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Filter\Filter; + +abstract class AbstractUserService implements DomainInterface +{ + public function __construct( + protected readonly QueryRepository $repository, + protected readonly TransportRepository $transport, + protected readonly Filter $filter, + protected readonly Security $security, + ) { + } +} diff --git a/src/Domain/Services/User/UserGetService.php b/src/Domain/Services/User/UserGetService.php index cb79dfa..c71735b 100644 --- a/src/Domain/Services/User/UserGetService.php +++ b/src/Domain/Services/User/UserGetService.php @@ -14,25 +14,14 @@ namespace Phalcon\Api\Domain\Services\User; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\ADR\DomainInterface; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; use Phalcon\Domain\Payload; -use Phalcon\Filter\Filter; /** * @phpstan-import-type TUserInput from InputTypes */ -final readonly class UserGetService implements DomainInterface +final class UserGetService extends AbstractUserService { - public function __construct( - private QueryRepository $repository, - private TransportRepository $transport, - private Filter $filter - ) { - } - /** * @param TUserInput $input * @@ -40,7 +29,7 @@ public function __construct( */ public function __invoke(array $input): Payload { - $userId = $this->filter->absint($input['userId'] ?? 0); + $userId = $this->filter->absint($input['id'] ?? 0); /** * Success From 2ba6955f90f3b37182ad203e008d850024583f4f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:50:44 -0600 Subject: [PATCH 132/301] [#.x] - abstracting constructor and common methods --- .../DataSource/AbstractRepository.php | 110 +++++++++++++++++ .../DataSource/User/UserRepository.php | 113 +++++++++++++----- 2 files changed, 195 insertions(+), 28 deletions(-) create mode 100644 src/Domain/Components/DataSource/AbstractRepository.php diff --git a/src/Domain/Components/DataSource/AbstractRepository.php b/src/Domain/Components/DataSource/AbstractRepository.php new file mode 100644 index 0000000..34c6314 --- /dev/null +++ b/src/Domain/Components/DataSource/AbstractRepository.php @@ -0,0 +1,110 @@ + + * + * 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\Components\DataSource; + +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\DataMapper\Query\Delete; +use Phalcon\DataMapper\Query\Select; + +/** + * @phpstan-import-type TUserRecord from UserTypes + */ +abstract class AbstractRepository +{ + /** + * @var string + */ + protected string $idField = ''; + /** + * @var string + */ + protected string $table = ''; + + public function __construct( + protected readonly Connection $connection, + ) { + } + + /** + * @param array $criteria + * + * @return int + */ + public function deleteBy(array $criteria): int + { + $delete = Delete::new($this->connection); + + $statement = $delete + ->table($this->table) + ->whereEquals($criteria) + ->perform() + ; + + return $statement->rowCount(); + } + + /** + * @param int $recordId + * + * @return int + */ + public function deleteById(int $recordId): int + { + return $this->deleteBy( + [ + $this->idField => $recordId, + ] + ); + } + + /** + * @param int $recordId + * + * @return TUserRecord + */ + public function findById(int $recordId): array + { + $result = []; + if ($recordId > 0) { + return $this->findOneBy( + [ + $this->idField => $recordId, + ] + ); + } + + return $result; + } + + + /** + * @param array $criteria + * + * @return TUserRecord + */ + public function findOneBy(array $criteria): array + { + $select = Select::new($this->connection); + + /** @var TUserRecord $result */ + $result = $select + ->from($this->table) + ->whereEquals($criteria) + ->fetchOne() + ; + + return $result; + } +} diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index a841ea0..8ddc986 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -13,24 +13,31 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; +use Phalcon\Api\Domain\Components\DataSource\AbstractRepository; use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; -use Phalcon\DataMapper\Pdo\Connection; -use Phalcon\DataMapper\Query\Select; +use Phalcon\DataMapper\Query\Insert; +use Phalcon\DataMapper\Query\Update; /** * @phpstan-import-type TUserRecord from UserTypes + * @phpstan-import-type TUserInsert from UserTypes + * @phpstan-import-type TUserUpdate from UserTypes * * The 'final' keyword was intentionally removed from this class to allow * extension for testing purposes (e.g., mocking in unit tests). * * Please avoid extending this class in production code unless absolutely necessary. */ -class UserRepository +class UserRepository extends AbstractRepository { - public function __construct( - private readonly Connection $connection, - ) { - } + /** + * @var string + */ + protected string $idField = 'usr_id'; + /** + * @var string + */ + protected string $table = 'co_users'; /** * @param string $email @@ -53,40 +60,90 @@ public function findByEmail(string $email): array } /** - * @param int $userId + * @param TUserInsert $userData * - * @return TUserRecord + * @return int */ - public function findById(int $userId): array + public function insert(array $userData): int { - $result = []; - if ($userId > 0) { - return $this->findOneBy( - [ - 'usr_id' => $userId, - ] - ); + $createdUserId = $userData['createdUserId']; + $updatedUserId = $userData['updatedUserId']; + + $columns = [ + 'usr_status_flag' => $userData['status'], + 'usr_email' => $userData['email'], + 'usr_password' => $userData['password'], + 'usr_name_prefix' => $userData['namePrefix'], + 'usr_name_first' => $userData['nameFirst'], + 'usr_name_middle' => $userData['nameMiddle'], + 'usr_name_last' => $userData['nameLast'], + 'usr_name_suffix' => $userData['nameSuffix'], + 'usr_issuer' => $userData['issuer'], + 'usr_token_password' => $userData['tokenPassword'], + 'usr_token_id' => $userData['tokenId'], + 'usr_preferences' => $userData['preferences'], + 'usr_created_date' => $userData['createdDate'], + 'usr_updated_date' => $userData['updatedDate'], + ]; + + $insert = Insert::new($this->connection); + + $insert + ->into($this->table) + ->columns($columns) + ; + + if ($createdUserId > 0) { + $insert->column('usr_created_usr_id', $createdUserId); + } + if ($updatedUserId > 0) { + $insert->column('usr_updated_usr_id', $updatedUserId); } - return $result; + $insert->perform(); + + return (int)$insert->getLastInsertId(); } /** - * @param array $criteria + * @param TUserUpdate $userData * - * @return TUserRecord + * @return int */ - public function findOneBy(array $criteria): array + public function update(array $userData): int { - $select = Select::new($this->connection); + $userId = $userData['id']; + $updatedUserId = $userData['updatedUserId']; + + $columns = [ + 'usr_status_flag' => $userData['status'], + 'usr_email' => $userData['email'], + 'usr_password' => $userData['password'], + 'usr_name_prefix' => $userData['namePrefix'], + 'usr_name_first' => $userData['nameFirst'], + 'usr_name_middle' => $userData['nameMiddle'], + 'usr_name_last' => $userData['nameLast'], + 'usr_name_suffix' => $userData['nameSuffix'], + 'usr_issuer' => $userData['issuer'], + 'usr_token_password' => $userData['tokenPassword'], + 'usr_token_id' => $userData['tokenId'], + 'usr_preferences' => $userData['preferences'], + 'usr_updated_date' => $userData['updatedDate'], + ]; - /** @var TUserRecord $result */ - $result = $select - ->from('co_users') - ->whereEquals($criteria) - ->fetchOne() + $update = Update::new($this->connection); + $update + ->table($this->table) + ->columns($columns) + ->where('usr_id = ', $userId) ; - return $result; + if ($updatedUserId > 1) { + $update->column('usr_updated_usr_id', $updatedUserId); + } + + $update->perform(); + + return (int)$userId; } } From 02f7a3d26a681a6718b9bc2f050c9bb3c014eb9e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:51:17 -0600 Subject: [PATCH 133/301] [#.x] - invalidate user tokens --- src/Domain/Components/Cache/Cache.php | 44 ++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Domain/Components/Cache/Cache.php b/src/Domain/Components/Cache/Cache.php index 6a2f963..1d1dcbd 100644 --- a/src/Domain/Components/Cache/Cache.php +++ b/src/Domain/Components/Cache/Cache.php @@ -17,6 +17,7 @@ use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Cache\Adapter\Redis; use Phalcon\Cache\Cache as PhalconCache; use Psr\SimpleCache\InvalidArgumentException; @@ -54,13 +55,54 @@ class Cache extends PhalconCache */ public function getCacheTokenKey(UserTransport $domainUser, string $token): string { + $tokenString = ''; + if (true !== empty($token)) { + $tokenString = sha1($token); + } + return sprintf( self::MASK_TOKEN_USER, $domainUser->getId(), - sha1($token) + $tokenString ); } + /** + * @param EnvManager $env + * @param UserTransport $domainUser + * + * @return bool + * @throws InvalidArgumentException + */ + public function invalidateForUser( + EnvManager $env, + UserTransport $domainUser + ): bool { + /** + * We could store the tokens in the database but this way is faster + * and Redis also has a TTL which auto expires elements. + * + * To get all the keys for a user, we use the underlying adapter + * of the cache which is Redis and call the `getKeys()` on it. The + * keys will come back with the prefix defined in the adapter. In order + * to delete them, we need to remove the prefix because `delete()` will + * automatically prepend each key with it. + */ + /** @var Redis $redis */ + $redis = $this->getAdapter(); + $pattern = $this->getCacheTokenKey($domainUser, ''); + $keys = $redis->getKeys($pattern); + /** @var string $prefix */ + $prefix = $env->get('CACHE_PREFIX', '-rest-', 'string'); + $newKeys = []; + /** @var string $key */ + foreach ($keys as $key) { + $newKeys[] = str_replace($prefix, '', $key); + } + + return $this->deleteMultiple($newKeys); + } + /** * @param EnvManager $env * @param UserTransport $domainUser From 8a61f3c02662e57e4c34825c7b56f9ac0c8c8124 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:51:26 -0600 Subject: [PATCH 134/301] [#.x] - added new services --- src/Domain/Components/Container.php | 76 +++++++++++++++++++---------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index 265f950..a8e97c5 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -23,10 +23,16 @@ use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; use Phalcon\Api\Domain\Components\Middleware\ValidateTokenClaimsMiddleware; use Phalcon\Api\Domain\Components\Middleware\ValidateTokenPresenceMiddleware; +use Phalcon\Api\Domain\Components\Middleware\ValidateTokenRevokedMiddleware; use Phalcon\Api\Domain\Components\Middleware\ValidateTokenStructureMiddleware; use Phalcon\Api\Domain\Components\Middleware\ValidateTokenUserMiddleware; use Phalcon\Api\Domain\Services\Auth\LoginPostService; +use Phalcon\Api\Domain\Services\Auth\LogoutPostService; +use Phalcon\Api\Domain\Services\Auth\RefreshPostService; +use Phalcon\Api\Domain\Services\User\UserDeleteService; use Phalcon\Api\Domain\Services\User\UserGetService; +use Phalcon\Api\Domain\Services\User\UserPostService; +use Phalcon\Api\Domain\Services\User\UserPutService; use Phalcon\Api\Responder\JsonResponder; use Phalcon\Cache\AdapterFactory; use Phalcon\DataMapper\Pdo\Connection; @@ -45,10 +51,6 @@ class Container extends Di { /** @var string */ public const APPLICATION = 'application'; - /** - * Services - */ - public const AUTH_LOGIN_POST_SERVICE = 'service.auth.login.post'; /** @var string */ public const CACHE = 'cache'; /** @var string */ @@ -63,13 +65,34 @@ class Container extends Di public const JWT_TOKEN = 'jwt.token'; /** @var string */ public const LOGGER = 'logger'; + /** @var string */ + public const REQUEST = 'request'; + /** @var string */ + public const RESPONSE = 'response'; + /** @var string */ + public const ROUTER = 'router'; + /** @var string */ + public const SECURITY = Security::class; + /** @var string */ + public const TIME = 'time'; + /** + * Services + */ + public const AUTH_LOGIN_POST_SERVICE = 'service.auth.login.post'; + public const AUTH_LOGOUT_POST_SERVICE = 'service.auth.logout.post'; + public const AUTH_REFRESH_POST_SERVICE = 'service.auth.refresh.post'; + public const USER_DELETE_SERVICE = 'service.user.delete'; + public const USER_GET_SERVICE = 'service.user.get'; + public const USER_POST_SERVICE = 'service.user.post'; + public const USER_PUT_SERVICE = 'service.user.put'; /** * Middleware */ - public const MIDDLEWARE_HEALTH = HealthMiddleware::class; - public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_CLAIMS = ValidateTokenClaimsMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_PRESENCE = ValidateTokenPresenceMiddleware::class; + public const MIDDLEWARE_HEALTH = HealthMiddleware::class; + public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_CLAIMS = ValidateTokenClaimsMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_PRESENCE = ValidateTokenPresenceMiddleware::class; + public const MIDDLEWARE_VALIDATE_TOKEN_REVOKED = ValidateTokenRevokedMiddleware::class; public const MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE = ValidateTokenStructureMiddleware::class; public const MIDDLEWARE_VALIDATE_TOKEN_USER = ValidateTokenUserMiddleware::class; /** @@ -77,22 +100,10 @@ class Container extends Di */ public const REPOSITORY = 'repository'; public const REPOSITORY_TRANSPORT = TransportRepository::class; - /** @var string */ - public const REQUEST = 'request'; /** * Responders */ public const RESPONDER_JSON = JsonResponder::class; -// public const MIDDLEWARE_VALIDATE_TOKEN_REVOKED = ValidateTokenRevokedMiddleware::class; - /** @var string */ - public const RESPONSE = 'response'; - /** @var string */ - public const ROUTER = 'router'; - /** @var string */ - public const SECURITY = Security::class; - /** @var string */ - public const TIME = 'time'; - public const USER_GET_SERVICE = 'service.user.get'; public function __construct() { @@ -110,21 +121,28 @@ public function __construct() self::REPOSITORY => $this->getServiceRepository(), - self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthLoginPost(), - self::USER_GET_SERVICE => $this->getServiceUserGet(), + self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthPost(LoginPostService::class), + self::AUTH_LOGOUT_POST_SERVICE => $this->getServiceAuthPost(LogoutPostService::class), + self::AUTH_REFRESH_POST_SERVICE => $this->getServiceAuthPost(RefreshPostService::class), + self::USER_DELETE_SERVICE => $this->getServiceUser(UserDeleteService::class), + self::USER_GET_SERVICE => $this->getServiceUser(UserGetService::class), + self::USER_POST_SERVICE => $this->getServiceUser(UserPostService::class), + self::USER_PUT_SERVICE => $this->getServiceUser(UserPutService::class), ]; parent::__construct(); } /** + * @param class-string $className + * * @return Service */ - private function getServiceAuthLoginPost(): Service + private function getServiceAuthPost(string $className): Service { return new Service( [ - 'className' => LoginPostService::class, + 'className' => $className, 'arguments' => [ [ 'type' => 'service', @@ -368,13 +386,15 @@ private function getServiceRouter(): Service } /** + * @param class-string $className + * * @return Service */ - private function getServiceUserGet(): Service + private function getServiceUser(string $className): Service { return new Service( [ - 'className' => UserGetService::class, + 'className' => $className, 'arguments' => [ [ 'type' => 'service', @@ -388,6 +408,10 @@ private function getServiceUserGet(): Service 'type' => 'service', 'name' => self::FILTER, ], + [ + 'type' => 'service', + 'name' => self::SECURITY, + ], ], ] ); From 79d828964b49a108db810cd1384af6825f6c8669 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:51:43 -0600 Subject: [PATCH 135/301] [#.x] - added user put service --- src/Domain/Services/User/UserPutService.php | 182 ++++++++ .../Services/User/UserServicePutTest.php | 409 ++++++++++++++++++ 2 files changed, 591 insertions(+) create mode 100644 src/Domain/Services/User/UserPutService.php create mode 100644 tests/Unit/Domain/Services/User/UserServicePutTest.php diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php new file mode 100644 index 0000000..61b5431 --- /dev/null +++ b/src/Domain/Services/User/UserPutService.php @@ -0,0 +1,182 @@ + + * + * 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\User; + +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Domain\Payload; + +/** + * @phpstan-import-type TUserSanitizedUpdateInput from InputTypes + * @phpstan-import-type TUserInput from InputTypes + * @phpstan-import-type TValidationErrors from InputTypes + */ +final class UserPutService extends AbstractUserService +{ + /** + * @param TUserInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + $inputData = $this->sanitizeInput($input); + $errors = $this->validateInput($inputData); + + /** + * Errors exist - return early + */ + if (true !== empty($errors)) { + return new Payload( + DomainStatus::INVALID, + [ + 'errors' => $errors, + ] + ); + } + + /** + * The password needs to be hashed + */ + $password = $inputData['password']; + $hashed = $this->security->hash($password); + + $inputData['password'] = $hashed; + + /** + * Update the record + */ + /** + * @todo get the user from the database to make sure that it is valid + */ + + try { + $userId = $this + ->repository + ->user() + ->update($inputData) + ; + } catch (PDOException $ex) { + /** + * @todo send generic response and log the error + */ + return $this->getErrorPayload($ex->getMessage()); + } + + if ($userId < 1) { + return $this->getErrorPayload('No id returned'); + } + + /** + * Get the user from the database + */ + $dbUser = $this->repository->user()->findById($userId); + $domainUser = $this->transport->newUser($dbUser); + + /** + * Return the user back + */ + return new Payload( + DomainStatus::UPDATED, + [ + 'data' => $domainUser->toArray(), + ] + ); + } + + /** + * @param string $message + * + * @return Payload + */ + private function getErrorPayload(string $message): Payload + { + return new Payload( + DomainStatus::ERROR, + [ + 'errors' => [ + HttpCodesEnum::AppCannotUpdateDatabaseRecord->text() + . $message, + ], + ] + ); + } + + /** + * @param TUserInput $input + * + * @return TUserSanitizedUpdateInput + */ + private function sanitizeInput(array $input): array + { + /** + * Only the fields we want + * + * @todo add sanitizers here + * @todo maybe this is another domain object? + */ + $sanitized = [ + 'id' => $input['id'] ?? 0, + 'status' => $input['status'] ?? 0, + 'email' => $input['email'] ?? '', + 'password' => $input['password'] ?? '', + 'namePrefix' => $input['namePrefix'] ?? '', + 'nameFirst' => $input['nameFirst'] ?? '', + 'nameLast' => $input['nameLast'] ?? '', + 'nameMiddle' => $input['nameMiddle'] ?? '', + 'nameSuffix' => $input['nameSuffix'] ?? '', + 'issuer' => $input['issuer'] ?? '', + 'tokenPassword' => $input['tokenPassword'] ?? '', + 'tokenId' => $input['tokenId'] ?? '', + 'preferences' => $input['preferences'] ?? '', + 'updatedDate' => $input['updatedDate'] ?? null, + 'updatedUserId' => $input['updatedUserId'] ?? 0, + ]; + + if (empty($sanitized['updatedDate'])) { + $sanitized['updatedDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); + } + + return $sanitized; + } + + /** + * @param TUserSanitizedUpdateInput $inputData + * + * @return TValidationErrors|array{} + */ + private function validateInput(array $inputData): array + { + $errors = []; + $required = [ + 'email', + 'password', + 'issuer', + 'tokenPassword', + 'tokenId', + ]; + + foreach ($required as $name) { + $field = $inputData[$name]; + if (true === empty($field)) { + $errors[] = ['Field ' . $name . ' cannot be empty.']; + } + } + + return $errors; + } +} diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php new file mode 100644 index 0000000..7fe18af --- /dev/null +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -0,0 +1,409 @@ + + * + * 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\User; + +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Services\User\UserPutService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Filter\Filter; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class UserServicePutTest extends AbstractUnitTestCase +{ + public function testServiceFailureNoIdReturned(): void + { + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'update', + ] + ) + ->getMock() + ; + $userRepository->method('update')->willReturn(0); + + $repository = $this + ->getMockBuilder(QueryRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'user', + ] + ) + ->getMock() + ; + $repository->method('user')->willReturn($userRepository); + + /** @var Filter $filter */ + $filter = $this->container->get(Container::FILTER); + /** @var Security $security */ + $security = $this->container->get(Container::SECURITY); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $service = new UserPutService($repository, $transport, $filter, $security); + + $userData = $this->getNewUserData(); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = ['Cannot update database record: No id returned']; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailurePdoError(): void + { + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'update', + ] + ) + ->getMock() + ; + $userRepository + ->method('update') + ->willThrowException(new PDOException('abcde')) + ; + + $repository = $this + ->getMockBuilder(QueryRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'user', + ] + ) + ->getMock() + ; + $repository + ->method('user') + ->willReturn($userRepository) + ; + + /** @var Filter $filter */ + $filter = $this->container->get(Container::FILTER); + /** @var Security $security */ + $security = $this->container->get(Container::SECURITY); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $service = new UserPutService($repository, $transport, $filter, $security); + + $userData = $this->getNewUserData(); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = ['Cannot update database record: abcde']; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailureValidation(): void + { + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $userData = $this->getNewUserData(); + + unset( + $userData['usr_email'], + $userData['usr_password'], + $userData['usr_issuer'], + $userData['usr_token_password'], + $userData['usr_token_id'] + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + ['Field email cannot be empty.'], + ['Field password cannot be empty.'], + ['Field issuer cannot be empty.'], + ['Field tokenPassword cannot be empty.'], + ['Field tokenId cannot be empty.'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccess(): void + { + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + $userData = $this->getNewUserData(); + + $userData['usr_created_usr_id'] = 4; + $userData['usr_updated_usr_id'] = 5; + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $domainData['id'] = $userId; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey($userId, $data); + + $data = $data[$userId]; + + $expected = $domainData['status']; + $actual = $data['status']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $actual = str_starts_with($data['password'], '$argon2i$'); + $this->assertTrue($actual); + + $expected = $domainData['namePrefix']; + $actual = $data['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameFirst']; + $actual = $data['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameMiddle']; + $actual = $data['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameLast']; + $actual = $data['nameLast']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameSuffix']; + $actual = $data['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['issuer']; + $actual = $data['issuer']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenPassword']; + $actual = $data['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenId']; + $actual = $data['tokenId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['preferences']; + $actual = $data['preferences']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_date']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_date']; + $actual = $data['updatedDate']; + $this->assertNotSame($expected, $actual); + + $expected = 5; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccessEmptyDates(): void + { + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + $userData = $this->getNewUserData(); + unset( + $userData['usr_updated_date'], + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $domainData['id'] = $userId; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey($userId, $data); + + $data = $data[$userId]; + + $expected = $domainData['status']; + $actual = $data['status']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $actual = str_starts_with($data['password'], '$argon2i$'); + $this->assertTrue($actual); + + $expected = $domainData['namePrefix']; + $actual = $data['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameFirst']; + $actual = $data['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameMiddle']; + $actual = $data['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameLast']; + $actual = $data['nameLast']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameSuffix']; + $actual = $data['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['issuer']; + $actual = $data['issuer']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenPassword']; + $actual = $data['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenId']; + $actual = $data['tokenId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['preferences']; + $actual = $data['preferences']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_date']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_date']; + $actual = $data['updatedDate']; + $this->assertNotSame($expected, $actual); + + $expected = $dbUser['usr_updated_usr_id']; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } +} From 897807a4994841c32df9b52b61cebd05e3aede1d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:51:55 -0600 Subject: [PATCH 136/301] [#.x] - added user post service --- src/Domain/Services/User/UserPostService.php | 183 ++++++++ .../Services/User/UserServicePostTest.php | 411 ++++++++++++++++++ 2 files changed, 594 insertions(+) create mode 100644 src/Domain/Services/User/UserPostService.php create mode 100644 tests/Unit/Domain/Services/User/UserServicePostTest.php diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Services/User/UserPostService.php new file mode 100644 index 0000000..d17c75b --- /dev/null +++ b/src/Domain/Services/User/UserPostService.php @@ -0,0 +1,183 @@ + + * + * 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\User; + +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Domain\Payload; + +/** + * @phpstan-import-type TUserSanitizedInsertInput from InputTypes + * @phpstan-import-type TUserInput from InputTypes + * @phpstan-import-type TValidationErrors from InputTypes + */ +final class UserPostService extends AbstractUserService +{ + /** + * @param TUserInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + $inputData = $this->sanitizeInput($input); + $errors = $this->validateInput($inputData); + + /** + * Errors exist - return early + */ + if (true !== empty($errors)) { + return new Payload( + DomainStatus::INVALID, + [ + 'errors' => $errors, + ] + ); + } + + /** + * The password needs to be hashed + */ + $password = $inputData['password']; + $hashed = $this->security->hash($password); + + $inputData['password'] = $hashed; + + /** + * Insert the record + */ + try { + $userId = $this + ->repository + ->user() + ->insert($inputData) + ; + } catch (PDOException $ex) { + /** + * @todo send generic response and log the error + */ + return $this->getErrorPayload($ex->getMessage()); + } + + if ($userId < 1) { + return $this->getErrorPayload('No id returned'); + } + + /** + * Get the user from the database + */ + $dbUser = $this->repository->user()->findById($userId); + $domainUser = $this->transport->newUser($dbUser); + + /** + * Return the user back + */ + return new Payload( + DomainStatus::CREATED, + [ + 'data' => $domainUser->toArray(), + ] + ); + } + + /** + * @param string $message + * + * @return Payload + */ + private function getErrorPayload(string $message): Payload + { + return new Payload( + DomainStatus::ERROR, + [ + 'errors' => [ + HttpCodesEnum::AppCannotCreateDatabaseRecord->text() + . $message, + ], + ] + ); + } + + /** + * @param TUserInput $input + * + * @return TUserSanitizedInsertInput + */ + private function sanitizeInput(array $input): array + { + /** + * Only the fields we want + * + * @todo add sanitizers here + * @todo maybe this is another domain object? + */ + $sanitized = [ + 'status' => $input['status'] ?? 0, + 'email' => $input['email'] ?? '', + 'password' => $input['password'] ?? '', + 'namePrefix' => $input['namePrefix'] ?? '', + 'nameFirst' => $input['nameFirst'] ?? '', + 'nameLast' => $input['nameLast'] ?? '', + 'nameMiddle' => $input['nameMiddle'] ?? '', + 'nameSuffix' => $input['nameSuffix'] ?? '', + 'issuer' => $input['issuer'] ?? '', + 'tokenPassword' => $input['tokenPassword'] ?? '', + 'tokenId' => $input['tokenId'] ?? '', + 'preferences' => $input['preferences'] ?? '', + 'createdDate' => $input['createdDate'] ?? null, + 'createdUserId' => $input['createdUserId'] ?? 0, + 'updatedDate' => $input['updatedDate'] ?? null, + 'updatedUserId' => $input['updatedUserId'] ?? 0, + ]; + + if (empty($sanitized['createdDate'])) { + $sanitized['createdDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); + } + + if (empty($sanitized['updatedDate'])) { + $sanitized['updatedDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); + } + + return $sanitized; + } + + /** + * @param TUserSanitizedInsertInput $inputData + * + * @return TValidationErrors|array{} + */ + private function validateInput(array $inputData): array + { + $errors = []; + $required = [ + 'email', + 'password', + 'issuer', + 'tokenPassword', + 'tokenId', + ]; + + foreach ($required as $name) { + $field = $inputData[$name]; + if (true === empty($field)) { + $errors[] = ['Field ' . $name . ' cannot be empty.']; + } + } + + return $errors; + } +} diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Services/User/UserServicePostTest.php new file mode 100644 index 0000000..0cbd8f4 --- /dev/null +++ b/tests/Unit/Domain/Services/User/UserServicePostTest.php @@ -0,0 +1,411 @@ + + * + * 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\User; + +use DateTimeImmutable; +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Services\User\UserPostService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class UserServicePostTest extends AbstractUnitTestCase +{ + public function testServiceFailureNoIdReturned(): void + { + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'insert', + ] + ) + ->getMock() + ; + $userRepository + ->method('insert') + ->willReturn(0) + ; + + $repository = $this + ->getMockBuilder(QueryRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'user', + ] + ) + ->getMock() + ; + $repository + ->method('user') + ->willReturn($userRepository) + ; + + /** + * Difference of achieving the same thing - see test above on + * how the class is used without getting it from the DI container + */ + $this->container->setShared(Container::REPOSITORY, $repository); + + /** @var UserPostService $service */ + $service = $this->container->get(Container::USER_POST_SERVICE); + /** @var TransportRepository $service */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $userData = $this->getNewUserData(); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = ['Cannot create database record: No id returned']; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailurePdoError(): void + { + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'insert', + ] + ) + ->getMock() + ; + $userRepository + ->method('insert') + ->willThrowException(new PDOException('abcde')) + ; + + $repository = $this + ->getMockBuilder(QueryRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'user', + ] + ) + ->getMock() + ; + $repository + ->method('user') + ->willReturn($userRepository) + ; + + /** @var Filter $filter */ + $filter = $this->container->get(Container::FILTER); + /** @var Security $security */ + $security = $this->container->get(Container::SECURITY); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $service = new UserPostService($repository, $transport, $filter, $security); + + $userData = $this->getNewUserData(); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = ['Cannot create database record: abcde']; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailureValidation(): void + { + /** @var UserPostService $service */ + $service = $this->container->get(Container::USER_POST_SERVICE); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $userData = $this->getNewUserData(); + + unset( + $userData['usr_email'], + $userData['usr_password'], + $userData['usr_issuer'], + $userData['usr_token_password'], + $userData['usr_token_id'] + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + ['Field email cannot be empty.'], + ['Field password cannot be empty.'], + ['Field issuer cannot be empty.'], + ['Field tokenPassword cannot be empty.'], + ['Field tokenId cannot be empty.'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccess(): void + { + /** @var UserPostService $service */ + $service = $this->container->get(Container::USER_POST_SERVICE); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $userData = $this->getNewUserData(); + $userData['usr_created_usr_id'] = 4; + $userData['usr_updated_usr_id'] = 5; + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $userId = array_key_first($data); + + $this->assertGreaterThan(0, $userId); + + $data = $data[$userId]; + + $expected = $domainData['status']; + $actual = $data['status']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $actual = str_starts_with($data['password'], '$argon2i$'); + $this->assertTrue($actual); + + $expected = $domainData['namePrefix']; + $actual = $data['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameFirst']; + $actual = $data['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameMiddle']; + $actual = $data['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameLast']; + $actual = $data['nameLast']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameSuffix']; + $actual = $data['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['issuer']; + $actual = $data['issuer']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenPassword']; + $actual = $data['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenId']; + $actual = $data['tokenId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['preferences']; + $actual = $data['preferences']; + $this->assertSame($expected, $actual); + + $expected = $domainData['createdDate']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + $expected = 4; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['updatedDate']; + $actual = $data['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = 5; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccessEmptyDates(): void + { + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); + /** @var UserPostService $service */ + $service = $this->container->get(Container::USER_POST_SERVICE); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $userData = $this->getNewUserData(); + unset( + $userData['usr_created_date'], + $userData['usr_updated_date'], + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $transport->newUser($userData); + $domainData = $domainUser->toArray(); + $domainData = $domainData[0]; + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $userId = array_key_first($data); + + $this->assertGreaterThan(0, $userId); + + $data = $data[$userId]; + + $expected = $domainData['status']; + $actual = $data['status']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $actual = str_starts_with($data['password'], '$argon2i$'); + $this->assertTrue($actual); + + $expected = $domainData['namePrefix']; + $actual = $data['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameFirst']; + $actual = $data['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameMiddle']; + $actual = $data['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameLast']; + $actual = $data['nameLast']; + $this->assertSame($expected, $actual); + + $expected = $domainData['nameSuffix']; + $actual = $data['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['issuer']; + $actual = $data['issuer']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenPassword']; + $actual = $data['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenId']; + $actual = $data['tokenId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['preferences']; + $actual = $data['preferences']; + $this->assertSame($expected, $actual); + + $actual = $data['createdDate']; + $this->assertStringContainsString($today, $actual); + + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $actual = $data['updatedDate']; + $this->assertStringContainsString($today, $actual); + + $expected = 0; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } +} From 0a5e19cf6bab427876b53761862532abad7b3721 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:52:09 -0600 Subject: [PATCH 137/301] [#.x] - added user delete service --- .../Services/User/UserDeleteService.php | 64 ++++++++++ .../Services/User/UserServiceDeleteTest.php | 112 ++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 src/Domain/Services/User/UserDeleteService.php create mode 100644 tests/Unit/Domain/Services/User/UserServiceDeleteTest.php diff --git a/src/Domain/Services/User/UserDeleteService.php b/src/Domain/Services/User/UserDeleteService.php new file mode 100644 index 0000000..77b45bd --- /dev/null +++ b/src/Domain/Services/User/UserDeleteService.php @@ -0,0 +1,64 @@ + + * + * 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\User; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Domain\Payload; + +/** + * @phpstan-import-type TUserInput from InputTypes + */ +final class UserDeleteService extends AbstractUserService +{ + /** + * @param TUserInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + $userId = $this->filter->absint($input['id'] ?? 0); + + /** + * Success + */ + if ($userId > 0) { + $rows = $this->repository->user()->deleteById($userId); + + if ($rows > 0) { + return new Payload( + DomainStatus::DELETED, + [ + 'data' => [ + 'Record deleted successfully [#' . $userId . '].', + ], + ] + ); + } + } + + /** + * 404 + */ + return new Payload( + DomainStatus::NOT_FOUND, + [ + 'errors' => [ + 'Record(s) not found', + ], + ] + ); + } +} diff --git a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php b/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php new file mode 100644 index 0000000..05807f5 --- /dev/null +++ b/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php @@ -0,0 +1,112 @@ + + * + * 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\User; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Services\User\UserDeleteService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class UserServiceDeleteTest extends AbstractUnitTestCase +{ + public function testServiceWithUserId(): void + { + /** @var UserDeleteService $service */ + $service = $this->container->get(Container::USER_DELETE_SERVICE); + + /** + * We need to ask for a user to be deleted with an ID that does not + * exist in the database. To ensure that, we will create a user, + * delete it and then try to delete the same user with that ID + */ + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + + $payload = $service->__invoke( + [ + 'id' => $userId, + ] + ); + + $expected = DomainStatus::DELETED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $expected = [ + 'Record deleted successfully [#' . $userId . '].', + ]; + $actual = $data; + $this->assertSame($expected, $actual); + + /** + * Now deleting it again, will result in a 404 + */ + $payload = $service->__invoke( + [ + 'id' => $userId, + ] + ); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + 'Record(s) not found', + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceZeroUserId(): void + { + /** @var UserDeleteService $service */ + $service = $this->container->get(Container::USER_DELETE_SERVICE); + + $payload = $service->__invoke( + [ + 'id' => 0, + ] + ); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + 'Record(s) not found', + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } +} From b5931487a72b8bb575896f33e4c6949395905222 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:52:26 -0600 Subject: [PATCH 138/301] [#.x] - added auth refresh service --- .../Services/Auth/RefreshPostService.php | 120 ++++++++ .../Services/Auth/RefreshPostServiceTest.php | 291 ++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 src/Domain/Services/Auth/RefreshPostService.php create mode 100644 tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php diff --git a/src/Domain/Services/Auth/RefreshPostService.php b/src/Domain/Services/Auth/RefreshPostService.php new file mode 100644 index 0000000..2431936 --- /dev/null +++ b/src/Domain/Services/Auth/RefreshPostService.php @@ -0,0 +1,120 @@ + + * + * 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\Auth; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Domain\Payload; + +/** + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TRefreshInput from InputTypes + * @phpstan-import-type TValidationErrors from InputTypes + */ +final class RefreshPostService extends AbstractAuthService +{ + /** + * @param TRefreshInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + /** + * Get email and password from the input and sanitize them + */ + $token = (string)($input['token'] ?? ''); + $token = $this->filter->string($token); + + /** + * Validation + * + * Empty token + */ + if (true === empty($token)) { + return $this->getUnauthorizedPayload( + [HttpCodesEnum::AppTokenNotPresent->error()] + ); + } + + /** + * @todo catch any exceptions here + * + * Is this the refresh token + */ + $tokenObject = $this->jwtToken->getObject($token); + $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); + if (false === $isRefresh) { + return $this->getUnauthorizedPayload( + [HttpCodesEnum::AppTokenNotValid->error()] + ); + } + + /** + * Get the user - if empty return error + */ + $user = $this + ->jwtToken + ->getUser($this->repository, $tokenObject) + ; + if (true === empty($user)) { + return $this->getUnauthorizedPayload( + [HttpCodesEnum::AppTokenInvalidUser->error()] + ); + } + + $domainUser = $this->transport->newUser($user); + + /** @var TValidationErrors $errors */ + $errors = $this->jwtToken->validate($tokenObject, $domainUser); + if (true !== empty($errors)) { + return $this->getUnauthorizedPayload($errors); + } + + /** + * @todo change this to be the domain user + */ + $userPayload = [ + 'usr_issuer' => $domainUser->getIssuer(), + 'usr_token_password' => $domainUser->getTokenPassword(), + 'usr_token_id' => $domainUser->getTokenId(), + 'usr_id' => $domainUser->getId(), + ]; + $newToken = $this->jwtToken->getForUser($userPayload); + $newRefreshToken = $this->jwtToken->getRefreshForUser($userPayload); + + /** + * Invalidate old tokens, store new tokens in cache + */ + $this->cache->invalidateForUser($this->env, $domainUser); + $this->cache->storeTokenInCache($this->env, $domainUser, $newToken); + $this->cache->storeTokenInCache($this->env, $domainUser, $newRefreshToken); + + /** + * Send the payload back + */ + return new Payload( + DomainStatus::SUCCESS, + [ + 'data' => [ + 'token' => $newToken, + 'refreshToken' => $newRefreshToken, + ], + ] + ); + } +} diff --git a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php b/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php new file mode 100644 index 0000000..f274adb --- /dev/null +++ b/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php @@ -0,0 +1,291 @@ + + * + * 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\Auth; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Services\Auth\LoginPostService; +use Phalcon\Api\Domain\Services\Auth\RefreshPostService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Encryption\Security\JWT\Token\Item; +use Phalcon\Encryption\Security\JWT\Token\Token; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class RefreshPostServiceTest extends AbstractUnitTestCase +{ + public function testServiceEmptyToken(): void + { + /** @var RefreshPostService $service */ + $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + + $payload = $service->__invoke([]); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppTokenNotPresent->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testServiceInvalidToken(): void + { + $user = $this->getNewUserData(); + $errors = [ + ['Incorrect token data'], + ]; + + /** + * Set up mock services + */ + $mockItem = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(true); + + $mockToken = $this->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + 'validate', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + $mockJWT->method('getUser')->willReturn($user); + $mockJWT->method('validate')->willReturn($errors); + + + /** + * Replace the service with the mocked one + */ + $this->container->set(Container::JWT_TOKEN, $mockJWT); + + /** @var RefreshPostService $service */ + $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + + $payload = $service->__invoke(['token' => '1234']); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = ['Incorrect token data']; + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testServiceNotRefreshToken(): void + { + /** + * Set up mock services + */ + $mockItem = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(false); + + $mockToken = $this->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + + /** + * Replace the service with the mocked one + */ + $this->container->set(Container::JWT_TOKEN, $mockJWT); + + /** @var RefreshPostService $service */ + $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + + $payload = $service->__invoke(['token' => '1234']); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppTokenNotValid->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testServiceWithCredentials(): void + { + /** @var LoginPostService $service */ + $loginService = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + /** @var RefreshPostService $service */ + $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + $migration = new UsersMigration($this->getConnection()); + + /** + * Setting the password to something we know + */ + $password = 'password'; + + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; + $payload = [ + 'email' => $email, + 'password' => $password, + ]; + + $payload = $loginService->__invoke($payload); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $result = $payload->getResult(); + $jwt = $result['data']['jwt']; + + $payload = $service->__invoke(['token' => $jwt['refreshToken']]); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey('token', $data); + $this->assertArrayHasKey('refreshToken', $data); + + $this->assertIsString($data['token']); + $this->assertIsString($data['refreshToken']); + $this->assertNotEmpty($data['token']); + $this->assertNotEmpty($data['refreshToken']); + } + + public function testServiceWrongUser(): void + { + /** + * Set up mock services + */ + $mockItem = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(true); + + $mockToken = $this->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + $mockJWT->method('getUser')->willReturn([]); + + + /** + * Replace the service with the mocked one + */ + $this->container->set(Container::JWT_TOKEN, $mockJWT); + + /** @var RefreshPostService $service */ + $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + + $payload = $service->__invoke(['token' => '1234']); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppTokenInvalidUser->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } +} From 76dfc8c998679830631a577e6a496337dc644131 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:52:37 -0600 Subject: [PATCH 139/301] [#.x] - added auth logout service --- .../Services/Auth/LogoutPostService.php | 108 ++++++ .../Services/Auth/LogoutPostServiceTest.php | 339 ++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 src/Domain/Services/Auth/LogoutPostService.php create mode 100644 tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php diff --git a/src/Domain/Services/Auth/LogoutPostService.php b/src/Domain/Services/Auth/LogoutPostService.php new file mode 100644 index 0000000..1bec8d1 --- /dev/null +++ b/src/Domain/Services/Auth/LogoutPostService.php @@ -0,0 +1,108 @@ + + * + * 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\Auth; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Domain\Payload; + +/** + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TLogoutInput from InputTypes + * @phpstan-import-type TValidationErrors from InputTypes + */ +final class LogoutPostService extends AbstractAuthService +{ + /** + * @param TLogoutInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + /** + * @todo common code with refresh + */ + /** + * Get the token + */ + $token = (string)($input['token'] ?? ''); + $token = $this->filter->string($token); + + /** + * Validation + * + * Empty token + */ + if (true === empty($token)) { + return $this->getUnauthorizedPayload( + [HttpCodesEnum::AppTokenNotPresent->error()] + ); + } + + /** + * @todo catch any exceptions here + * + * Is this the refresh token + */ + $tokenObject = $this->jwtToken->getObject($token); + $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); + if (false === $isRefresh) { + return $this->getUnauthorizedPayload( + [HttpCodesEnum::AppTokenNotValid->error()] + ); + } + + /** + * Get the user - if empty return error + */ + $user = $this + ->jwtToken + ->getUser($this->repository, $tokenObject) + ; + if (true === empty($user)) { + return $this->getUnauthorizedPayload( + [HttpCodesEnum::AppTokenInvalidUser->error()] + ); + } + + $domainUser = $this->transport->newUser($user); + + /** @var TValidationErrors $errors */ + $errors = $this->jwtToken->validate($tokenObject, $domainUser); + if (true !== empty($errors)) { + return $this->getUnauthorizedPayload($errors); + } + + /** + * Invalidate old tokens + */ + $this->cache->invalidateForUser($this->env, $domainUser); + + /** + * Send the payload back + */ + return new Payload( + DomainStatus::SUCCESS, + [ + 'data' => [ + 'authenticated' => false, + ], + ] + ); + } +} diff --git a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php new file mode 100644 index 0000000..75f9637 --- /dev/null +++ b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php @@ -0,0 +1,339 @@ + + * + * 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\Auth; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Services\Auth\LoginPostService; +use Phalcon\Api\Domain\Services\Auth\LogoutPostService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Encryption\Security\JWT\Token\Item; +use Phalcon\Encryption\Security\JWT\Token\Token; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class LogoutPostServiceTest extends AbstractUnitTestCase +{ + public function testServiceEmptyToken(): void + { + /** @var LogoutPostService $service */ + $service = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + + $payload = $service->__invoke([]); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppTokenNotPresent->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testServiceInvalidToken(): void + { + $user = $this->getNewUserData(); + $errors = [ + ['Incorrect token data'], + ]; + + /** + * Set up mock services + */ + $mockItem = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(true); + + $mockToken = $this->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + 'validate', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + $mockJWT->method('getUser')->willReturn($user); + $mockJWT->method('validate')->willReturn($errors); + + + /** + * Replace the service with the mocked one + */ + $this->container->set(Container::JWT_TOKEN, $mockJWT); + + /** @var LogoutPostService $service */ + $service = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + + $payload = $service->__invoke(['token' => '1234']); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = ['Incorrect token data']; + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testServiceNotRefreshToken(): void + { + /** + * Set up mock services + */ + $mockItem = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(false); + + $mockToken = $this->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + + /** + * Replace the service with the mocked one + */ + $this->container->set(Container::JWT_TOKEN, $mockJWT); + /** @var LogoutPostService $service */ + $logoutService = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + + /** + * Logout now + */ + $payload = $logoutService->__invoke(['token' => '1234']); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [HttpCodesEnum::AppTokenNotValid->error()]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccess(): void + { + /** @var LogoutPostService $service */ + $logoutService = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + /** @var Cache $cache */ + $cache = $this->container->getShared(Container::CACHE); + /** @var LoginPostService $service */ + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + /** @var TransportRepository $transport */ + $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); + $migration = new UsersMigration($this->getConnection()); + + /** + * Setting the password to something we know + */ + $password = 'password'; + + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; + $input = [ + 'email' => $email, + 'password' => $password, + ]; + + $payload = $service->__invoke($input); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey('authenticated', $data); + + $authenticated = $data['authenticated']; + + $actual = $authenticated; + $this->assertTrue($actual); + + $token = $data['jwt']['token']; + $domainUser = $transport->newUser($dbUser); + $tokenKey = $cache->getCacheTokenKey($domainUser, $token); + + $actual = $cache->has($tokenKey); + $this->assertTrue($actual); + + $refreshToken = $data['jwt']['refreshToken']; + $tokenKey = $cache->getCacheTokenKey($domainUser, $refreshToken); + + $actual = $cache->has($tokenKey); + $this->assertTrue($actual); + + $refreshToken = $data['jwt']['refreshToken']; + + /** + * Logout now + */ + $input = [ + 'token' => $refreshToken, + ]; + $payload = $logoutService->__invoke($input); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey('authenticated', $data); + + $authenticated = $data['authenticated']; + + $actual = $authenticated; + $this->assertFalse($actual); + + $actual = $cache->has($tokenKey); + $this->assertFalse($actual); + } + + public function testServiceWrongUser(): void + { + /** + * Set up mock services + */ + $mockItem = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(true); + + $mockToken = $this->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + $mockJWT->method('getUser')->willReturn([]); + + + /** + * Replace the service with the mocked one + */ + $this->container->set(Container::JWT_TOKEN, $mockJWT); + + /** @var LogoutPostService $service */ + $logoutService = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + + /** + * Logout now + */ + $payload = $logoutService->__invoke(['token' => '1234']); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [HttpCodesEnum::AppTokenInvalidUser->error()]; + $actual = $errors; + $this->assertSame($expected, $actual); + } +} From 309784a17d09be06fbe89a36238c1be383f4a81b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 5 Nov 2025 11:53:02 -0600 Subject: [PATCH 140/301] [#.x] - reworking tests - moving dispatch tests to own file --- .../Services/User/UserServiceDispatchTest.php | 83 +++++++++++++++++++ ...ServiceTest.php => UserServiceGetTest.php} | 56 +------------ 2 files changed, 86 insertions(+), 53 deletions(-) create mode 100644 tests/Unit/Domain/Services/User/UserServiceDispatchTest.php rename tests/Unit/Domain/Services/User/{UserServiceTest.php => UserServiceGetTest.php} (73%) diff --git a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php new file mode 100644 index 0000000..2557457 --- /dev/null +++ b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php @@ -0,0 +1,83 @@ + + * + * 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\User; + +use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use PHPUnit\Framework\Attributes\BackupGlobals; + +#[BackupGlobals(true)] +final class UserServiceDispatchTest extends AbstractUnitTestCase +{ + public function testDispatchGet(): void + { + /** @var EnvManager $env */ + $env = $this->container->getShared(Container::ENV); + /** @var Cache $cache */ + $cache = $this->container->getShared(Container::CACHE); + /** @var TransportRepository $transport */ + $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + $token = $this->getUserToken($dbUser); + $domainUser = $transport->newUser($dbUser); + + /** + * Store the token in the cache + */ + $cache->storeTokenInCache($env, $domainUser, $token); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => RoutesEnum::userGet->endpoint(), + ]; + + $_GET = [ + 'id' => $userId, + ]; + + ob_start(); + require_once $env->appPath('public/index.php'); + $response = ob_get_clean(); + + $contents = json_decode($response, true); + + restore_error_handler(); + + $this->assertArrayHasKey('data', $contents); + $this->assertArrayHasKey('errors', $contents); + + $data = $contents['data']; + $errors = $contents['errors']; + + $expected = []; + $actual = $errors; + $this->assertSame($expected, $actual); + + $user = $transport->newUser($dbUser); + $expected = $user->toArray(); + $actual = $data; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Services/User/UserServiceTest.php b/tests/Unit/Domain/Services/User/UserServiceGetTest.php similarity index 73% rename from tests/Unit/Domain/Services/User/UserServiceTest.php rename to tests/Unit/Domain/Services/User/UserServiceGetTest.php index 9140759..d84e820 100644 --- a/tests/Unit/Domain/Services/User/UserServiceTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceGetTest.php @@ -15,64 +15,14 @@ use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Domain\Services\User\UserGetService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use PHPUnit\Framework\Attributes\BackupGlobals; #[BackupGlobals(true)] -final class UserServiceTest extends AbstractUnitTestCase +final class UserServiceGetTest extends AbstractUnitTestCase { - public function testDispatch(): void - { - /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $migration = new UsersMigration($this->getConnection()); - $dbUser = $this->getNewUser($migration); - $userId = $dbUser['usr_id']; - $token = $this->getUserToken($dbUser); - - $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); - $_SERVER = [ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_TIME_FLOAT' => $time, - 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, - 'REQUEST_URI' => '/user', - ]; - - $_GET = [ - 'userId' => $userId, - ]; - - ob_start(); - require_once $env->appPath('public/index.php'); - $response = ob_get_clean(); - - $contents = json_decode($response, true); - - restore_error_handler(); - - $this->assertArrayHasKey('data', $contents); - $this->assertArrayHasKey('errors', $contents); - - $data = $contents['data']; - $errors = $contents['errors']; - - $expected = []; - $actual = $errors; - $this->assertSame($expected, $actual); - - $user = $transport->newUser($dbUser); - $expected = $user->toArray(); - $actual = $data; - $this->assertSame($expected, $actual); - } - public function testServiceEmptyUserId(): void { /** @var UserGetService $service */ @@ -103,7 +53,7 @@ public function testServiceWithUserId(): void $payload = $service->__invoke( [ - 'userId' => $userId, + 'id' => $userId, ] ); @@ -194,7 +144,7 @@ public function testServiceWrongUserId(): void $payload = $service->__invoke( [ - 'userId' => 999999, + 'id' => 999999, ] ); From f3d2b985810f478f6350110bb03b1d92c2a90b90 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 6 Nov 2025 13:35:30 +0000 Subject: [PATCH 141/301] Update src/Domain/Components/DataSource/User/UserRepository.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Domain/Components/DataSource/User/UserRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index 8ddc986..36d903a 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -138,7 +138,7 @@ public function update(array $userData): int ->where('usr_id = ', $userId) ; - if ($updatedUserId > 1) { + if ($updatedUserId > 0) { $update->column('usr_updated_usr_id', $updatedUserId); } From 6ea7c8a9122e25e4a0a6f03166f5842e5c894627 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 6 Nov 2025 13:35:45 +0000 Subject: [PATCH 142/301] Update src/Domain/Services/User/UserPostService.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Domain/Services/User/UserPostService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Services/User/UserPostService.php index d17c75b..499cc04 100644 --- a/src/Domain/Services/User/UserPostService.php +++ b/src/Domain/Services/User/UserPostService.php @@ -40,7 +40,7 @@ public function __invoke(array $input): Payload /** * Errors exist - return early */ - if (true !== empty($errors)) { + if (!empty($errors)) { return new Payload( DomainStatus::INVALID, [ From 6d4cfc694841dda76891cb201115bf289d68b3f7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 6 Nov 2025 13:35:54 +0000 Subject: [PATCH 143/301] Update src/Domain/Services/User/UserPutService.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Domain/Services/User/UserPutService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php index 61b5431..22ad499 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Services/User/UserPutService.php @@ -40,7 +40,7 @@ public function __invoke(array $input): Payload /** * Errors exist - return early */ - if (true !== empty($errors)) { + if (!empty($errors)) { return new Payload( DomainStatus::INVALID, [ From 015f5121f382c09705ceec93bbcbcfe3098aec67 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:37:56 -0600 Subject: [PATCH 144/301] [#.x] - removed transport repository replaced it smaller objects --- .../DataSource/TransportRepository.php | 115 --------------- .../DataSource/TransportRepositoryTest.php | 139 ------------------ 2 files changed, 254 deletions(-) delete mode 100644 src/Domain/Components/DataSource/TransportRepository.php delete mode 100644 tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php diff --git a/src/Domain/Components/DataSource/TransportRepository.php b/src/Domain/Components/DataSource/TransportRepository.php deleted file mode 100644 index c7edf02..0000000 --- a/src/Domain/Components/DataSource/TransportRepository.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * 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\Components\DataSource; - -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Encryption\Security\JWT\Token\Token; - -/** - * @phpstan-import-type TLoginResponsePayload from UserTypes - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TUserRecord from UserTypes - * - * Note: The 'readonly' keyword was intentionally removed from this class. - * Properties $sessionToken and $sessionUser are mutable to support session - * management, allowing updates to the current session state. This change - * removes immutability guarantees, but is necessary for the intended use. - */ -final class TransportRepository -{ - private ?Token $sessionToken = null; - - private ?UserTransport $sessionUser = null; - - /** - * Returns the session token. - * - * @return Token|null - */ - public function getSessionToken(): ?Token - { - return $this->sessionToken; - } - - /** - * Returns the session user. - * - * @return UserTransport|null - */ - public function getSessionUser(): ?UserTransport - { - return $this->sessionUser; - } - - /** - * @param UserTransport $user - * @param string $token - * @param string $refreshToken - * - * @return TLoginResponsePayload - */ - public function newLoginUser( - UserTransport $user, - string $token, - string $refreshToken - ): array { - return [ - 'authenticated' => true, - 'user' => [ - 'id' => $user->getId(), - 'name' => $user->getFullName(), - 'email' => $user->getEmail(), - ], - 'jwt' => [ - 'token' => $token, - 'refreshToken' => $refreshToken, - ], - ]; - } - - /** - * @param TUserRecord $dbUser - * - * @return UserTransport - */ - public function newUser(array $dbUser): UserTransport - { - return new UserTransport($dbUser); - } - - /** - * Sets the session Token - * - * @param Token $token - * - * @return void - */ - public function setSessionToken(Token $token): void - { - $this->sessionToken = $token; - } - - /** - * Populates the session user with the user data. - * - * @param TUserDbRecord $user - * - * @return void - */ - public function setSessionUser(array $user): void - { - $this->sessionUser = $this->newUser($user); - } -} diff --git a/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php b/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php deleted file mode 100644 index 90b6120..0000000 --- a/tests/Unit/Domain/Components/DataSource/TransportRepositoryTest.php +++ /dev/null @@ -1,139 +0,0 @@ - - * - * 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\Components\DataSource; - -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Tests\AbstractUnitTestCase; - -final class TransportRepositoryTest extends AbstractUnitTestCase -{ - public function testNewUser(): void - { - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $user = $transport->newUser([]); - $actual = $user->isEmpty(); - $this->assertTrue($actual); - - $dbUser = $this->getNewUserData(); - $user = $transport->newUser($dbUser); - - $fullName = trim( - $dbUser['usr_name_last'] - . ', ' - . $dbUser['usr_name_first'] - . ' ' - . $dbUser['usr_name_middle'] - ); - - $expected = [ - $dbUser['usr_id'] => [ - 'id' => $dbUser['usr_id'], - 'status' => $dbUser['usr_status_flag'], - 'email' => $dbUser['usr_email'], - 'password' => $dbUser['usr_password'], - 'namePrefix' => $dbUser['usr_name_prefix'], - 'nameFirst' => $dbUser['usr_name_first'], - 'nameMiddle' => $dbUser['usr_name_middle'], - 'nameLast' => $dbUser['usr_name_last'], - 'nameSuffix' => $dbUser['usr_name_suffix'], - 'issuer' => $dbUser['usr_issuer'], - 'tokenPassword' => $dbUser['usr_token_password'], - 'tokenId' => $dbUser['usr_token_id'], - 'preferences' => $dbUser['usr_preferences'], - 'createdDate' => $dbUser['usr_created_date'], - 'createdUserId' => $dbUser['usr_created_usr_id'], - 'updatedDate' => $dbUser['usr_updated_date'], - 'updatedUserId' => $dbUser['usr_updated_usr_id'], - 'fullName' => $fullName, - ], - ]; - $actual = $user->toArray(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_id']; - $actual = $user->getId(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_status_flag']; - $actual = $user->getStatus(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_email']; - $actual = $user->getEmail(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_password']; - $actual = $user->getPassword(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_prefix']; - $actual = $user->getNamePrefix(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_first']; - $actual = $user->getNameFirst(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_middle']; - $actual = $user->getNameMiddle(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_last']; - $actual = $user->getNameLast(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_name_suffix']; - $actual = $user->getNameSuffix(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_issuer']; - $actual = $user->getIssuer(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_token_password']; - $actual = $user->getTokenPassword(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_token_id']; - $actual = $user->getTokenId(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_preferences']; - $actual = $user->getPreferences(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_created_date']; - $actual = $user->getCreatedDate(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_created_usr_id']; - $actual = $user->getCreatedUserId(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_updated_date']; - $actual = $user->getUpdatedDate(); - $this->assertSame($expected, $actual); - - $expected = $dbUser['usr_updated_usr_id']; - $actual = $user->getUpdatedUserId(); - $this->assertSame($expected, $actual); - - $expected = $fullName; - $actual = $user->getFullName(); - $this->assertSame($expected, $actual); - } -} From 5f518608e4b142e6956b67ef5e1cb397a68441f3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:38:22 -0600 Subject: [PATCH 145/301] [#.x] - new payload class with helper methods --- src/Domain/Components/Payload.php | 82 +++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/Domain/Components/Payload.php diff --git a/src/Domain/Components/Payload.php b/src/Domain/Components/Payload.php new file mode 100644 index 0000000..203e8d3 --- /dev/null +++ b/src/Domain/Components/Payload.php @@ -0,0 +1,82 @@ + + * + * 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\Components; + +use PayloadInterop\DomainStatus; +use Phalcon\Domain\Payload as PhalconPayload; + +final class Payload extends PhalconPayload +{ + private function __construct( + string $status, + array $data = [], + array $errors = [] + ) { + $result = []; + + if (true !== empty($data)) { + $result = [ + 'data' => $data, + ]; + } + + if (true !== empty($errors)) { + $result = [ + 'errors' => $errors, + ]; + } + + parent::__construct($status, $result); + } + + public static function created(array $data): self + { + return new self(DomainStatus::CREATED, $data); + } + + public static function deleted(array $data): self + { + return new self(DomainStatus::DELETED, $data); + } + + public static function error(array $errors): self + { + return new self(status: DomainStatus::ERROR, errors: $errors); + } + + public static function invalid(array $errors): self + { + return new self(status: DomainStatus::INVALID, errors: $errors); + } + + public static function notFound(array $errors): self + { + return new self(status: DomainStatus::NOT_FOUND, errors: $errors); + } + + public static function success(array $data): self + { + return new self(DomainStatus::SUCCESS, $data); + } + + public static function unauthorized(array $errors): self + { + return new self(status: DomainStatus::UNAUTHORIZED, errors: $errors); + } + + public static function updated(array $data): self + { + return new self(DomainStatus::UPDATED, $data); + } +} From 619108f934be4005f9f861482b6b8aae94f8eedc Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:38:41 -0600 Subject: [PATCH 146/301] [#.x] - added user sanitizer for input --- .../DataSource/User/UserSanitizer.php | 95 +++++++++++++++++ .../DataSource/User/UserSanitizerTest.php | 100 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 src/Domain/Components/DataSource/User/UserSanitizer.php create mode 100644 tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php diff --git a/src/Domain/Components/DataSource/User/UserSanitizer.php b/src/Domain/Components/DataSource/User/UserSanitizer.php new file mode 100644 index 0000000..2a1f9c9 --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserSanitizer.php @@ -0,0 +1,95 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use Phalcon\Filter\Filter; + +final class UserSanitizer implements SanitizerInterface +{ + public function __construct( + private readonly Filter $filter, + ) { + } + + /** + * Return a sanitized array of the input + * + * @param array $input + * + * @return array + */ + public function sanitize(array $input): array + { + $fields = [ + 'id' => 0, + 'status' => 0, + 'email' => null, + 'password' => null, + 'namePrefix' => null, + 'nameFirst' => null, + 'nameLast' => null, + 'nameMiddle' => null, + 'nameSuffix' => null, + 'issuer' => null, + 'tokenPassword' => null, + 'tokenId' => null, + 'preferences' => null, + 'createdDate' => null, + 'createdUserId' => null, + 'updatedDate' => null, + 'updatedUserId' => null, + ]; + + /** + * Sanitize all the fields. The fields can be `null` meaning they + * were not defined with the input or a value. If the value exists + * we will sanitize it + */ + $sanitized = []; + foreach ($fields as $name => $defaultValue) { + $value = $input[$name] ?? $defaultValue; + + if (null !== $value) { + $sanitizer = $this->getSanitizer($name); + if (true !== empty($sanitizer)) { + $value = $this->filter->sanitize($value, $sanitizer); + } + } + $sanitized[$name] = $value; + } + + return $sanitized; + } + + /** + * @param string $name + * + * @return string + */ + private function getSanitizer(string $name): string + { + return match ($name) { + 'id', + 'status', + 'createdUserId', + 'updatedUserId' => Filter::FILTER_ABSINT, + 'email' => Filter::FILTER_EMAIL, + 'password', + 'tokenId', + 'tokenPassword' => '', // Password will be distorted + default => Filter::FILTER_STRING, + }; + } +} diff --git a/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php b/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php new file mode 100644 index 0000000..8b6ef93 --- /dev/null +++ b/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php @@ -0,0 +1,100 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class UserSanitizerTest extends AbstractUnitTestCase +{ + public function testEmpty(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new UserSanitizer($filter); + + $expected = [ + 'id' => 0, + 'status' => 0, + 'email' => null, + 'password' => null, + 'namePrefix' => null, + 'nameFirst' => null, + 'nameLast' => null, + 'nameMiddle' => null, + 'nameSuffix' => null, + 'issuer' => null, + 'tokenPassword' => null, + 'tokenId' => null, + 'preferences' => null, + 'createdDate' => null, + 'createdUserId' => null, + 'updatedDate' => null, + 'updatedUserId' => null, + ]; + $actual = $sanitizer->sanitize([]); + $this->assertSame($expected, $actual); + } + + public function testObject(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new UserSanitizer($filter); + + $userData = [ + 'id' => '123', + 'status' => '4', + 'email' => 'John.Doe (newsletter) +spam@example.COM', + 'password' => 'some ', + 'namePrefix' => 'some ', + 'nameFirst' => 'some ', + 'nameLast' => 'some ', + 'nameMiddle' => 'some ', + 'nameSuffix' => 'some ', + 'issuer' => 'some ', + 'tokenPassword' => 'some ', + 'tokenId' => 'some ', + 'preferences' => 'some ', + 'createdDate' => 'some ', + 'createdUserId' => '123', + 'updatedDate' => 'some ', + 'updatedUserId' => '123', + ]; + + $expected = [ + 'id' => 123, + 'status' => 4, + 'email' => 'John.Doenewsletter+spam@example.COM', + 'password' => 'some ', + 'namePrefix' => 'some <value>', + 'nameFirst' => 'some <value>', + 'nameLast' => 'some <value>', + 'nameMiddle' => 'some <value>', + 'nameSuffix' => 'some <value>', + 'issuer' => 'some <value>', + 'tokenPassword' => 'some ', + 'tokenId' => 'some ', + 'preferences' => 'some <value>', + 'createdDate' => 'some <value>', + 'createdUserId' => 123, + 'updatedDate' => 'some <value>', + 'updatedUserId' => 123, + ]; + $actual = $sanitizer->sanitize($userData); + $this->assertSame($expected, $actual); + } +} From 0026f43903e086095c992ff0beedfb4fc5757233 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:39:01 -0600 Subject: [PATCH 147/301] [#.x] - mapper class to convert objects (domain/db etc.) --- .../Components/DataSource/User/UserMapper.php | 101 +++++++ .../DataSource/User/UserMapperTest.php | 260 ++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 src/Domain/Components/DataSource/User/UserMapper.php create mode 100644 tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php diff --git a/src/Domain/Components/DataSource/User/UserMapper.php b/src/Domain/Components/DataSource/User/UserMapper.php new file mode 100644 index 0000000..2555b2d --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserMapper.php @@ -0,0 +1,101 @@ + + * + * 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\Components\DataSource\User; + +final class UserMapper +{ + /** + * Map Domain User -> DB row (usr_*) + * + * @return array + */ + public function db(User $user): array + { + return [ + 'usr_id' => $user->id, + 'usr_status_flag' => $user->status, + 'usr_email' => $user->email, + 'usr_password' => $user->password, + 'usr_name_prefix' => $user->namePrefix, + 'usr_name_first' => $user->nameFirst, + 'usr_name_middle' => $user->nameMiddle, + 'usr_name_last' => $user->nameLast, + 'usr_name_suffix' => $user->nameSuffix, + 'usr_issuer' => $user->issuer, + 'usr_token_password' => $user->tokenPassword, + 'usr_token_id' => $user->tokenId, + 'usr_preferences' => $user->preferences, + 'usr_created_date' => $user->createdDate, + 'usr_created_usr_id' => $user->createdUserId, + 'usr_updated_date' => $user->updatedDate, + 'usr_updated_usr_id' => $user->updatedUserId, + ]; + } + + /** + * Map DB row (usr_*) -> Domain User + * + * @param array $row + */ + public function domain(array $row): User + { + return new User( + (int)($row['usr_id'] ?? 0), + (int)($row['usr_status_flag'] ?? 0), + (string)($row['usr_email'] ?? ''), + $row['usr_password'] ?? '', + $row['usr_name_prefix'] ?? null, + $row['usr_name_first'] ?? null, + $row['usr_name_middle'] ?? null, + $row['usr_name_last'] ?? null, + $row['usr_name_suffix'] ?? null, + $row['usr_issuer'] ?? null, + $row['usr_token_password'] ?? null, + $row['usr_token_id'] ?? null, + $row['usr_preferences'] ?? null, + $row['usr_created_date'] ?? null, + isset($row['usr_created_usr_id']) ? (int)$row['usr_created_usr_id'] : null, + $row['usr_updated_date'] ?? null, + isset($row['usr_updated_usr_id']) ? (int)$row['usr_updated_usr_id'] : null, + ); + } + + /** + * Map input row -> Domain User + * + * @param array $row + */ + public function input(array $row): User + { + return new User( + $row['id'], + $row['status'], + $row['email'], + $row['password'], + $row['namePrefix'], + $row['nameFirst'], + $row['nameMiddle'], + $row['nameLast'], + $row['nameSuffix'], + $row['issuer'], + $row['tokenPassword'], + $row['tokenId'], + $row['preferences'], + $row['createdDate'], + $row['createdUserId'], + $row['updatedDate'], + $row['updatedUserId'], + ); + } +} diff --git a/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php b/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php new file mode 100644 index 0000000..a44e353 --- /dev/null +++ b/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php @@ -0,0 +1,260 @@ + + * + * 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\Components\DataSource\User; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function json_encode; + +final class UserMapperTest extends AbstractUnitTestCase +{ + public function testDb(): void + { + $faker = FakerFactory::create(); + + $preferences = [ + 'theme' => $faker->randomElement(['dark', 'light']), + 'lang' => $faker->languageCode(), + ]; + $preferencesJson = json_encode($preferences); + + $createdDate = $faker->date(Dates::DATE_TIME_FORMAT); + $updatedDate = $faker->date(Dates::DATE_TIME_FORMAT); + + $user = new User( + $faker->numberBetween(1, 1000), + $faker->numberBetween(0, 9), + $faker->safeEmail(), + $faker->password(), + $faker->optional()->title(), + $faker->firstName(), + $faker->optional()->word(), + $faker->lastName(), + $faker->optional()->suffix(), + $faker->company(), + $faker->optional()->password(), + $faker->uuid(), + $preferencesJson, + $createdDate, + $faker->numberBetween(1, 1000), + $updatedDate, + $faker->numberBetween(1, 1000) + ); + + $mapper = new UserMapper(); + $row = $mapper->db($user); + + $expected = [ + 'usr_id' => $user->id, + 'usr_status_flag' => $user->status, + 'usr_email' => $user->email, + 'usr_password' => $user->password, + 'usr_name_prefix' => $user->namePrefix, + 'usr_name_first' => $user->nameFirst, + 'usr_name_middle' => $user->nameMiddle, + 'usr_name_last' => $user->nameLast, + 'usr_name_suffix' => $user->nameSuffix, + 'usr_issuer' => $user->issuer, + 'usr_token_password' => $user->tokenPassword, + 'usr_token_id' => $user->tokenId, + 'usr_preferences' => $user->preferences, + 'usr_created_date' => $user->createdDate, + 'usr_created_usr_id' => $user->createdUserId, + 'usr_updated_date' => $user->updatedDate, + 'usr_updated_usr_id' => $user->updatedUserId, + ]; + + $actual = $row; + $this->assertSame($expected, $actual); + } + + public function testDomain(): void + { + $faker = FakerFactory::create(); + $mapper = new UserMapper(); + + // Empty row: defaults should be applied + $emptyUser = $mapper->domain([]); + + $expected = 0; + $actual = $emptyUser->id; + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = $emptyUser->status; + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = $emptyUser->email; + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = $emptyUser->password; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->preferences; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->createdDate; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->createdUserId; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->updatedDate; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyUser->updatedUserId; + $this->assertSame($expected, $actual); + + // Row with present created/updated user ids as strings should be cast to int + $row = [ + 'usr_id' => (string)$faker->numberBetween(1, 1000), + 'usr_status_flag' => (string)$faker->numberBetween(0, 9), + 'usr_email' => $faker->safeEmail(), + 'usr_created_usr_id' => (string)$faker->numberBetween(1, 1000), + 'usr_updated_usr_id' => (string)$faker->numberBetween(1, 1000), + ]; + + $user = $mapper->domain($row); + + $expected = (int)$row['usr_id']; + $actual = $user->id; + $this->assertSame($expected, $actual); + + $expected = (int)$row['usr_status_flag']; + $actual = $user->status; + $this->assertSame($expected, $actual); + + $expected = $row['usr_email']; + $actual = $user->email; + $this->assertSame($expected, $actual); + + $expected = (int)$row['usr_created_usr_id']; + $actual = $user->createdUserId; + $this->assertSame($expected, $actual); + + $expected = (int)$row['usr_updated_usr_id']; + $actual = $user->updatedUserId; + $this->assertSame($expected, $actual); + } + + public function testInput(): void + { + $faker = FakerFactory::create(); + $mapper = new UserMapper(); + + $preferences = ['k' => $faker->word()]; + $preferencesJson = json_encode($preferences); + + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + 'namePrefix' => $faker->optional()->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->optional()->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => null, + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->optional()->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => $preferencesJson, + 'createdDate' => $faker->date('Y-m-d'), + 'createdUserId' => $faker->numberBetween(1, 1000), + 'updatedDate' => $faker->date('Y-m-d'), + 'updatedUserId' => $faker->numberBetween(1, 1000), + ]; + + $user = $mapper->input($input); + + $expected = $input['id']; + $actual = $user->id; + $this->assertSame($expected, $actual); + + $expected = $input['status']; + $actual = $user->status; + $this->assertSame($expected, $actual); + + $expected = $input['email']; + $actual = $user->email; + $this->assertSame($expected, $actual); + + $expected = $input['password']; + $actual = $user->password; + $this->assertSame($expected, $actual); + + $expected = $input['namePrefix']; + $actual = $user->namePrefix; + $this->assertSame($expected, $actual); + + $expected = $input['nameFirst']; + $actual = $user->nameFirst; + $this->assertSame($expected, $actual); + + $expected = $input['nameMiddle']; + $actual = $user->nameMiddle; + $this->assertSame($expected, $actual); + + $expected = $input['nameLast']; + $actual = $user->nameLast; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $user->nameSuffix; + $this->assertSame($expected, $actual); + + $expected = $input['issuer']; + $actual = $user->issuer; + $this->assertSame($expected, $actual); + + $expected = $input['tokenPassword']; + $actual = $user->tokenPassword; + $this->assertSame($expected, $actual); + + $expected = $input['tokenId']; + $actual = $user->tokenId; + $this->assertSame($expected, $actual); + + $expected = $input['preferences']; + $actual = $user->preferences; + $this->assertSame($expected, $actual); + + $expected = $input['createdDate']; + $actual = $user->createdDate; + $this->assertSame($expected, $actual); + + $expected = $input['createdUserId']; + $actual = $user->createdUserId; + $this->assertSame($expected, $actual); + + $expected = $input['updatedDate']; + $actual = $user->updatedDate; + $this->assertSame($expected, $actual); + + $expected = $input['updatedUserId']; + $actual = $user->updatedUserId; + $this->assertSame($expected, $actual); + } +} From e79345d3e65534a4c7f59a568144bfd287c8cc6f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:39:23 -0600 Subject: [PATCH 148/301] [#.x] - added user validator (input) --- .../DataSource/User/UserValidator.php | 50 ++++++++++++++ .../DataSource/User/UserValidatorTest.php | 69 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/Domain/Components/DataSource/User/UserValidator.php create mode 100644 tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php diff --git a/src/Domain/Components/DataSource/User/UserValidator.php b/src/Domain/Components/DataSource/User/UserValidator.php new file mode 100644 index 0000000..47be3f0 --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserValidator.php @@ -0,0 +1,50 @@ + + * + * 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\Components\DataSource\User; + +final class UserValidator +{ + /** + * Validate a UserInput and return an array of errors. + * Empty array means valid. + * + * @param UserInput $input + * + * @return array + */ + public function validate(UserInput $input): array + { + $errors = []; + $required = [ + 'email', + 'password', + 'issuer', + 'tokenPassword', + 'tokenId', + ]; + + foreach ($required as $name) { + $value = $input->$name; + if (true === empty($value)) { + $errors[] = ['Field ' . $name . ' cannot be empty.']; + } + } + + /** + * @todo add validators + */ + + return $errors; + } +} diff --git a/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php b/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php new file mode 100644 index 0000000..a71e7b2 --- /dev/null +++ b/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php @@ -0,0 +1,69 @@ + + * + * 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\Components\DataSource\User; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; +use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; +use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class UserValidatorTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + + $input = []; + $userInput = UserInput::new($sanitizer, $input); + + $validator = new UserValidator(); + $actual = $validator->validate($userInput); + + $expected = [ + ['Field email cannot be empty.'], + ['Field password cannot be empty.'], + ['Field issuer cannot be empty.'], + ['Field tokenPassword cannot be empty.'], + ['Field tokenId cannot be empty.'], + ]; + + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + $faker = FakerFactory::create(); + + $input = [ + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + ]; + + $userInput = UserInput::new($sanitizer, $input); + + $validator = new UserValidator(); + $actual = $validator->validate($userInput); + + $expected = []; + $this->assertSame($expected, $actual); + } +} From cf62a202d5b394b66308937d3de7ac54ce73d9f3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:39:41 -0600 Subject: [PATCH 149/301] [#.x] - added sanitizer interface --- .../DataSource/SanitizerInterface.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/Domain/Components/DataSource/SanitizerInterface.php diff --git a/src/Domain/Components/DataSource/SanitizerInterface.php b/src/Domain/Components/DataSource/SanitizerInterface.php new file mode 100644 index 0000000..b4fd17b --- /dev/null +++ b/src/Domain/Components/DataSource/SanitizerInterface.php @@ -0,0 +1,26 @@ + + * + * 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\Components\DataSource; + +interface SanitizerInterface +{ + /** + * Return a sanitized array of the input + * + * @param array $input + * + * @return array + */ + public function sanitize(array $input): array; +} From 9a45dc9a1a2d5f82396a8a13e80acc15fad51ea7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:39:53 -0600 Subject: [PATCH 150/301] [#.x] - added auth input and sanitizer --- .../Components/DataSource/Auth/AuthInput.php | 37 ++++++++++ .../DataSource/Auth/AuthSanitizer.php | 74 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 src/Domain/Components/DataSource/Auth/AuthInput.php create mode 100644 src/Domain/Components/DataSource/Auth/AuthSanitizer.php diff --git a/src/Domain/Components/DataSource/Auth/AuthInput.php b/src/Domain/Components/DataSource/Auth/AuthInput.php new file mode 100644 index 0000000..b4250fb --- /dev/null +++ b/src/Domain/Components/DataSource/Auth/AuthInput.php @@ -0,0 +1,37 @@ + + * + * 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\Components\DataSource\Auth; + +use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; + +final class AuthInput +{ + public function __construct( + public readonly ?string $email, + public readonly ?string $password, + public readonly ?string $token + ) { + } + + public static function new(SanitizerInterface $sanitizer, array $input): self + { + $sanitized = $sanitizer->sanitize($input); + + return new self( + $sanitized['email'] ?? null, + $sanitized['password'] ?? null, + $sanitized['token'] ?? null, + ); + } +} diff --git a/src/Domain/Components/DataSource/Auth/AuthSanitizer.php b/src/Domain/Components/DataSource/Auth/AuthSanitizer.php new file mode 100644 index 0000000..6cbc431 --- /dev/null +++ b/src/Domain/Components/DataSource/Auth/AuthSanitizer.php @@ -0,0 +1,74 @@ + + * + * 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\Components\DataSource\Auth; + +use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use Phalcon\Filter\Filter; + +final class AuthSanitizer implements SanitizerInterface +{ + public function __construct( + private readonly Filter $filter, + ) { + } + + /** + * Return a sanitized array of the input + * + * @param array $input + * + * @return array + */ + public function sanitize(array $input): array + { + $fields = [ + 'email' => null, + 'password' => null, + 'token' => null, + ]; + + /** + * Sanitize all the fields. The fields can be `null` meaning they + * were not defined with the input or a value. If the value exists + * we will sanitize it + */ + $sanitized = []; + foreach ($fields as $name => $defaultValue) { + $value = $input[$name] ?? $defaultValue; + + if (null !== $value) { + $sanitizer = $this->getSanitizer($name); + if (true !== empty($sanitizer)) { + $value = $this->filter->sanitize($value, $sanitizer); + } + } + $sanitized[$name] = $value; + } + + return $sanitized; + } + + /** + * @param string $name + * + * @return string + */ + private function getSanitizer(string $name): string + { + return match ($name) { + 'email' => Filter::FILTER_EMAIL, + default => '', + }; + } +} From 7d4ec11b71c905f7ef4a7fdb1bb2d2d0ab437734 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:40:10 -0600 Subject: [PATCH 151/301] [#.x] - new user domain object and tests --- .../Components/DataSource/User/User.php | 52 ++++++++++++ .../Components/DataSource/User/UserTest.php | 80 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/Domain/Components/DataSource/User/User.php create mode 100644 tests/Unit/Domain/Components/DataSource/User/UserTest.php diff --git a/src/Domain/Components/DataSource/User/User.php b/src/Domain/Components/DataSource/User/User.php new file mode 100644 index 0000000..f6a599a --- /dev/null +++ b/src/Domain/Components/DataSource/User/User.php @@ -0,0 +1,52 @@ + + * + * 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\Components\DataSource\User; + +use function trim; + +final class User +{ + public function __construct( + public readonly int $id, + public readonly int $status, + public readonly string $email, + public readonly string $password, + public readonly ?string $namePrefix, + public readonly ?string $nameFirst, + public readonly ?string $nameMiddle, + public readonly ?string $nameLast, + public readonly ?string $nameSuffix, + public readonly ?string $issuer, + public readonly ?string $tokenPassword, + public readonly ?string $tokenId, + public readonly ?string $preferences, + public readonly ?string $createdDate, + public readonly ?int $createdUserId, + public readonly ?string $updatedDate, + public readonly ?int $updatedUserId, + ) { + } + + public function fullName(): string + { + return trim( + ($this->nameLast ?? '') . ', ' . ($this->nameFirst ?? '') . ' ' . ($this->nameMiddle ?? '') + ); + } + + public function toArray(): array + { + return [$this->id => get_object_vars($this)]; + } +} diff --git a/tests/Unit/Domain/Components/DataSource/User/UserTest.php b/tests/Unit/Domain/Components/DataSource/User/UserTest.php new file mode 100644 index 0000000..0ce4a01 --- /dev/null +++ b/tests/Unit/Domain/Components/DataSource/User/UserTest.php @@ -0,0 +1,80 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function rand; + +final class UserTest extends AbstractUnitTestCase +{ + public function testObject(): void + { + $userData = $this->getNewUserData(); + $userData['usr_id'] = rand(1, 100); + + $user = new User( + $userData['usr_id'], + $userData['usr_status_flag'], + $userData['usr_email'], + $userData['usr_password'], + $userData['usr_name_prefix'], + $userData['usr_name_first'], + $userData['usr_name_middle'], + $userData['usr_name_last'], + $userData['usr_name_suffix'], + $userData['usr_issuer'], + $userData['usr_token_password'], + $userData['usr_token_id'], + $userData['usr_preferences'], + $userData['usr_created_date'], + $userData['usr_created_usr_id'], + $userData['usr_updated_date'], + $userData['usr_updated_usr_id'], + ); + + $expected = ($userData['usr_name_last'] ?? '') + . ', ' + . ($userData['usr_name_first'] ?? '') + . ' ' + . ($userData['usr_name_middle'] ?? ''); + $actual = $user->fullName(); + $this->assertSame($expected, $actual); + + $expected = [ + $userData['usr_id'] => [ + 'id' => $userData['usr_id'], + 'status' => $userData['usr_status_flag'], + 'email' => $userData['usr_email'], + 'password' => $userData['usr_password'], + 'namePrefix' => $userData['usr_name_prefix'], + 'nameFirst' => $userData['usr_name_first'], + 'nameMiddle' => $userData['usr_name_middle'], + 'nameLast' => $userData['usr_name_last'], + 'nameSuffix' => $userData['usr_name_suffix'], + 'issuer' => $userData['usr_issuer'], + 'tokenPassword' => $userData['usr_token_password'], + 'tokenId' => $userData['usr_token_id'], + 'preferences' => $userData['usr_preferences'], + 'createdDate' => $userData['usr_created_date'], + 'createdUserId' => $userData['usr_created_usr_id'], + 'updatedDate' => $userData['usr_updated_date'], + 'updatedUserId' => $userData['usr_updated_usr_id'], + ], + ]; + $actual = $user->toArray(); + $this->assertSame($expected, $actual); + } +} From e01c835df75e72bd7bf564db200c859d4f085509 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:40:23 -0600 Subject: [PATCH 152/301] [#.x] - added input class for users --- .../Components/DataSource/User/UserInput.php | 70 ++++++++++ .../DataSource/User/UserInputTest.php | 130 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 src/Domain/Components/DataSource/User/UserInput.php create mode 100644 tests/Unit/Domain/Components/DataSource/User/UserInputTest.php diff --git a/src/Domain/Components/DataSource/User/UserInput.php b/src/Domain/Components/DataSource/User/UserInput.php new file mode 100644 index 0000000..15f6545 --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserInput.php @@ -0,0 +1,70 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; + +final class UserInput +{ + public function __construct( + public readonly ?int $id, + public readonly ?int $status, + public readonly ?string $email, + public readonly ?string $password, + public readonly ?string $namePrefix, + public readonly ?string $nameFirst, + public readonly ?string $nameMiddle, + public readonly ?string $nameLast, + public readonly ?string $nameSuffix, + public readonly ?string $issuer, + public readonly ?string $tokenPassword, + public readonly ?string $tokenId, + public readonly ?string $preferences, + public readonly ?string $createdDate, + public readonly ?int $createdUserId, + public readonly ?string $updatedDate, + public readonly ?int $updatedUserId, + ) { + } + + public static function new(SanitizerInterface $sanitizer, array $input): self + { + $sanitized = $sanitizer->sanitize($input); + + return new self( + $sanitized['id'], + $sanitized['status'], + $sanitized['email'], + $sanitized['password'], + $sanitized['namePrefix'], + $sanitized['nameFirst'], + $sanitized['nameMiddle'], + $sanitized['nameLast'], + $sanitized['nameSuffix'], + $sanitized['issuer'], + $sanitized['tokenPassword'], + $sanitized['tokenId'], + $sanitized['preferences'], + $sanitized['createdDate'], + $sanitized['createdUserId'], + $sanitized['updatedDate'], + $sanitized['updatedUserId'] + ); + } + + public function toArray(): array + { + return [$this->id => get_object_vars($this)]; + } +} diff --git a/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php b/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php new file mode 100644 index 0000000..2424f60 --- /dev/null +++ b/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php @@ -0,0 +1,130 @@ + + * + * 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\Components\DataSource\User; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; +use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function json_encode; + +final class UserInputTest extends AbstractUnitTestCase +{ + public function testToArray(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + $faker = FakerFactory::create(); + + // Build an input with many fields present + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => " Foo.Bar+tag@Example.COM ", + 'password' => $faker->password(), + 'namePrefix' => $faker->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => $faker->randomElement([null, $faker->suffix()]), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => json_encode(['k' => $faker->word()]), + 'createdDate' => $faker->date('Y-m-d'), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + 'updatedDate' => $faker->date('Y-m-d'), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + ]; + + $sanitized = $sanitizer->sanitize($input); + $userInput = UserInput::new($sanitizer, $input); + + // For each property use $expected and $actual then assertSame + $expected = $sanitized['id']; + $actual = $userInput->id; + $this->assertSame($expected, $actual); + + $expected = $sanitized['status']; + $actual = $userInput->status; + $this->assertSame($expected, $actual); + + $expected = $sanitized['email']; + $actual = $userInput->email; + $this->assertSame($expected, $actual); + + $expected = $sanitized['password']; + $actual = $userInput->password; + $this->assertSame($expected, $actual); + + $expected = $sanitized['namePrefix']; + $actual = $userInput->namePrefix; + $this->assertSame($expected, $actual); + + $expected = $sanitized['nameFirst']; + $actual = $userInput->nameFirst; + $this->assertSame($expected, $actual); + + $expected = $sanitized['nameMiddle']; + $actual = $userInput->nameMiddle; + $this->assertSame($expected, $actual); + + $expected = $sanitized['nameLast']; + $actual = $userInput->nameLast; + $this->assertSame($expected, $actual); + + $expected = $sanitized['nameSuffix']; + $actual = $userInput->nameSuffix; + $this->assertSame($expected, $actual); + + $expected = $sanitized['issuer']; + $actual = $userInput->issuer; + $this->assertSame($expected, $actual); + + $expected = $sanitized['tokenPassword']; + $actual = $userInput->tokenPassword; + $this->assertSame($expected, $actual); + + $expected = $sanitized['tokenId']; + $actual = $userInput->tokenId; + $this->assertSame($expected, $actual); + + $expected = $sanitized['preferences']; + $actual = $userInput->preferences; + $this->assertSame($expected, $actual); + + $expected = $sanitized['createdDate']; + $actual = $userInput->createdDate; + $this->assertSame($expected, $actual); + + $expected = $sanitized['createdUserId']; + $actual = $userInput->createdUserId; + $this->assertSame($expected, $actual); + + $expected = $sanitized['updatedDate']; + $actual = $userInput->updatedDate; + $this->assertSame($expected, $actual); + + $expected = $sanitized['updatedUserId']; + $actual = $userInput->updatedUserId; + $this->assertSame($expected, $actual); + + // Verify toArray behaviour: key is the id value per implementation + $expected = [$userInput->id => get_object_vars($userInput)]; + $actual = $userInput->toArray(); + $this->assertSame($expected, $actual); + } +} From 40ee867788692c55ff8dec42fc77aa16901bfa18 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:40:54 -0600 Subject: [PATCH 153/301] [#.x] - refactored middleware after domain transport object changes --- .../ValidateTokenClaimsMiddleware.php | 14 ++++---- .../ValidateTokenRevokedMiddleware.php | 14 ++++---- .../ValidateTokenStructureMiddleware.php | 7 ++-- .../ValidateTokenUserMiddleware.php | 15 ++++---- .../ValidateTokenClaimsMiddlewareTest.php | 35 +++++++++++-------- .../ValidateTokenRevokedMiddlewareTest.php | 34 ++++++++++-------- .../ValidateTokenStructureMiddlewareTest.php | 6 +++- .../ValidateTokenUserMiddlewareTest.php | 17 ++++++--- 8 files changed, 82 insertions(+), 60 deletions(-) diff --git a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php index f659890..bb72854 100644 --- a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php @@ -14,14 +14,14 @@ namespace Phalcon\Api\Domain\Components\Middleware; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; /** * Validates the token claims @@ -39,20 +39,20 @@ public function call(Micro $application): bool { /** @var JWTToken $jwtToken */ $jwtToken = $application->getSharedService(Container::JWT_TOKEN); - /** @var TransportRepository $transport */ - $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + /** @var Registry $registry */ + $registry = $application->getSharedService(Container::REGISTRY); /** * Get the token object */ /** @var Token $tokenObject */ - $tokenObject = $transport->getSessionToken(); + $tokenObject = $registry->get('token'); /** * Our used is in the transport, so we can get it without a * database call */ - /** @var UserTransport $sessionUser */ - $sessionUser = $transport->getSessionUser(); + /** @var User $sessionUser */ + $sessionUser = $registry->get('user'); /** * This is where we validate everything. Even though the user diff --git a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php index 81fddca..ea9be43 100644 --- a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php @@ -15,12 +15,12 @@ use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Http\RequestInterface; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; final class ValidateTokenRevokedMiddleware extends AbstractMiddleware { @@ -37,17 +37,17 @@ public function call(Micro $application): bool $cache = $application->getSharedService(Container::CACHE); /** @var EnvManager $env */ $env = $application->getSharedService(Container::ENV); - /** @var TransportRepository $userTransport */ - $userTransport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + /** @var Registry $registry */ + $registry = $application->getSharedService(Container::REGISTRY); - /** @var UserTransport $user */ - $user = $userTransport->getSessionUser(); + /** @var User $user */ + $domainUser = $registry->get('user'); /** * Get the token object */ $token = $this->getBearerTokenFromHeader($request, $env); - $cacheKey = $cache->getCacheTokenKey($user, $token); + $cacheKey = $cache->getCacheTokenKey($domainUser, $token); $exists = $cache->has($cacheKey); if (true !== $exists) { diff --git a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php index d69eee0..eed5801 100644 --- a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php @@ -23,6 +23,7 @@ use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; final class ValidateTokenStructureMiddleware extends AbstractMiddleware { @@ -63,9 +64,9 @@ public function call(Micro $application): bool /** * If we are down here the token is an object and is valid */ - /** @var TransportRepository $transport */ - $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); - $transport->setSessionToken($token); + /** @var Registry $registry */ + $registry = $application->getSharedService(Container::REGISTRY); + $registry->set('token', $token); return true; } diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php index 9720a05..bc1b12f 100644 --- a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php @@ -23,6 +23,7 @@ use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; /** * @phpstan-import-type TUserDbRecord from UserTypes @@ -43,18 +44,17 @@ public function call(Micro $application): bool $jwtToken = $application->getSharedService(Container::JWT_TOKEN); /** @var QueryRepository $repository */ $repository = $application->getSharedService(Container::REPOSITORY); - /** @var TransportRepository $transport */ - $transport = $application->getSharedService(Container::REPOSITORY_TRANSPORT); + /** @var Registry $registry */ + $registry = $application->getSharedService(Container::REGISTRY); /** * Get the token object */ /** @var Token $tokenObject */ - $tokenObject = $transport->getSessionToken(); - /** @var TUserRecord $dbUser */ - $dbUser = $jwtToken->getUser($repository, $tokenObject); + $tokenObject = $registry->get('token'); + $domainUser = $jwtToken->getUser($repository, $tokenObject); - if (true === empty($dbUser)) { + if (null === $domainUser) { $this->halt( $application, HttpCodesEnum::Unauthorized->value, @@ -70,8 +70,7 @@ public function call(Micro $application): bool * If we are here everything is fine and we need to keep the user * as a "session" user in the transport */ - /** @var TUserDbRecord $dbUser */ - $transport->setSessionUser($dbUser); + $registry->set('user', $domainUser); return true; } diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php index 453701a..bcbfec2 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php @@ -14,10 +14,11 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\DataProvider; @@ -67,26 +68,28 @@ public function testValidateTokenClaimsFailure( array $userData, array $expectedErrors ): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); - $tokenUser = $user; [$micro, $middleware, $jwtToken] = $this->setupTest(); /** * Make the signature non valid */ - $tokenUser = array_replace($tokenUser, $userData); + $tokenUser = array_replace($user, $userData); $token = $this->getUserToken($tokenUser); $tokenObject = $jwtToken->getObject($token); + $domainUser = $userMapper->domain($user); /** - * Store the user in the session + * Store the user in the registry */ - /** @var TransportRepository $transport */ - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); - $transport->setSessionUser($user); - $transport->setSessionToken($tokenObject); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('user', $domainUser); + $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ @@ -113,22 +116,24 @@ public function testValidateTokenClaimsFailure( public function testValidateTokenClaimsSuccess(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); - $tokenUser = $user; + $tokenUser = $userMapper->domain($user); [$micro, $middleware, $jwtToken] = $this->setupTest(); - $token = $this->getUserToken($tokenUser); + $token = $this->getUserToken($user); $tokenObject = $jwtToken->getObject($token); /** - * Store the user in the session + * Store the user in the registry */ - /** @var TransportRepository $transport */ - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); - $transport->setSessionUser($user); - $transport->setSessionToken($tokenObject); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('user', $tokenUser); + $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php index 6c65fdd..8ce166e 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php @@ -15,11 +15,12 @@ use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; use PHPUnit\Framework\Attributes\BackupGlobals; #[BackupGlobals(true)] @@ -27,20 +28,22 @@ final class ValidateTokenRevokedMiddlewareTest extends AbstractUnitTestCase { public function testValidateTokenRevokedFailureInvalidToken(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); - $tokenUser = $user; + $tokenUser = $userMapper->domain($user); [$micro, $middleware] = $this->setupTest(); - $token = $this->getUserToken($tokenUser); + $token = $this->getUserToken($user); /** - * Store the user in the session + * Store the user in the registry */ - /** @var UserTransport $userRepository */ - $userRepository = $micro->getSharedService(Container::REPOSITORY_TRANSPORT); - $userRepository->setSessionUser($user); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('user', $tokenUser); // There is no entry in the cache for this token, so this should fail. $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); @@ -68,23 +71,26 @@ public function testValidateTokenRevokedFailureInvalidToken(): void public function testValidateTokenRevokedSuccess(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); - $tokenUser = $user; + $tokenUser = $userMapper->domain($user); [$micro, $middleware] = $this->setupTest(); - $token = $this->getUserToken($tokenUser); + $token = $this->getUserToken($user); /** - * Store the user in the session + * Store the user in the registry */ - /** @var UserTransport $userRepository */ - $userRepository = $micro->getSharedService(Container::REPOSITORY_TRANSPORT); - $userRepository->setSessionUser($user); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('user', $tokenUser); + /** @var Cache $cache */ $cache = $micro->getSharedService(Container::CACHE); - $sessionUser = $userRepository->getSessionUser(); + $sessionUser = $registry->get('user'); $cacheKey = $cache->getCacheTokenKey($sessionUser, $token); $payload = [ 'token' => $token, diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php index 539aa48..4a7ebbf 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php @@ -14,6 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; @@ -82,13 +83,16 @@ public function testValidateTokenStructureFailureNoDots(): void public function testValidateTokenStructureSuccess(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $userData = $this->getNewUserData(); + $domainUser = $userMapper->domain($userData); [$micro, $middleware] = $this->setupTest(); /** @var JWTToken $jwtToken */ $jwtToken = $micro->getSharedService(Container::JWT_TOKEN); - $token = $jwtToken->getForUser($userData); + $token = $jwtToken->getForUser($domainUser); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ 'REQUEST_METHOD' => 'GET', diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php index 827d519..a207a99 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php @@ -14,10 +14,12 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; +use Phalcon\Support\Registry; use PHPUnit\Framework\Attributes\BackupGlobals; #[BackupGlobals(true)] @@ -30,8 +32,9 @@ public function testValidateTokenUserFailureRecordNotFound(): void $userData['usr_id'] = 1; $token = $this->getUserToken($userData); $tokenObject = $jwtToken->getObject($token); - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); - $transport->setSessionToken($tokenObject); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ @@ -58,15 +61,19 @@ public function testValidateTokenUserFailureRecordNotFound(): void public function testValidateTokenUserSuccess(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); + $domainUser = $userMapper->domain($user); [$micro, $middleware, $jwtToken] = $this->setupTest(); - $token = $jwtToken->getForUser($user); + $token = $jwtToken->getForUser($domainUser); $tokenObject = $jwtToken->getObject($token); - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); - $transport->setSessionToken($tokenObject); + /** @var Registry $registry */ + $registry = $this->container->get(Container::REGISTRY); + $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ From a1d655aae20733f042fccd4d8e6e35619062934e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:41:15 -0600 Subject: [PATCH 154/301] [#.x] - refactored jwt token for new DTOs --- src/Domain/Components/Encryption/JWTToken.php | 47 +++++++++---------- .../Components/Encryption/JWTTokenTest.php | 39 +++++++++------ 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Components/Encryption/JWTToken.php index c4c067b..47908f6 100644 --- a/src/Domain/Components/Encryption/JWTToken.php +++ b/src/Domain/Components/Encryption/JWTToken.php @@ -17,7 +17,7 @@ use InvalidArgumentException; use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; @@ -53,11 +53,11 @@ public function __construct( /** * Returns the string token * - * @param TUserTokenDbRecord $user + * @param User $user * * @return string */ - public function getForUser(array $user): string + public function getForUser(User $user): string { return $this->generateTokenForUser($user); } @@ -87,11 +87,11 @@ public function getObject(string $token): Token /** * Returns the string token * - * @param TUserTokenDbRecord $user + * @param User $user * * @return string */ - public function getRefreshForUser(array $user): string + public function getRefreshForUser(User $user): string { return $this->generateTokenForUser($user, true); } @@ -100,12 +100,12 @@ public function getRefreshForUser(array $user): string * @param QueryRepository $repository * @param Token $token * - * @return TUserRecord + * @return User|null */ public function getUser( QueryRepository $repository, Token $token, - ): array { + ): ?User { /** @var string $issuer */ $issuer = $token->getClaims()->get(JWTEnum::Issuer->value); /** @var string $tokenId */ @@ -120,37 +120,34 @@ public function getUser( 'usr_token_id' => $tokenId, ]; - /** @var TUserRecord $user */ - $user = $repository->user()->findOneBy($criteria); - - return $user; + return $repository->user()->findOneBy($criteria); } /** * Returns an array with the validation errors for this token * - * @param Token $tokenObject - * @param UserTransport $user + * @param Token $tokenObject + * @param User $user * * @return TValidatorErrors */ public function validate( Token $tokenObject, - UserTransport $user + User $user ): array { $validator = new Validator($tokenObject); $signer = new Hmac(); $now = new DateTimeImmutable(); $validator - ->validateId($user->getTokenId()) + ->validateId($user->tokenId) ->validateAudience($this->getTokenAudience()) - ->validateIssuer($user->getIssuer()) + ->validateIssuer($user->issuer) ->validateNotBefore($now->getTimestamp()) ->validateIssuedAt($now->getTimestamp()) ->validateExpiration($now->getTimestamp()) - ->validateSignature($signer, $user->getTokenPassword()) - ->validateClaim(JWTEnum::UserId->value, $user->getId()) + ->validateSignature($signer, $user->tokenPassword) + ->validateClaim(JWTEnum::UserId->value, $user->id) ; /** @var TValidatorErrors $errors */ @@ -162,13 +159,13 @@ public function validate( /** * Returns the string token * - * @param TUserTokenDbRecord $user - * @param bool $isRefresh + * @param User $user + * @param bool $isRefresh * * @return string */ private function generateTokenForUser( - array $user, + User $user, bool $isRefresh = false ): string { /** @var int $expiration */ @@ -187,13 +184,13 @@ private function generateTokenForUser( $tokenBuilder = new Builder(new Hmac()); /** @var string $issuer */ - $issuer = $user['usr_issuer']; + $issuer = $user->issuer; /** @var string $tokenPassword */ - $tokenPassword = $user['usr_token_password']; + $tokenPassword = $user->tokenPassword; /** @var string $tokenId */ - $tokenId = $user['usr_token_id']; + $tokenId = $user->tokenId; /** @var string $userId */ - $userId = $user['usr_id']; + $userId = $user->id; $tokenObject = $tokenBuilder ->setIssuer($issuer) diff --git a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php b/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php index 476ff38..17b517e 100644 --- a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php +++ b/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php @@ -15,8 +15,8 @@ use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -37,18 +37,24 @@ public function setUp(): void public function testGetForUserReturnsTokenString(): void { - $user = $this->getUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - $token = $this->jwtToken->getForUser($user); + $token = $this->jwtToken->getForUser($domainUser); $this->assertIsString($token); $this->assertNotEmpty($token); } public function testGetObjectReturnsPlainToken(): void { - $user = $this->getUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - $tokenString = $this->jwtToken->getForUser($user); + $tokenString = $this->jwtToken->getForUser($domainUser); $plain = $this->jwtToken->getObject($tokenString); $this->assertInstanceOf(Token::class, $plain); @@ -73,9 +79,12 @@ public function testGetObjectThrowsOnInvalidTokenStructure(): void public function testGetUserReturnsUserArray(): void { - $user = $this->getUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - $tokenString = $this->jwtToken->getForUser($user); + $tokenString = $this->jwtToken->getForUser($domainUser); $plain = $this->jwtToken->getObject($tokenString); $userRepository = $this @@ -91,7 +100,7 @@ public function testGetUserReturnsUserArray(): void $userRepository->expects($this->once()) ->method('findOneBy') - ->willReturn($user) + ->willReturn($domainUser) ; $mockRepository = $this @@ -110,20 +119,20 @@ public function testGetUserReturnsUserArray(): void ; $result = $this->jwtToken->getUser($mockRepository, $plain); - $this->assertEquals($user, $result); + $this->assertEquals($domainUser, $result); } public function testValidateSuccess(): void { - $user = $this->getUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - $tokenString = $this->jwtToken->getForUser($user); + $tokenString = $this->jwtToken->getForUser($domainUser); $plain = $this->jwtToken->getObject($tokenString); - $userTransport = new UserTransport($user); - sleep(1); - - $actual = $this->jwtToken->validate($plain, $userTransport); + $actual = $this->jwtToken->validate($plain, $domainUser); $this->assertSame([], $actual); } From e117370b8304f05d5b2aabb2b9b625172048723e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:41:34 -0600 Subject: [PATCH 155/301] [#.x] - added interface for user repository --- .../User/UserRepositoryInterface.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/Domain/Components/DataSource/User/UserRepositoryInterface.php diff --git a/src/Domain/Components/DataSource/User/UserRepositoryInterface.php b/src/Domain/Components/DataSource/User/UserRepositoryInterface.php new file mode 100644 index 0000000..4f6702a --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserRepositoryInterface.php @@ -0,0 +1,71 @@ + + * + * 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\Components\DataSource\User; + +/** + * @phpstan-import-type TUserRecord from UserTypes + * @phpstan-import-type TUserInsert from UserTypes + * @phpstan-import-type TUserUpdate from UserTypes + */ +interface UserRepositoryInterface +{ + /** + * @param array $criteria + * + * @return int + */ + public function deleteBy(array $criteria): int; + + /** + * @param int $recordId + * + * @return int + */ + public function deleteById(int $recordId): int; + + /** + * @param string $email + * + * @return User|null + */ + public function findByEmail(string $email): ?User; + + /** + * @param int $recordId + * + * @return User|null + */ + public function findById(int $recordId): ?User; + + /** + * @param array $criteria + * + * @return User|null + */ + public function findOneBy(array $criteria): ?User; + + /** + * @param User $user + * + * @return int + */ + public function insert(User $user): int; + + /** + * @param User $user + * + * @return int + */ + public function update(User $user): int; +} From cf80dbd3dfdeef77cc85ec0b92ad7c84fd462c58 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:42:10 -0600 Subject: [PATCH 156/301] [#.x] - introduced abstract class - refactoring --- .../DataSource/AbstractRepository.php | 77 ++++--- .../DataSource/User/UserRepository.php | 194 ++++++++++++------ 2 files changed, 165 insertions(+), 106 deletions(-) diff --git a/src/Domain/Components/DataSource/AbstractRepository.php b/src/Domain/Components/DataSource/AbstractRepository.php index 34c6314..c60fe7e 100644 --- a/src/Domain/Components/DataSource/AbstractRepository.php +++ b/src/Domain/Components/DataSource/AbstractRepository.php @@ -16,7 +16,6 @@ use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\DataMapper\Query\Delete; -use Phalcon\DataMapper\Query\Select; /** * @phpstan-import-type TUserRecord from UserTypes @@ -69,42 +68,42 @@ public function deleteById(int $recordId): int ); } - /** - * @param int $recordId - * - * @return TUserRecord - */ - public function findById(int $recordId): array - { - $result = []; - if ($recordId > 0) { - return $this->findOneBy( - [ - $this->idField => $recordId, - ] - ); - } - - return $result; - } - - - /** - * @param array $criteria - * - * @return TUserRecord - */ - public function findOneBy(array $criteria): array - { - $select = Select::new($this->connection); - - /** @var TUserRecord $result */ - $result = $select - ->from($this->table) - ->whereEquals($criteria) - ->fetchOne() - ; - - return $result; - } +// /** +// * @param int $recordId +// * +// * @return TUserRecord +// */ +// public function findById(int $recordId): array +// { +// $result = []; +// if ($recordId > 0) { +// return $this->findOneBy( +// [ +// $this->idField => $recordId, +// ] +// ); +// } +// +// return $result; +// } +// +// +// /** +// * @param array $criteria +// * +// * @return TUserRecord +// */ +// public function findOneBy(array $criteria): array +// { +// $select = Select::new($this->connection); +// +// /** @var TUserRecord $result */ +// $result = $select +// ->from($this->table) +// ->whereEquals($criteria) +// ->fetchOne() +// ; +// +// return $result; +// } } diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index 36d903a..2b9d846 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -13,22 +13,25 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; +use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\DataSource\AbstractRepository; use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; +use Phalcon\DataMapper\Pdo\Connection; use Phalcon\DataMapper\Query\Insert; +use Phalcon\DataMapper\Query\Select; use Phalcon\DataMapper\Query\Update; +use function array_filter; + /** * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserInsert from UserTypes - * @phpstan-import-type TUserUpdate from UserTypes * * The 'final' keyword was intentionally removed from this class to allow * extension for testing purposes (e.g., mocking in unit tests). * * Please avoid extending this class in production code unless absolutely necessary. */ -class UserRepository extends AbstractRepository +class UserRepository extends AbstractRepository implements UserRepositoryInterface { /** * @var string @@ -39,111 +42,168 @@ class UserRepository extends AbstractRepository */ protected string $table = 'co_users'; + public function __construct( + Connection $connection, + private readonly UserMapper $mapper, + ) { + parent::__construct($connection); + } + + /** * @param string $email * - * @return TUserRecord + * @return User|null */ - public function findByEmail(string $email): array + public function findByEmail(string $email): ?User { - $result = []; if (true !== empty($email)) { return $this->findOneBy( [ - 'usr_email' => $email, + 'usr_email' => $email, 'usr_status_flag' => FlagsEnum::Active->value, ] ); } - return $result; + return null; } /** - * @param TUserInsert $userData + * @param int $recordId * - * @return int + * @return User|null */ - public function insert(array $userData): int + public function findById(int $recordId): ?User { - $createdUserId = $userData['createdUserId']; - $updatedUserId = $userData['updatedUserId']; - - $columns = [ - 'usr_status_flag' => $userData['status'], - 'usr_email' => $userData['email'], - 'usr_password' => $userData['password'], - 'usr_name_prefix' => $userData['namePrefix'], - 'usr_name_first' => $userData['nameFirst'], - 'usr_name_middle' => $userData['nameMiddle'], - 'usr_name_last' => $userData['nameLast'], - 'usr_name_suffix' => $userData['nameSuffix'], - 'usr_issuer' => $userData['issuer'], - 'usr_token_password' => $userData['tokenPassword'], - 'usr_token_id' => $userData['tokenId'], - 'usr_preferences' => $userData['preferences'], - 'usr_created_date' => $userData['createdDate'], - 'usr_updated_date' => $userData['updatedDate'], - ]; - - $insert = Insert::new($this->connection); + if ($recordId > 0) { + return $this->findOneBy( + [ + $this->idField => $recordId, + ] + ); + } - $insert - ->into($this->table) - ->columns($columns) + return null; + } + + + /** + * @param array $criteria + * + * @return User|null + */ + public function findOneBy(array $criteria): ?User + { + $select = Select::new($this->connection); + + /** @var TUserRecord $result */ + $result = $select + ->from($this->table) + ->whereEquals($criteria) + ->fetchOne() ; - if ($createdUserId > 0) { - $insert->column('usr_created_usr_id', $createdUserId); + if (empty($result)) { + return null; + } + + return $this->mapper->domain($result); + } + + + /** + * @param User $user + * + * @return int + */ + public function insert(User $user): int + { + $row = $this->mapper->db($user); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + + /** + * @todo this should not be here - the insert should just add data not validate + */ + if (true === empty($row['usr_created_date'])) { + $row['usr_created_date'] = $now; } - if ($updatedUserId > 0) { - $insert->column('usr_updated_usr_id', $updatedUserId); + if (true === empty($row['usr_updated_date'])) { + $row['usr_updated_date'] = $now; } - $insert->perform(); + /** + * Remove usr_id just in case + */ + unset($row['usr_id']); + + /** + * Cleanup empty fields if needed + */ + $columns = $this->cleanupFields($row); + $insert = Insert::new($this->connection); + $insert + ->into($this->table) + ->columns($columns) + ->perform() + ; return (int)$insert->getLastInsertId(); } /** - * @param TUserUpdate $userData + * @param User $user * * @return int */ - public function update(array $userData): int + public function update(User $user): int { - $userId = $userData['id']; - $updatedUserId = $userData['updatedUserId']; - - $columns = [ - 'usr_status_flag' => $userData['status'], - 'usr_email' => $userData['email'], - 'usr_password' => $userData['password'], - 'usr_name_prefix' => $userData['namePrefix'], - 'usr_name_first' => $userData['nameFirst'], - 'usr_name_middle' => $userData['nameMiddle'], - 'usr_name_last' => $userData['nameLast'], - 'usr_name_suffix' => $userData['nameSuffix'], - 'usr_issuer' => $userData['issuer'], - 'usr_token_password' => $userData['tokenPassword'], - 'usr_token_id' => $userData['tokenId'], - 'usr_preferences' => $userData['preferences'], - 'usr_updated_date' => $userData['updatedDate'], - ]; - - $update = Update::new($this->connection); + $row = $this->mapper->db($user); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + $userId = $row['usr_id']; + /** + * @todo this should not be here - the update should just add data not validate + */ + /** + * Set updated date to now if it has not been set + */ + if (true === empty($row['usr_updated_date'])) { + $row['usr_updated_date'] = $now; + } + + /** + * Remove createdDate and createdUserId - cannot be changed. This + * needs to be here because we don't want to touch those fields. + */ + unset($row['usr_created_date'], $row['usr_created_usr_id']); + + /** + * Cleanup empty fields if needed + */ + $columns = $this->cleanupFields($row); + $update = Update::new($this->connection); $update ->table($this->table) ->columns($columns) ->where('usr_id = ', $userId) + ->perform() ; - if ($updatedUserId > 0) { - $update->column('usr_updated_usr_id', $updatedUserId); - } + return $userId; + } - $update->perform(); + /** + * @param array $row + * + * @return array + */ + private function cleanupFields(array $row): array + { + unset($row['usr_id']); - return (int)$userId; + return array_filter( + $row, + static fn($v) => $v !== null && $v !== '' + ); } } From baa077d0d357743a9469b7e10d4e1ddeeb990059 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:42:43 -0600 Subject: [PATCH 157/301] [#.x] - adjusted for new dependencies --- .../Services/User/AbstractUserService.php | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/Domain/Services/User/AbstractUserService.php b/src/Domain/Services/User/AbstractUserService.php index f6d5c84..684a323 100644 --- a/src/Domain/Services/User/AbstractUserService.php +++ b/src/Domain/Services/User/AbstractUserService.php @@ -15,17 +15,62 @@ use Phalcon\Api\Domain\ADR\DomainInterface; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; +use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Filter\Filter; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Payload; + +use function array_shift; abstract class AbstractUserService implements DomainInterface { + protected HttpCodesEnum $errorMessage; + public function __construct( protected readonly QueryRepository $repository, - protected readonly TransportRepository $transport, - protected readonly Filter $filter, + protected readonly UserMapper $mapper, + protected readonly UserValidator $validator, + protected readonly SanitizerInterface $sanitizer, protected readonly Security $security, + ) { } + + /** + * @param string $message + * + * @return Payload + */ + protected function getErrorPayload(string $message): Payload + { + return Payload::error( + [ + $this->errorMessage->text() . $message, + ] + ); + } + + /** + * @param UserInput $inputObject + * + * @return array + */ + protected function processPassword(UserInput $inputObject): User + { + $inputData = $inputObject->toArray(); + $inputData = array_shift($inputData); + + if (null !== $inputData['password']) { + $plain = $inputData['password']; + $hashed = $this->security->hash($plain); + + $inputData['password'] = $hashed; + } + + return $this->mapper->input($inputData); + } } From 61649a76573527795041b3b4287ba900377b9e8a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:43:03 -0600 Subject: [PATCH 158/301] [#.x] - adjusted for new functionality --- tests/Fixtures/Domain/Services/ServiceFixture.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/Fixtures/Domain/Services/ServiceFixture.php b/tests/Fixtures/Domain/Services/ServiceFixture.php index 8e04a05..fc9dee9 100644 --- a/tests/Fixtures/Domain/Services/ServiceFixture.php +++ b/tests/Fixtures/Domain/Services/ServiceFixture.php @@ -13,19 +13,13 @@ namespace Phalcon\Api\Tests\Fixtures\Domain\Services; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\Payload; final readonly class ServiceFixture implements DomainInterface { public function __invoke(array $input): Payload { - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => $input, - ] - ); + return Payload::success($input); } } From c5f465a4c4b76bb94032adc331fc68df84855ce9 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:43:38 -0600 Subject: [PATCH 159/301] [#.x] - refactored for new DTOs --- src/Domain/Services/Auth/LoginPostService.php | 67 ++++++++++--------- .../Services/Auth/LoginPostServiceTest.php | 31 ++++++++- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Services/Auth/LoginPostService.php index d6f9a4f..fe00570 100644 --- a/src/Domain/Services/Auth/LoginPostService.php +++ b/src/Domain/Services/Auth/LoginPostService.php @@ -13,11 +13,12 @@ namespace Phalcon\Api\Domain\Services\Auth; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserDbRecord from UserTypes @@ -35,16 +36,15 @@ public function __invoke(array $input): Payload /** * Get email and password from the input and sanitize them */ - $email = (string)($input['email'] ?? ''); - $password = (string)($input['password'] ?? ''); - $email = $this->filter->string($email); - $password = $this->filter->string($password); + $inputObject = AuthInput::new($this->sanitizer, $input); + $email = $inputObject->email; + $password = $inputObject->password; /** * Check if email or password are empty */ if (true === empty($email) || true === empty($password)) { - return $this->getUnauthorizedPayload( + return Payload::unauthorized( [HttpCodesEnum::AppIncorrectCredentials->error()] ); } @@ -52,18 +52,22 @@ public function __invoke(array $input): Payload /** * Find the user in the database */ - $dbUser = $this->repository->user()->findByEmail($email); - $dbUserId = (int)($dbUser['usr_id'] ?? 0); - $dbPassword = $dbUser['usr_password'] ?? ''; + $domainUser = $this->repository->user()->findByEmail($email); /** - * Check if the user exists and if the password matches + * Check if the user exists */ - if ( - $dbUserId < 1 || - true !== $this->security->verify($password, $dbPassword) - ) { - return $this->getUnauthorizedPayload( + if (null === $domainUser) { + return Payload::unauthorized( + [HttpCodesEnum::AppIncorrectCredentials->error()] + ); + } + + /** + * Check if the password matches + */ + if (true !== $this->security->verify($password, $domainUser->password)) { + return Payload::unauthorized( [HttpCodesEnum::AppIncorrectCredentials->error()] ); } @@ -71,15 +75,8 @@ public function __invoke(array $input): Payload /** * Get a new token for this user */ - /** @var TUserDbRecord $dbUser */ - $token = $this->jwtToken->getForUser($dbUser); - $refreshToken = $this->jwtToken->getRefreshForUser($dbUser); - $domainUser = $this->transport->newUser($dbUser); - $results = $this->transport->newLoginUser( - $domainUser, - $token, - $refreshToken - ); + $token = $this->jwtToken->getForUser($domainUser); + $refreshToken = $this->jwtToken->getRefreshForUser($domainUser); /** * Store the token in cache @@ -90,11 +87,19 @@ public function __invoke(array $input): Payload /** * Send the payload back */ - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => $results, - ] - ); + $results = [ + 'authenticated' => true, + 'user' => [ + 'id' => $domainUser->id, + 'name' => $domainUser->fullName(), + 'email' => $domainUser->email, + ], + 'jwt' => [ + 'token' => $token, + 'refreshToken' => $refreshToken, + ], + ]; + + return Payload::success($results); } } diff --git a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php index 2049fb1..b3d60be 100644 --- a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php @@ -13,6 +13,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; +use Faker\Factory; use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; @@ -109,7 +110,7 @@ public function testServiceWithCredentials(): void $this->assertNotEmpty($jwt['refreshToken']); } - public function testServiceWrongCredentials(): void + public function testServiceWrongCredentialsForUser(): void { /** @var LoginPostService $service */ $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); @@ -140,4 +141,32 @@ public function testServiceWrongCredentials(): void $actual = $actual['errors'][0]; $this->assertSame($expected, $actual); } + + public function testServiceWrongCredentials(): void + { + $faker = Factory::create(); + /** @var LoginPostService $service */ + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + + /** + * Issue a wrong password + */ + $payload = [ + 'email' => $faker->email(), + 'password' => $faker->password(), + ]; + + $payload = $service->__invoke($payload); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppIncorrectCredentials->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } } From 496fca0d774f29adb71cb4ef96b8eadce12b807e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:43:52 -0600 Subject: [PATCH 160/301] [#.x] - refactored for new DTOs --- .../Services/Auth/LogoutPostService.php | 32 ++- .../Services/Auth/RefreshPostService.php | 46 ++--- .../Services/Auth/LogoutPostServiceTest.php | 185 ++++++++++-------- .../Services/Auth/RefreshPostServiceTest.php | 172 ++++++++-------- 4 files changed, 224 insertions(+), 211 deletions(-) diff --git a/src/Domain/Services/Auth/LogoutPostService.php b/src/Domain/Services/Auth/LogoutPostService.php index 1bec8d1..0a50524 100644 --- a/src/Domain/Services/Auth/LogoutPostService.php +++ b/src/Domain/Services/Auth/LogoutPostService.php @@ -13,12 +13,12 @@ namespace Phalcon\Api\Domain\Services\Auth; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserDbRecord from UserTypes @@ -40,8 +40,8 @@ public function __invoke(array $input): Payload /** * Get the token */ - $token = (string)($input['token'] ?? ''); - $token = $this->filter->string($token); + $inputObject = AuthInput::new($this->sanitizer, $input); + $token = $inputObject->token; /** * Validation @@ -49,7 +49,7 @@ public function __invoke(array $input): Payload * Empty token */ if (true === empty($token)) { - return $this->getUnauthorizedPayload( + return Payload::unauthorized( [HttpCodesEnum::AppTokenNotPresent->error()] ); } @@ -62,7 +62,7 @@ public function __invoke(array $input): Payload $tokenObject = $this->jwtToken->getObject($token); $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); if (false === $isRefresh) { - return $this->getUnauthorizedPayload( + return Payload::unauthorized( [HttpCodesEnum::AppTokenNotValid->error()] ); } @@ -70,22 +70,21 @@ public function __invoke(array $input): Payload /** * Get the user - if empty return error */ - $user = $this + $domainUser = $this ->jwtToken ->getUser($this->repository, $tokenObject) ; - if (true === empty($user)) { - return $this->getUnauthorizedPayload( + + if (null === $domainUser) { + return Payload::unauthorized( [HttpCodesEnum::AppTokenInvalidUser->error()] ); } - $domainUser = $this->transport->newUser($user); - /** @var TValidationErrors $errors */ $errors = $this->jwtToken->validate($tokenObject, $domainUser); if (true !== empty($errors)) { - return $this->getUnauthorizedPayload($errors); + return Payload::unauthorized($errors); } /** @@ -96,13 +95,10 @@ public function __invoke(array $input): Payload /** * Send the payload back */ - return new Payload( - DomainStatus::SUCCESS, + return Payload::success( [ - 'data' => [ - 'authenticated' => false, - ], - ] + 'authenticated' => false, + ], ); } } diff --git a/src/Domain/Services/Auth/RefreshPostService.php b/src/Domain/Services/Auth/RefreshPostService.php index 2431936..63b978f 100644 --- a/src/Domain/Services/Auth/RefreshPostService.php +++ b/src/Domain/Services/Auth/RefreshPostService.php @@ -13,12 +13,12 @@ namespace Phalcon\Api\Domain\Services\Auth; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserDbRecord from UserTypes @@ -37,8 +37,8 @@ public function __invoke(array $input): Payload /** * Get email and password from the input and sanitize them */ - $token = (string)($input['token'] ?? ''); - $token = $this->filter->string($token); + $inputObject = AuthInput::new($this->sanitizer, $input); + $token = $inputObject->token; /** * Validation @@ -46,7 +46,7 @@ public function __invoke(array $input): Payload * Empty token */ if (true === empty($token)) { - return $this->getUnauthorizedPayload( + return Payload::unauthorized( [HttpCodesEnum::AppTokenNotPresent->error()] ); } @@ -59,7 +59,7 @@ public function __invoke(array $input): Payload $tokenObject = $this->jwtToken->getObject($token); $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); if (false === $isRefresh) { - return $this->getUnauthorizedPayload( + return Payload::unauthorized( [HttpCodesEnum::AppTokenNotValid->error()] ); } @@ -67,35 +67,24 @@ public function __invoke(array $input): Payload /** * Get the user - if empty return error */ - $user = $this + $domainUser = $this ->jwtToken ->getUser($this->repository, $tokenObject) ; - if (true === empty($user)) { - return $this->getUnauthorizedPayload( + if (null === $domainUser) { + return Payload::unauthorized( [HttpCodesEnum::AppTokenInvalidUser->error()] ); } - $domainUser = $this->transport->newUser($user); - /** @var TValidationErrors $errors */ $errors = $this->jwtToken->validate($tokenObject, $domainUser); if (true !== empty($errors)) { - return $this->getUnauthorizedPayload($errors); + return Payload::unauthorized($errors); } - /** - * @todo change this to be the domain user - */ - $userPayload = [ - 'usr_issuer' => $domainUser->getIssuer(), - 'usr_token_password' => $domainUser->getTokenPassword(), - 'usr_token_id' => $domainUser->getTokenId(), - 'usr_id' => $domainUser->getId(), - ]; - $newToken = $this->jwtToken->getForUser($userPayload); - $newRefreshToken = $this->jwtToken->getRefreshForUser($userPayload); + $newToken = $this->jwtToken->getForUser($domainUser); + $newRefreshToken = $this->jwtToken->getRefreshForUser($domainUser); /** * Invalidate old tokens, store new tokens in cache @@ -107,14 +96,11 @@ public function __invoke(array $input): Payload /** * Send the payload back */ - return new Payload( - DomainStatus::SUCCESS, + return Payload::success( [ - 'data' => [ - 'token' => $newToken, - 'refreshToken' => $newRefreshToken, - ], - ] + 'token' => $newToken, + 'refreshToken' => $newRefreshToken, + ], ); } } diff --git a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php index 75f9637..57b9be8 100644 --- a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php @@ -16,7 +16,7 @@ use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Services\Auth\LoginPostService; @@ -51,49 +51,56 @@ public function testServiceEmptyToken(): void public function testServiceInvalidToken(): void { - $user = $this->getNewUserData(); - $errors = [ + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $user = $this->getNewUserData(); + $user['usr_id'] = 1; + $domainUser = $userMapper->domain($user); + $errors = [ ['Incorrect token data'], ]; /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(true); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - 'getUser', - 'validate', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + 'validate', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); - $mockJWT->method('getUser')->willReturn($user); + $mockJWT->method('getUser')->willReturn($domainUser); $mockJWT->method('validate')->willReturn($errors); @@ -124,36 +131,39 @@ public function testServiceNotRefreshToken(): void /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(false); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); @@ -185,14 +195,14 @@ public function testServiceNotRefreshToken(): void public function testServiceSuccess(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); /** @var LogoutPostService $service */ $logoutService = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); /** @var Cache $cache */ $cache = $this->container->getShared(Container::CACHE); /** @var LoginPostService $service */ - $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->getShared(Container::REPOSITORY_TRANSPORT); + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); $migration = new UsersMigration($this->getConnection()); /** @@ -226,7 +236,7 @@ public function testServiceSuccess(): void $this->assertTrue($actual); $token = $data['jwt']['token']; - $domainUser = $transport->newUser($dbUser); + $domainUser = $userMapper->domain($dbUser); $tokenKey = $cache->getCacheTokenKey($domainUser, $token); $actual = $cache->has($tokenKey); @@ -271,43 +281,50 @@ public function testServiceSuccess(): void public function testServiceWrongUser(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $domainUser = $userMapper->domain([]); + /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(true); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - 'getUser', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); - $mockJWT->method('getUser')->willReturn([]); + $mockJWT->method('getUser')->willReturn(null); /** diff --git a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php b/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php index f274adb..406b74d 100644 --- a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php @@ -15,6 +15,7 @@ use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Services\Auth\LoginPostService; @@ -49,49 +50,56 @@ public function testServiceEmptyToken(): void public function testServiceInvalidToken(): void { - $user = $this->getNewUserData(); - $errors = [ + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $user = $this->getNewUserData(); + $user['usr_id'] = 1; + $domainUser = $userMapper->domain($user); + $errors = [ ['Incorrect token data'], ]; /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(true); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - 'getUser', - 'validate', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + 'validate', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); - $mockJWT->method('getUser')->willReturn($user); + $mockJWT->method('getUser')->willReturn($domainUser); $mockJWT->method('validate')->willReturn($errors); @@ -122,36 +130,39 @@ public function testServiceNotRefreshToken(): void /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(false); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); @@ -231,40 +242,43 @@ public function testServiceWrongUser(): void /** * Set up mock services */ - $mockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'get', - ] - ) - ->getMock() + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() ; $mockItem->method('get')->willReturn(true); - $mockToken = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getClaims', - ] - ) - ->getMock() + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() ; $mockToken->method('getClaims')->willReturn($mockItem); - $mockJWT = $this->getMockBuilder(JWTToken::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getObject', - 'getUser', - ] - ) - ->getMock() + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + ] + ) + ->getMock() ; $mockJWT->method('getObject')->willReturn($mockToken); - $mockJWT->method('getUser')->willReturn([]); + $mockJWT->method('getUser')->willReturn(null); /** From 38f7827848d70fa09caac33f8e396cefc68247b1 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:44:20 -0600 Subject: [PATCH 161/301] [#.x] - refactored for new DTOs --- .../Services/User/UserDeleteService.php | 25 +- src/Domain/Services/User/UserGetService.php | 30 +-- src/Domain/Services/User/UserPostService.php | 124 +--------- src/Domain/Services/User/UserPutService.php | 131 ++-------- .../DataSource/User/UserRepositoryTest.php | 37 +-- .../Services/User/UserServiceDispatchTest.php | 9 +- .../Services/User/UserServicePostTest.php | 101 ++++---- .../Services/User/UserServicePutTest.php | 228 ++++++++++++------ 8 files changed, 277 insertions(+), 408 deletions(-) diff --git a/src/Domain/Services/User/UserDeleteService.php b/src/Domain/Services/User/UserDeleteService.php index 77b45bd..72b55f1 100644 --- a/src/Domain/Services/User/UserDeleteService.php +++ b/src/Domain/Services/User/UserDeleteService.php @@ -13,9 +13,9 @@ namespace Phalcon\Api\Domain\Services\User; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; +use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserInput from InputTypes @@ -29,7 +29,8 @@ final class UserDeleteService extends AbstractUserService */ public function __invoke(array $input): Payload { - $userId = $this->filter->absint($input['id'] ?? 0); + $inputObject = UserInput::new($this->sanitizer, $input); + $userId = $inputObject->id; /** * Success @@ -38,13 +39,10 @@ public function __invoke(array $input): Payload $rows = $this->repository->user()->deleteById($userId); if ($rows > 0) { - return new Payload( - DomainStatus::DELETED, + return Payload::deleted( [ - 'data' => [ - 'Record deleted successfully [#' . $userId . '].', - ], - ] + 'Record deleted successfully [#' . $userId . '].', + ], ); } } @@ -52,13 +50,6 @@ public function __invoke(array $input): Payload /** * 404 */ - return new Payload( - DomainStatus::NOT_FOUND, - [ - 'errors' => [ - 'Record(s) not found', - ], - ] - ); + return Payload::notFound(['Record(s) not found']); } } diff --git a/src/Domain/Services/User/UserGetService.php b/src/Domain/Services/User/UserGetService.php index c71735b..b487174 100644 --- a/src/Domain/Services/User/UserGetService.php +++ b/src/Domain/Services/User/UserGetService.php @@ -13,9 +13,9 @@ namespace Phalcon\Api\Domain\Services\User; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; +use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserInput from InputTypes @@ -29,35 +29,23 @@ final class UserGetService extends AbstractUserService */ public function __invoke(array $input): Payload { - $userId = $this->filter->absint($input['id'] ?? 0); + $inputObject = UserInput::new($this->sanitizer, $input); + $userId = $inputObject->id; /** * Success */ if ($userId > 0) { - $dbUser = $this->repository->user()->findById($userId); - $user = $this->transport->newUser($dbUser); - - if (true !== $user->isEmpty()) { - return new Payload( - DomainStatus::SUCCESS, - [ - 'data' => $user->toArray(), - ] - ); + $user = $this->repository->user()->findById($userId); + + if (null !== $user) { + return Payload::success($user->toArray()); } } /** * 404 */ - return new Payload( - DomainStatus::NOT_FOUND, - [ - 'errors' => [ - 'Record(s) not found', - ], - ] - ); + return Payload::notFound(['Record(s) not found']); } } diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Services/User/UserPostService.php index 499cc04..da19d0d 100644 --- a/src/Domain/Services/User/UserPostService.php +++ b/src/Domain/Services/User/UserPostService.php @@ -13,12 +13,11 @@ namespace Phalcon\Api\Domain\Services\User; -use PayloadInterop\DomainStatus; use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserSanitizedInsertInput from InputTypes @@ -27,6 +26,8 @@ */ final class UserPostService extends AbstractUserService { + protected HttpCodesEnum $errorMessage = HttpCodesEnum::AppCannotCreateDatabaseRecord; + /** * @param TUserInput $input * @@ -34,28 +35,20 @@ final class UserPostService extends AbstractUserService */ public function __invoke(array $input): Payload { - $inputData = $this->sanitizeInput($input); - $errors = $this->validateInput($inputData); + $inputObject = UserInput::new($this->sanitizer, $input); + $errors = $this->validator->validate($inputObject); /** * Errors exist - return early */ - if (!empty($errors)) { - return new Payload( - DomainStatus::INVALID, - [ - 'errors' => $errors, - ] - ); + if (true !== empty($errors)) { + return Payload::invalid($errors); } /** * The password needs to be hashed */ - $password = $inputData['password']; - $hashed = $this->security->hash($password); - - $inputData['password'] = $hashed; + $domainUser = $this->processPassword($inputObject); /** * Insert the record @@ -64,7 +57,7 @@ public function __invoke(array $input): Payload $userId = $this ->repository ->user() - ->insert($inputData) + ->insert($domainUser) ; } catch (PDOException $ex) { /** @@ -80,104 +73,11 @@ public function __invoke(array $input): Payload /** * Get the user from the database */ - $dbUser = $this->repository->user()->findById($userId); - $domainUser = $this->transport->newUser($dbUser); + $domainUser = $this->repository->user()->findById($userId); /** * Return the user back */ - return new Payload( - DomainStatus::CREATED, - [ - 'data' => $domainUser->toArray(), - ] - ); - } - - /** - * @param string $message - * - * @return Payload - */ - private function getErrorPayload(string $message): Payload - { - return new Payload( - DomainStatus::ERROR, - [ - 'errors' => [ - HttpCodesEnum::AppCannotCreateDatabaseRecord->text() - . $message, - ], - ] - ); - } - - /** - * @param TUserInput $input - * - * @return TUserSanitizedInsertInput - */ - private function sanitizeInput(array $input): array - { - /** - * Only the fields we want - * - * @todo add sanitizers here - * @todo maybe this is another domain object? - */ - $sanitized = [ - 'status' => $input['status'] ?? 0, - 'email' => $input['email'] ?? '', - 'password' => $input['password'] ?? '', - 'namePrefix' => $input['namePrefix'] ?? '', - 'nameFirst' => $input['nameFirst'] ?? '', - 'nameLast' => $input['nameLast'] ?? '', - 'nameMiddle' => $input['nameMiddle'] ?? '', - 'nameSuffix' => $input['nameSuffix'] ?? '', - 'issuer' => $input['issuer'] ?? '', - 'tokenPassword' => $input['tokenPassword'] ?? '', - 'tokenId' => $input['tokenId'] ?? '', - 'preferences' => $input['preferences'] ?? '', - 'createdDate' => $input['createdDate'] ?? null, - 'createdUserId' => $input['createdUserId'] ?? 0, - 'updatedDate' => $input['updatedDate'] ?? null, - 'updatedUserId' => $input['updatedUserId'] ?? 0, - ]; - - if (empty($sanitized['createdDate'])) { - $sanitized['createdDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); - } - - if (empty($sanitized['updatedDate'])) { - $sanitized['updatedDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); - } - - return $sanitized; - } - - /** - * @param TUserSanitizedInsertInput $inputData - * - * @return TValidationErrors|array{} - */ - private function validateInput(array $inputData): array - { - $errors = []; - $required = [ - 'email', - 'password', - 'issuer', - 'tokenPassword', - 'tokenId', - ]; - - foreach ($required as $name) { - $field = $inputData[$name]; - if (true === empty($field)) { - $errors[] = ['Field ' . $name . ' cannot be empty.']; - } - } - - return $errors; + return Payload::created($domainUser->toArray()); } } diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php index 22ad499..44c7366 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Services/User/UserPutService.php @@ -13,12 +13,11 @@ namespace Phalcon\Api\Domain\Services\User; -use PayloadInterop\DomainStatus; use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserSanitizedUpdateInput from InputTypes @@ -27,6 +26,8 @@ */ final class UserPutService extends AbstractUserService { + protected HttpCodesEnum $errorMessage = HttpCodesEnum::AppCannotUpdateDatabaseRecord; + /** * @param TUserInput $input * @@ -34,41 +35,39 @@ final class UserPutService extends AbstractUserService */ public function __invoke(array $input): Payload { - $inputData = $this->sanitizeInput($input); - $errors = $this->validateInput($inputData); + $inputObject = UserInput::new($this->sanitizer, $input); + $errors = $this->validator->validate($inputObject); /** * Errors exist - return early */ - if (!empty($errors)) { - return new Payload( - DomainStatus::INVALID, - [ - 'errors' => $errors, - ] - ); + if (true !== empty($errors)) { + return Payload::invalid($errors); } /** - * The password needs to be hashed + * Check if the user exists, If not, return an error */ - $password = $inputData['password']; - $hashed = $this->security->hash($password); + $userId = $inputObject->id; + $domainUser = $this->repository->user()->findById($userId); - $inputData['password'] = $hashed; + if (null === $domainUser) { + return Payload::notFound(['Record(s) not found']); + } /** - * Update the record + * The password needs to be hashed */ + $domainUser = $this->processPassword($inputObject); + /** - * @todo get the user from the database to make sure that it is valid + * Update the record */ - try { $userId = $this ->repository ->user() - ->update($inputData) + ->update($domainUser) ; } catch (PDOException $ex) { /** @@ -84,99 +83,11 @@ public function __invoke(array $input): Payload /** * Get the user from the database */ - $dbUser = $this->repository->user()->findById($userId); - $domainUser = $this->transport->newUser($dbUser); + $domainUser = $this->repository->user()->findById($userId); /** * Return the user back */ - return new Payload( - DomainStatus::UPDATED, - [ - 'data' => $domainUser->toArray(), - ] - ); - } - - /** - * @param string $message - * - * @return Payload - */ - private function getErrorPayload(string $message): Payload - { - return new Payload( - DomainStatus::ERROR, - [ - 'errors' => [ - HttpCodesEnum::AppCannotUpdateDatabaseRecord->text() - . $message, - ], - ] - ); - } - - /** - * @param TUserInput $input - * - * @return TUserSanitizedUpdateInput - */ - private function sanitizeInput(array $input): array - { - /** - * Only the fields we want - * - * @todo add sanitizers here - * @todo maybe this is another domain object? - */ - $sanitized = [ - 'id' => $input['id'] ?? 0, - 'status' => $input['status'] ?? 0, - 'email' => $input['email'] ?? '', - 'password' => $input['password'] ?? '', - 'namePrefix' => $input['namePrefix'] ?? '', - 'nameFirst' => $input['nameFirst'] ?? '', - 'nameLast' => $input['nameLast'] ?? '', - 'nameMiddle' => $input['nameMiddle'] ?? '', - 'nameSuffix' => $input['nameSuffix'] ?? '', - 'issuer' => $input['issuer'] ?? '', - 'tokenPassword' => $input['tokenPassword'] ?? '', - 'tokenId' => $input['tokenId'] ?? '', - 'preferences' => $input['preferences'] ?? '', - 'updatedDate' => $input['updatedDate'] ?? null, - 'updatedUserId' => $input['updatedUserId'] ?? 0, - ]; - - if (empty($sanitized['updatedDate'])) { - $sanitized['updatedDate'] = Dates::toUTC('now', Dates::DATE_TIME_FORMAT); - } - - return $sanitized; - } - - /** - * @param TUserSanitizedUpdateInput $inputData - * - * @return TValidationErrors|array{} - */ - private function validateInput(array $inputData): array - { - $errors = []; - $required = [ - 'email', - 'password', - 'issuer', - 'tokenPassword', - 'tokenId', - ]; - - foreach ($required as $name) { - $field = $inputData[$name]; - if (true === empty($field)) { - $errors[] = ['Field ' . $name . ' cannot be empty.']; - } - } - - return $errors; + return Payload::updated($domainUser->toArray()); } } diff --git a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php b/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php index 1443a5e..569a63c 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php @@ -15,6 +15,7 @@ use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; @@ -56,74 +57,74 @@ public function testFindById(): void $this->runAssertions($migrationUser, $repositoryUser); } - private function runAssertions(array $dbUser, array $user): void + private function runAssertions(array $dbUser, User $user): void { $expected = $dbUser['usr_id']; - $actual = $user['usr_id']; + $actual = $user->id; $this->assertSame($expected, $actual); $expected = $dbUser['usr_status_flag']; - $actual = $user['usr_status_flag']; + $actual = $user->status; $this->assertSame($expected, $actual); $expected = $dbUser['usr_email']; - $actual = $user['usr_email']; + $actual = $user->email; $this->assertSame($expected, $actual); $expected = $dbUser['usr_password']; - $actual = $user['usr_password']; + $actual = $user->password; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_prefix']; - $actual = $user['usr_name_prefix']; + $actual = $user->namePrefix; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_first']; - $actual = $user['usr_name_first']; + $actual = $user->nameFirst; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_middle']; - $actual = $user['usr_name_middle']; + $actual = $user->nameMiddle; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_last']; - $actual = $user['usr_name_last']; + $actual = $user->nameLast; $this->assertSame($expected, $actual); $expected = $dbUser['usr_name_suffix']; - $actual = $user['usr_name_suffix']; + $actual = $user->nameSuffix; $this->assertSame($expected, $actual); $expected = $dbUser['usr_issuer']; - $actual = $user['usr_issuer']; + $actual = $user->issuer; $this->assertSame($expected, $actual); $expected = $dbUser['usr_token_password']; - $actual = $user['usr_token_password']; + $actual = $user->tokenPassword; $this->assertSame($expected, $actual); $expected = $dbUser['usr_token_id']; - $actual = $user['usr_token_id']; + $actual = $user->tokenId; $this->assertSame($expected, $actual); $expected = $dbUser['usr_preferences']; - $actual = $user['usr_preferences']; + $actual = $user->preferences; $this->assertSame($expected, $actual); $expected = $dbUser['usr_created_date']; - $actual = $user['usr_created_date']; + $actual = $user->createdDate; $this->assertSame($expected, $actual); $expected = $dbUser['usr_created_usr_id']; - $actual = $user['usr_created_usr_id']; + $actual = $user->createdUserId; $this->assertSame($expected, $actual); $expected = $dbUser['usr_updated_date']; - $actual = $user['usr_updated_date']; + $actual = $user->updatedDate; $this->assertSame($expected, $actual); $expected = $dbUser['usr_updated_usr_id']; - $actual = $user['usr_updated_usr_id']; + $actual = $user->updatedUserId; $this->assertSame($expected, $actual); } } diff --git a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php index 2557457..0167686 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php @@ -16,6 +16,7 @@ use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -31,14 +32,14 @@ public function testDispatchGet(): void $env = $this->container->getShared(Container::ENV); /** @var Cache $cache */ $cache = $this->container->getShared(Container::CACHE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); $userId = $dbUser['usr_id']; $token = $this->getUserToken($dbUser); - $domainUser = $transport->newUser($dbUser); + $domainUser = $userMapper->domain($dbUser); /** * Store the token in the cache @@ -75,7 +76,7 @@ public function testDispatchGet(): void $actual = $errors; $this->assertSame($expected, $actual); - $user = $transport->newUser($dbUser); + $user = $userMapper->domain($dbUser); $expected = $user->toArray(); $actual = $data; $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Services/User/UserServicePostTest.php index 0cbd8f4..653de11 100644 --- a/tests/Unit/Domain/Services/User/UserServicePostTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePostTest.php @@ -18,14 +18,15 @@ use PDOException; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Services\User\UserPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Filter; use PHPUnit\Framework\Attributes\BackupGlobals; +use function array_shift; +use function htmlspecialchars; + #[BackupGlobals(true)] final class UserServicePostTest extends AbstractUnitTestCase { @@ -41,10 +42,7 @@ public function testServiceFailureNoIdReturned(): void ) ->getMock() ; - $userRepository - ->method('insert') - ->willReturn(0) - ; + $userRepository->method('insert')->willReturn(0); $repository = $this ->getMockBuilder(QueryRepository::class) @@ -56,30 +54,24 @@ public function testServiceFailureNoIdReturned(): void ) ->getMock() ; - $repository - ->method('user') - ->willReturn($userRepository) - ; + $repository->method('user')->willReturn($userRepository); - /** - * Difference of achieving the same thing - see test above on - * how the class is used without getting it from the DI container - */ $this->container->setShared(Container::REPOSITORY, $repository); /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); - /** @var TransportRepository $service */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -129,23 +121,22 @@ public function testServiceFailurePdoError(): void ->willReturn($userRepository) ; - /** @var Filter $filter */ - $filter = $this->container->get(Container::FILTER); - /** @var Security $security */ - $security = $this->container->get(Container::SECURITY); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + $this->container->set(Container::REPOSITORY, $repository); - $service = new UserPostService($repository, $transport, $filter, $security); + /** @var UserPostService $service */ + $service = $this->container->get(Container::USER_POST_SERVICE); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -167,8 +158,8 @@ public function testServiceFailureValidation(): void { /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $userData = $this->getNewUserData(); @@ -183,7 +174,7 @@ public function testServiceFailureValidation(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); $domainData = $domainData[0]; @@ -213,8 +204,8 @@ public function testServiceSuccess(): void { /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $userData = $this->getNewUserData(); $userData['usr_created_usr_id'] = 4; @@ -223,9 +214,9 @@ public function testServiceSuccess(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -255,23 +246,23 @@ public function testServiceSuccess(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = $domainData['namePrefix']; + $expected = htmlspecialchars($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = $domainData['nameFirst']; + $expected = htmlspecialchars($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = $domainData['nameMiddle']; + $expected = htmlspecialchars($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = $domainData['nameLast']; + $expected = htmlspecialchars($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = $domainData['nameSuffix']; + $expected = htmlspecialchars($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -287,9 +278,8 @@ public function testServiceSuccess(): void $actual = $data['tokenId']; $this->assertSame($expected, $actual); - $expected = $domainData['preferences']; - $actual = $data['preferences']; - $this->assertSame($expected, $actual); + $actual = $data['preferences']; + $this->assertNull($actual); $expected = $domainData['createdDate']; $actual = $data['createdDate']; @@ -314,8 +304,8 @@ public function testServiceSuccessEmptyDates(): void $today = $now->format('Y-m-d'); /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $userData = $this->getNewUserData(); unset( @@ -326,7 +316,7 @@ public function testServiceSuccessEmptyDates(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); $domainData = $domainData[0]; @@ -358,23 +348,23 @@ public function testServiceSuccessEmptyDates(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = $domainData['namePrefix']; + $expected = htmlspecialchars($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = $domainData['nameFirst']; + $expected = htmlspecialchars($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = $domainData['nameMiddle']; + $expected = htmlspecialchars($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = $domainData['nameLast']; + $expected = htmlspecialchars($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = $domainData['nameSuffix']; + $expected = htmlspecialchars($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -390,9 +380,8 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['tokenId']; $this->assertSame($expected, $actual); - $expected = $domainData['preferences']; - $actual = $data['preferences']; - $this->assertSame($expected, $actual); + $actual = $data['preferences']; + $this->assertNull($actual); $actual = $data['createdDate']; $this->assertStringContainsString($today, $actual); diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php index 7fe18af..1f6a945 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -13,35 +13,111 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; +use DateTimeImmutable; use PayloadInterop\DomainStatus; use PDOException; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Services\User\UserPutService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; -use Phalcon\Filter\Filter; use PHPUnit\Framework\Attributes\BackupGlobals; +use function array_shift; + #[BackupGlobals(true)] final class UserServicePutTest extends AbstractUnitTestCase { + public function testServiceFailureRecordNotFound(): void + { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $findByUser = $userMapper->domain($userData); + + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'findById' + ] + ) + ->getMock() + ; + $userRepository->method('findById')->willReturn(null); + + $repository = $this + ->getMockBuilder(QueryRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'user', + ] + ) + ->getMock() + ; + $repository->method('user')->willReturn($userRepository); + + $this->container->setShared(Container::REPOSITORY, $repository); + + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); + + /** + * Update user + */ + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + + $updateUser = $userMapper->domain($userData); + $updateUser = $updateUser->toArray(); + $updateUser = array_shift($updateUser); + + $payload = $service->__invoke($updateUser); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = ['Record(s) not found']; + $actual = $errors; + $this->assertSame($expected, $actual); + } + public function testServiceFailureNoIdReturned(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $findByUser = $userMapper->domain($userData); + $userRepository = $this ->getMockBuilder(UserRepository::class) ->disableOriginalConstructor() ->onlyMethods( [ 'update', + 'findById' ] ) ->getMock() ; $userRepository->method('update')->willReturn(0); + $userRepository->method('findById')->willReturn($findByUser); $repository = $this ->getMockBuilder(QueryRepository::class) @@ -55,25 +131,22 @@ public function testServiceFailureNoIdReturned(): void ; $repository->method('user')->willReturn($userRepository); - /** @var Filter $filter */ - $filter = $this->container->get(Container::FILTER); - /** @var Security $security */ - $security = $this->container->get(Container::SECURITY); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $service = new UserPutService($repository, $transport, $filter, $security); + $this->container->setShared(Container::REPOSITORY, $repository); - $userData = $this->getNewUserData(); + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); /** - * $userData is a db record. We need a domain object here + * Update user */ - $domainUser = $transport->newUser($userData); - $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; - $payload = $service->__invoke($domainData); + $updateUser = $userMapper->domain($userData); + $updateUser = $updateUser->toArray(); + $updateUser = array_shift($updateUser); + + $payload = $service->__invoke($updateUser); $expected = DomainStatus::ERROR; $actual = $payload->getStatus(); @@ -91,16 +164,26 @@ public function testServiceFailureNoIdReturned(): void public function testServiceFailurePdoError(): void { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $findByUser = $userMapper->domain($userData); + $userRepository = $this ->getMockBuilder(UserRepository::class) ->disableOriginalConstructor() ->onlyMethods( [ 'update', + 'findById' ] ) ->getMock() ; + $userRepository->method('findById')->willReturn($findByUser); $userRepository ->method('update') ->willThrowException(new PDOException('abcde')) @@ -121,25 +204,21 @@ public function testServiceFailurePdoError(): void ->willReturn($userRepository) ; - /** @var Filter $filter */ - $filter = $this->container->get(Container::FILTER); - /** @var Security $security */ - $security = $this->container->get(Container::SECURITY); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $service = new UserPutService($repository, $transport, $filter, $security); + $this->container->set(Container::REPOSITORY, $repository); + /** @var UserPutService $service */ + $service = $this->container->get(Container::USER_PUT_SERVICE); - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); - $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $updateUser = $userMapper->domain($userData); + $updateUser = $updateUser->toArray(); + $updateUser = array_shift($updateUser); - $payload = $service->__invoke($domainData); + $payload = $service->__invoke($updateUser); $expected = DomainStatus::ERROR; $actual = $payload->getStatus(); @@ -159,10 +238,9 @@ public function testServiceFailureValidation(): void { /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $userData = $this->getNewUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + $userData = $this->getNewUserData(); unset( $userData['usr_email'], @@ -175,7 +253,7 @@ public function testServiceFailureValidation(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); $domainData = $domainData[0]; @@ -205,13 +283,18 @@ public function testServiceSuccess(): void { /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); - - $migration = new UsersMigration($this->getConnection()); - $dbUser = $this->getNewUser($migration); - $userId = $dbUser['usr_id']; - $userData = $this->getNewUserData(); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + $userData = $this->getNewUserData(); + $userData['usr_id'] = $userId; + /** + * Don't hash the password + */ + $userData['usr_password'] = $this->getStrongPassword(); $userData['usr_created_usr_id'] = 4; $userData['usr_updated_usr_id'] = 5; @@ -219,11 +302,9 @@ public function testServiceSuccess(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; - - $domainData['id'] = $userId; + $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -251,23 +332,23 @@ public function testServiceSuccess(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = $domainData['namePrefix']; + $expected = htmlspecialchars($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = $domainData['nameFirst']; + $expected = htmlspecialchars($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = $domainData['nameMiddle']; + $expected = htmlspecialchars($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = $domainData['nameLast']; + $expected = htmlspecialchars($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = $domainData['nameSuffix']; + $expected = htmlspecialchars($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -283,7 +364,7 @@ public function testServiceSuccess(): void $actual = $data['tokenId']; $this->assertSame($expected, $actual); - $expected = $domainData['preferences']; + $expected = ''; $actual = $data['preferences']; $this->assertSame($expected, $actual); @@ -295,9 +376,9 @@ public function testServiceSuccess(): void $actual = $data['createdUserId']; $this->assertSame($expected, $actual); - $expected = $dbUser['usr_updated_date']; + $expected = $domainData['updatedDate']; $actual = $data['updatedDate']; - $this->assertNotSame($expected, $actual); + $this->assertSame($expected, $actual); $expected = 5; $actual = $data['updatedUserId']; @@ -306,15 +387,24 @@ public function testServiceSuccess(): void public function testServiceSuccessEmptyDates(): void { + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); - /** @var TransportRepository $transport */ - $transport = $this->container->get(Container::REPOSITORY_TRANSPORT); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(Container::USER_MAPPER); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); - $userId = $dbUser['usr_id']; - $userData = $this->getNewUserData(); + + $userId = $dbUser['usr_id']; + $userData = $this->getNewUserData(); + $userData['usr_id'] = $userId; + /** + * Don't hash the password + */ + $userData['usr_password'] = $this->getStrongPassword(); + unset( $userData['usr_updated_date'], ); @@ -322,11 +412,9 @@ public function testServiceSuccessEmptyDates(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $transport->newUser($userData); + $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; - - $domainData['id'] = $userId; + $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -354,23 +442,23 @@ public function testServiceSuccessEmptyDates(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = $domainData['namePrefix']; + $expected = htmlspecialchars($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = $domainData['nameFirst']; + $expected = htmlspecialchars($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = $domainData['nameMiddle']; + $expected = htmlspecialchars($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = $domainData['nameLast']; + $expected = htmlspecialchars($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = $domainData['nameSuffix']; + $expected = htmlspecialchars($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -386,7 +474,7 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['tokenId']; $this->assertSame($expected, $actual); - $expected = $domainData['preferences']; + $expected = ''; $actual = $data['preferences']; $this->assertSame($expected, $actual); @@ -398,9 +486,9 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['createdUserId']; $this->assertSame($expected, $actual); - $expected = $dbUser['usr_updated_date']; + $today = date('Y-m-d '); $actual = $data['updatedDate']; - $this->assertNotSame($expected, $actual); + $this->assertStringContainsString($today, $actual); $expected = $dbUser['usr_updated_usr_id']; $actual = $data['updatedUserId']; From d54d8d6aedc4df1baf19f7ec4979185ae7c7d231 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:44:42 -0600 Subject: [PATCH 162/301] [#.x] - removed old (kinda) dto --- .../DataSource/User/UserTransport.php | 135 ------------------ 1 file changed, 135 deletions(-) delete mode 100644 src/Domain/Components/DataSource/User/UserTransport.php diff --git a/src/Domain/Components/DataSource/User/UserTransport.php b/src/Domain/Components/DataSource/User/UserTransport.php deleted file mode 100644 index 98490cf..0000000 --- a/src/Domain/Components/DataSource/User/UserTransport.php +++ /dev/null @@ -1,135 +0,0 @@ - - * - * 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\Components\DataSource\User; - -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; - -/** - * The domain representation of a user. - * - * @method int getId() - * @method int getTenantId() - * @method string getStatus() - * @method string getEmail() - * @method string getPassword() - * @method string getNamePrefix() - * @method string getNameFirst() - * @method string getNameMiddle() - * @method string getNameLast() - * @method string getNameSuffix() - * @method string getIssuer() - * @method string getTokenPassword() - * @method string getTokenId() - * @method string getPreferences() - * @method string getCreatedDate() - * @method int|null getCreatedUserId() - * @method string getUpdatedDate() - * @method int|null getUpdatedUserId() - * - * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserTransport from UserTypes - */ -final class UserTransport -{ - /** @var TUserTransport */ - private array $store; - - /** - * @param TUserRecord $input - */ - public function __construct(array $input) - { - $this->store = [ - 'id' => (int)($input['usr_id'] ?? 0), - 'status' => (int)($input['usr_status_flag'] ?? 0), - 'email' => (string)($input['usr_email'] ?? ''), - 'password' => (string)($input['usr_password'] ?? ''), - 'namePrefix' => (string)($input['usr_name_prefix'] ?? ''), - 'nameFirst' => (string)($input['usr_name_first'] ?? ''), - 'nameMiddle' => (string)($input['usr_name_middle'] ?? ''), - 'nameLast' => (string)($input['usr_name_last'] ?? ''), - 'nameSuffix' => (string)($input['usr_name_suffix'] ?? ''), - 'issuer' => (string)($input['usr_issuer'] ?? ''), - 'tokenPassword' => (string)($input['usr_token_password'] ?? ''), - 'tokenId' => (string)($input['usr_token_id'] ?? ''), - 'preferences' => (string)($input['usr_preferences'] ?? ''), - 'createdDate' => (string)($input['usr_created_date'] ?? ''), - 'createdUserId' => (int)($input['usr_created_usr_id'] ?? 0), - 'updatedDate' => (string)($input['usr_updated_date'] ?? ''), - 'updatedUserId' => (int)($input['usr_updated_usr_id'] ?? 0), - ]; - - $this->store['fullName'] = $this->getFullName(); - } - - /** - * @param string $name - * @param array $arguments - * - * @return int|string - */ - public function __call(string $name, array $arguments): int | string - { - return match ($name) { - 'getId' => $this->store['id'], - 'getStatus' => $this->store['status'], - 'getEmail' => $this->store['email'], - 'getPassword' => $this->store['password'], - 'getNamePrefix' => $this->store['namePrefix'], - 'getNameFirst' => $this->store['nameFirst'], - 'getNameMiddle' => $this->store['nameMiddle'], - 'getNameLast' => $this->store['nameLast'], - 'getNameSuffix' => $this->store['nameSuffix'], - 'getIssuer' => $this->store['issuer'], - 'getTokenPassword' => $this->store['tokenPassword'], - 'getTokenId' => $this->store['tokenId'], - 'getPreferences' => $this->store['preferences'], - 'getCreatedDate' => $this->store['createdDate'], - 'getCreatedUserId' => $this->store['createdUserId'], - 'getUpdatedDate' => $this->store['updatedDate'], - 'getUpdatedUserId' => $this->store['updatedUserId'], - default => throw InvalidConfigurationArgumentException::new( - 'The ' . $name . ' method is not supported. [' - . json_encode($arguments) . ']', - ), - }; - } - - /** - * @return string - */ - public function getFullName(): string - { - return trim( - $this->getNameLast() - . ', ' - . $this->getNameFirst() - . ' ' - . $this->getNameMiddle() - ); - } - - public function isEmpty(): bool - { - return 0 === $this->store['id']; - } - - /** - * @return array - */ - public function toArray(): array - { - return [$this->store['id'] => $this->store]; - } -} From 19efaf1c72d240bd0a56d1fdaa32ba0ee55dbcf3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:45:07 -0600 Subject: [PATCH 163/301] [#.x] - corrected references and new objects --- src/Domain/Components/Cache/Cache.php | 24 +++++------ src/Domain/Components/Container.php | 60 +++++++++++++++++++++------ 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/Domain/Components/Cache/Cache.php b/src/Domain/Components/Cache/Cache.php index 1d1dcbd..e1f38ef 100644 --- a/src/Domain/Components/Cache/Cache.php +++ b/src/Domain/Components/Cache/Cache.php @@ -15,7 +15,7 @@ use DateTimeImmutable; use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\DataSource\User\UserTransport; +use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Cache\Adapter\Redis; use Phalcon\Cache\Cache as PhalconCache; @@ -48,12 +48,12 @@ class Cache extends PhalconCache private const MASK_TOKEN_USER = 'tk-%s-%s'; /** - * @param UserTransport $domainUser - * @param string $token + * @param User $domainUser + * @param string $token * * @return string */ - public function getCacheTokenKey(UserTransport $domainUser, string $token): string + public function getCacheTokenKey(User $domainUser, string $token): string { $tokenString = ''; if (true !== empty($token)) { @@ -62,21 +62,21 @@ public function getCacheTokenKey(UserTransport $domainUser, string $token): stri return sprintf( self::MASK_TOKEN_USER, - $domainUser->getId(), + $domainUser->id, $tokenString ); } /** - * @param EnvManager $env - * @param UserTransport $domainUser + * @param EnvManager $env + * @param User $domainUser * * @return bool * @throws InvalidArgumentException */ public function invalidateForUser( EnvManager $env, - UserTransport $domainUser + User $domainUser ): bool { /** * We could store the tokens in the database but this way is faster @@ -104,16 +104,16 @@ public function invalidateForUser( } /** - * @param EnvManager $env - * @param UserTransport $domainUser - * @param string $token + * @param EnvManager $env + * @param User $domainUser + * @param string $token * * @return bool * @throws InvalidArgumentException */ public function storeTokenInCache( EnvManager $env, - UserTransport $domainUser, + User $domainUser, string $token ): bool { $cacheKey = $this->getCacheTokenKey($domainUser, $token); diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index a8e97c5..861fee6 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -14,8 +14,11 @@ namespace Phalcon\Api\Domain\Components; use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthSanitizer; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; +use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; +use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Env\EnvManager; @@ -46,6 +49,7 @@ use Phalcon\Logger\Logger; use Phalcon\Mvc\Router; use Phalcon\Storage\SerializerFactory; +use Phalcon\Support\Registry; class Container extends Di { @@ -66,6 +70,8 @@ class Container extends Di /** @var string */ public const LOGGER = 'logger'; /** @var string */ + public const REGISTRY = 'registry'; + /** @var string */ public const REQUEST = 'request'; /** @var string */ public const RESPONSE = 'response'; @@ -96,10 +102,13 @@ class Container extends Di public const MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE = ValidateTokenStructureMiddleware::class; public const MIDDLEWARE_VALIDATE_TOKEN_USER = ValidateTokenUserMiddleware::class; /** - * Repositories + * Repositories/Sanitizers/Validators */ - public const REPOSITORY = 'repository'; - public const REPOSITORY_TRANSPORT = TransportRepository::class; + public const REPOSITORY = 'repository'; + public const AUTH_SANITIZER = 'auth.sanitizer'; + public const USER_MAPPER = UserMapper::class; + public const USER_VALIDATOR = UserValidator::class; + public const USER_SANITIZER = 'user.sanitizer'; /** * Responders */ @@ -115,11 +124,14 @@ public function __construct() self::FILTER => $this->getServiceFilter(), self::JWT_TOKEN => $this->getServiceJWTToken(), self::LOGGER => $this->getServiceLogger(), + self::REGISTRY => new Service(Registry::class, true), self::REQUEST => new Service(Request::class, true), self::RESPONSE => new Service(Response::class, true), self::ROUTER => $this->getServiceRouter(), - self::REPOSITORY => $this->getServiceRepository(), + self::REPOSITORY => $this->getServiceRepository(), + self::AUTH_SANITIZER => $this->getServiceSanitizer(AuthSanitizer::class), + self::USER_SANITIZER => $this->getServiceSanitizer(UserSanitizer::class), self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthPost(LoginPostService::class), self::AUTH_LOGOUT_POST_SERVICE => $this->getServiceAuthPost(LogoutPostService::class), @@ -148,10 +160,6 @@ private function getServiceAuthPost(string $className): Service 'type' => 'service', 'name' => self::REPOSITORY, ], - [ - 'type' => 'service', - 'name' => self::REPOSITORY_TRANSPORT, - ], [ 'type' => 'service', 'name' => self::CACHE, @@ -166,7 +174,7 @@ private function getServiceAuthPost(string $className): Service ], [ 'type' => 'service', - 'name' => self::FILTER, + 'name' => self::AUTH_SANITIZER, ], [ 'type' => 'service', @@ -362,6 +370,10 @@ private function getServiceRepository(): Service 'type' => 'service', 'name' => self::CONNECTION, ], + [ + 'type' => 'service', + 'name' => self::USER_MAPPER, + ], ], ] ); @@ -402,11 +414,15 @@ private function getServiceUser(string $className): Service ], [ 'type' => 'service', - 'name' => self::REPOSITORY_TRANSPORT, + 'name' => self::USER_MAPPER, ], [ 'type' => 'service', - 'name' => self::FILTER, + 'name' => self::USER_VALIDATOR, + ], + [ + 'type' => 'service', + 'name' => self::USER_SANITIZER, ], [ 'type' => 'service', @@ -416,4 +432,24 @@ private function getServiceUser(string $className): Service ] ); } + + /** + * @param class-string $className + * + * @return Service + */ + private function getServiceSanitizer(string $className): Service + { + return new Service( + [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::FILTER, + ], + ], + ] + ); + } } From 791169d53b94267973713398d43abfa3f5603672 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:45:41 -0600 Subject: [PATCH 164/301] [#.x] - adjusting signature --- .../Services/Auth/AbstractAuthService.php | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Services/Auth/AbstractAuthService.php index 3094540..446b8c8 100644 --- a/src/Domain/Services/Auth/AbstractAuthService.php +++ b/src/Domain/Services/Auth/AbstractAuthService.php @@ -13,18 +13,15 @@ namespace Phalcon\Api\Domain\Services\Auth; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\DomainInterface; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; +use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Domain\Payload; -use Phalcon\Filter\Filter; /** * @phpstan-import-type TUserDbRecord from UserTypes @@ -35,27 +32,11 @@ abstract class AbstractAuthService implements DomainInterface { public function __construct( protected readonly QueryRepository $repository, - protected readonly TransportRepository $transport, protected readonly Cache $cache, protected readonly EnvManager $env, protected readonly JWTToken $jwtToken, - protected readonly Filter $filter, + protected readonly SanitizerInterface $sanitizer, protected readonly Security $security, ) { } - - /** - * @param TValidationErrors $errors - * - * @return Payload - */ - protected function getUnauthorizedPayload(array $errors): Payload - { - return new Payload( - DomainStatus::UNAUTHORIZED, - [ - 'errors' => $errors, - ] - ); - } } From 0499ee716b0a530c294ea381157e3e74d193dfc3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:45:57 -0600 Subject: [PATCH 165/301] [#.x] - adjusting objects --- src/Domain/ADR/DomainInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/ADR/DomainInterface.php b/src/Domain/ADR/DomainInterface.php index 52be69d..2c0a249 100644 --- a/src/Domain/ADR/DomainInterface.php +++ b/src/Domain/ADR/DomainInterface.php @@ -13,7 +13,7 @@ namespace Phalcon\Api\Domain\ADR; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TLoginInput from InputTypes From a19d744449c31b741569a31f91df1d3f139725c5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 11:46:17 -0600 Subject: [PATCH 166/301] [#.x] - switched to interfaces --- src/Domain/Components/DataSource/QueryRepository.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Domain/Components/DataSource/QueryRepository.php b/src/Domain/Components/DataSource/QueryRepository.php index 5e5f58b..120eec3 100644 --- a/src/Domain/Components/DataSource/QueryRepository.php +++ b/src/Domain/Components/DataSource/QueryRepository.php @@ -13,28 +13,31 @@ namespace Phalcon\Api\Domain\Components\DataSource; +use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; use Phalcon\DataMapper\Pdo\Connection; class QueryRepository { - private ?UserRepository $user = null; + private ?UserRepositoryInterface $user = null; /** * @param Connection $connection */ public function __construct( private readonly Connection $connection, + private readonly UserMapper $userMapper, ) { } /** * @return UserRepository */ - public function user(): UserRepository + public function user(): UserRepositoryInterface { if (null === $this->user) { - $this->user = new UserRepository($this->connection); + $this->user = new UserRepository($this->connection, $this->userMapper); } return $this->user; From a9a6f119d1aa7332ca50fdbba680f5cfaa9b5b03 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 14:20:05 -0600 Subject: [PATCH 167/301] [#.x] - changed toArray to return just the object as an array --- .../Components/DataSource/User/User.php | 12 +++++- .../Components/DataSource/User/UserInput.php | 41 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/Domain/Components/DataSource/User/User.php b/src/Domain/Components/DataSource/User/User.php index f6a599a..dd58248 100644 --- a/src/Domain/Components/DataSource/User/User.php +++ b/src/Domain/Components/DataSource/User/User.php @@ -13,8 +13,12 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; +use function get_object_vars; use function trim; +/** + * @phpstan-import-type TUser from UserTypes + */ final class User { public function __construct( @@ -45,8 +49,14 @@ public function fullName(): string ); } + /** + * @return TUser + */ public function toArray(): array { - return [$this->id => get_object_vars($this)]; + /** @var TUser $vars */ + $vars = get_object_vars($this); + + return $vars; } } diff --git a/src/Domain/Components/DataSource/User/UserInput.php b/src/Domain/Components/DataSource/User/UserInput.php index 15f6545..e983dc7 100644 --- a/src/Domain/Components/DataSource/User/UserInput.php +++ b/src/Domain/Components/DataSource/User/UserInput.php @@ -13,10 +13,36 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; +use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use function get_object_vars; + +/** + * @phpstan-import-type TUserInput from InputTypes + * @phpstan-import-type TUser from UserTypes + */ final class UserInput { + /** + * @param int|null $id + * @param int|null $status + * @param string|null $email + * @param string|null $password + * @param string|null $namePrefix + * @param string|null $nameFirst + * @param string|null $nameMiddle + * @param string|null $nameLast + * @param string|null $nameSuffix + * @param string|null $issuer + * @param string|null $tokenPassword + * @param string|null $tokenId + * @param string|null $preferences + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + */ public function __construct( public readonly ?int $id, public readonly ?int $status, @@ -38,8 +64,15 @@ public function __construct( ) { } + /** + * @param SanitizerInterface $sanitizer + * @param TUserInput $input + * + * @return self + */ public static function new(SanitizerInterface $sanitizer, array $input): self { + /** @var TUser $sanitized */ $sanitized = $sanitizer->sanitize($input); return new self( @@ -63,8 +96,14 @@ public static function new(SanitizerInterface $sanitizer, array $input): self ); } + /** + * @return TUser + */ public function toArray(): array { - return [$this->id => get_object_vars($this)]; + /** @var TUser $vars */ + $vars = get_object_vars($this); + + return $vars; } } From 8c614019368c9acc0e1123883af0f6ca54581a0e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 14:20:38 -0600 Subject: [PATCH 168/301] [#.x] - adjustments for payload and processPassword --- .../Services/User/AbstractUserService.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Domain/Services/User/AbstractUserService.php b/src/Domain/Services/User/AbstractUserService.php index 684a323..a907c59 100644 --- a/src/Domain/Services/User/AbstractUserService.php +++ b/src/Domain/Services/User/AbstractUserService.php @@ -19,6 +19,7 @@ use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; @@ -26,10 +27,20 @@ use function array_shift; +/** + * @phpstan-import-type TUser from UserTypes + */ abstract class AbstractUserService implements DomainInterface { protected HttpCodesEnum $errorMessage; + /** + * @param QueryRepository $repository + * @param UserMapper $mapper + * @param UserValidator $validator + * @param SanitizerInterface $sanitizer + * @param Security $security + */ public function __construct( protected readonly QueryRepository $repository, protected readonly UserMapper $mapper, @@ -49,7 +60,7 @@ protected function getErrorPayload(string $message): Payload { return Payload::error( [ - $this->errorMessage->text() . $message, + [$this->errorMessage->text() . $message], ] ); } @@ -57,12 +68,12 @@ protected function getErrorPayload(string $message): Payload /** * @param UserInput $inputObject * - * @return array + * @return User */ protected function processPassword(UserInput $inputObject): User { + /** @var TUser $inputData */ $inputData = $inputObject->toArray(); - $inputData = array_shift($inputData); if (null !== $inputData['password']) { $plain = $inputData['password']; From 9cf6bdcc7ba3f867404ad968dbcf1262a4f1bdc0 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 14:21:38 -0600 Subject: [PATCH 169/301] [#.x] - refactored payload:notFound, adjusted return payload to include the ID --- src/Domain/Services/User/UserDeleteService.php | 2 +- src/Domain/Services/User/UserGetService.php | 4 ++-- src/Domain/Services/User/UserPostService.php | 6 ++++-- src/Domain/Services/User/UserPutService.php | 9 ++++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Domain/Services/User/UserDeleteService.php b/src/Domain/Services/User/UserDeleteService.php index 72b55f1..b380eff 100644 --- a/src/Domain/Services/User/UserDeleteService.php +++ b/src/Domain/Services/User/UserDeleteService.php @@ -50,6 +50,6 @@ public function __invoke(array $input): Payload /** * 404 */ - return Payload::notFound(['Record(s) not found']); + return Payload::notFound(); } } diff --git a/src/Domain/Services/User/UserGetService.php b/src/Domain/Services/User/UserGetService.php index b487174..9950ec6 100644 --- a/src/Domain/Services/User/UserGetService.php +++ b/src/Domain/Services/User/UserGetService.php @@ -39,13 +39,13 @@ public function __invoke(array $input): Payload $user = $this->repository->user()->findById($userId); if (null !== $user) { - return Payload::success($user->toArray()); + return Payload::success([$user->id => $user->toArray()]); } } /** * 404 */ - return Payload::notFound(['Record(s) not found']); + return Payload::notFound(); } } diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Services/User/UserPostService.php index da19d0d..ba39ebb 100644 --- a/src/Domain/Services/User/UserPostService.php +++ b/src/Domain/Services/User/UserPostService.php @@ -15,12 +15,12 @@ use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** - * @phpstan-import-type TUserSanitizedInsertInput from InputTypes * @phpstan-import-type TUserInput from InputTypes * @phpstan-import-type TValidationErrors from InputTypes */ @@ -36,6 +36,7 @@ final class UserPostService extends AbstractUserService public function __invoke(array $input): Payload { $inputObject = UserInput::new($this->sanitizer, $input); + /** @var TValidationErrors $errors */ $errors = $this->validator->validate($inputObject); /** @@ -73,11 +74,12 @@ public function __invoke(array $input): Payload /** * Get the user from the database */ + /** @var User $domainUser */ $domainUser = $this->repository->user()->findById($userId); /** * Return the user back */ - return Payload::created($domainUser->toArray()); + return Payload::created([$domainUser->id => $domainUser->toArray()]); } } diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php index 44c7366..82871f8 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Services/User/UserPutService.php @@ -15,12 +15,12 @@ use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** - * @phpstan-import-type TUserSanitizedUpdateInput from InputTypes * @phpstan-import-type TUserInput from InputTypes * @phpstan-import-type TValidationErrors from InputTypes */ @@ -36,6 +36,7 @@ final class UserPutService extends AbstractUserService public function __invoke(array $input): Payload { $inputObject = UserInput::new($this->sanitizer, $input); + /** @var TValidationErrors $errors */ $errors = $this->validator->validate($inputObject); /** @@ -48,11 +49,12 @@ public function __invoke(array $input): Payload /** * Check if the user exists, If not, return an error */ + /** @var int $userId */ $userId = $inputObject->id; $domainUser = $this->repository->user()->findById($userId); if (null === $domainUser) { - return Payload::notFound(['Record(s) not found']); + return Payload::notFound(); } /** @@ -83,11 +85,12 @@ public function __invoke(array $input): Payload /** * Get the user from the database */ + /** @var User $domainUser */ $domainUser = $this->repository->user()->findById($userId); /** * Return the user back */ - return Payload::updated($domainUser->toArray()); + return Payload::updated([$domainUser->id => $domainUser->toArray()]); } } From 4442e70b80bcee728675464be91443249210b830 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 14:21:57 -0600 Subject: [PATCH 170/301] [#.x] - phpstan corrections --- src/Action/ActionHandler.php | 4 +- src/Domain/ADR/DomainInterface.php | 4 +- src/Domain/ADR/InputTypes.php | 45 +------ .../DataSource/AbstractRepository.php | 43 +------ .../Components/DataSource/Auth/AuthInput.php | 28 ++++- .../DataSource/Auth/AuthSanitizer.php | 10 +- .../Components/DataSource/QueryRepository.php | 2 +- .../DataSource/SanitizerInterface.php | 7 +- .../Components/DataSource/User/UserMapper.php | 17 ++- .../DataSource/User/UserRepository.php | 28 +++-- .../User/UserRepositoryInterface.php | 8 +- .../DataSource/User/UserSanitizer.php | 12 +- .../Components/DataSource/User/UserTypes.php | 117 +++++++++--------- .../DataSource/User/UserValidator.php | 7 +- src/Domain/Components/Encryption/JWTToken.php | 21 ++-- .../ValidateTokenRevokedMiddleware.php | 2 +- .../ValidateTokenStructureMiddleware.php | 1 - .../ValidateTokenUserMiddleware.php | 3 - src/Domain/Components/Payload.php | 55 +++++++- .../Services/Auth/AbstractAuthService.php | 2 - src/Domain/Services/Auth/LoginPostService.php | 6 +- .../Services/Auth/LogoutPostService.php | 6 +- .../Services/Auth/RefreshPostService.php | 6 +- .../DataSource/User/UserInputTest.php | 3 +- .../DataSource/User/UserSanitizerTest.php | 4 +- .../Components/DataSource/User/UserTest.php | 36 +++--- .../Services/User/UserServiceDeleteTest.php | 8 +- .../Services/User/UserServiceDispatchTest.php | 3 +- .../Services/User/UserServiceGetTest.php | 16 ++- .../Services/User/UserServicePostTest.php | 9 +- .../Services/User/UserServicePutTest.php | 30 +++-- 31 files changed, 280 insertions(+), 263 deletions(-) diff --git a/src/Action/ActionHandler.php b/src/Action/ActionHandler.php index d169ae1..d9a8533 100644 --- a/src/Action/ActionHandler.php +++ b/src/Action/ActionHandler.php @@ -21,7 +21,7 @@ use Phalcon\Http\ResponseInterface; /** - * @phpstan-import-type TLoginInput from InputTypes + * @phpstan-import-type TAuthLoginInput from InputTypes * @phpstan-import-type TUserInput from InputTypes */ final readonly class ActionHandler implements ActionInterface @@ -37,7 +37,7 @@ public function __construct( public function __invoke(): void { $input = new Input(); - /** @var TLoginInput|TUserInput $data */ + /** @var TAuthLoginInput|TUserInput $data */ $data = $input->__invoke($this->request); $this->responder->__invoke( diff --git a/src/Domain/ADR/DomainInterface.php b/src/Domain/ADR/DomainInterface.php index 2c0a249..346a756 100644 --- a/src/Domain/ADR/DomainInterface.php +++ b/src/Domain/ADR/DomainInterface.php @@ -16,13 +16,13 @@ use Phalcon\Api\Domain\Components\Payload; /** - * @phpstan-import-type TLoginInput from InputTypes + * @phpstan-import-type TAuthLoginInput from InputTypes * @phpstan-import-type TUserInput from InputTypes */ interface DomainInterface { /** - * @param TLoginInput|TUserInput $input + * @param TAuthLoginInput|TUserInput $input * * @return Payload */ diff --git a/src/Domain/ADR/InputTypes.php b/src/Domain/ADR/InputTypes.php index b68da4d..14021fd 100644 --- a/src/Domain/ADR/InputTypes.php +++ b/src/Domain/ADR/InputTypes.php @@ -14,16 +14,18 @@ namespace Phalcon\Api\Domain\ADR; /** - * @phpstan-type TLoginInput array{ + * @phpstan-type TAuthInput TAuthLoginInput|TAuthLogoutInput + * + * @phpstan-type TAuthLoginInput array{ * email?: string, * password?: string * } * - * @phpstan-type TLogoutInput array{ + * @phpstan-type TAuthLogoutInput array{ * token?: string * } * - * @phpstan-type TRefreshInput array{ + * @phpstan-type TAuthRefreshInput array{ * token?: string * } * @@ -47,43 +49,6 @@ * updatedUserId?: int, * } * - * @phpstan-type TUserSanitizedInsertInput array{ - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * createdDate: string, - * createdUserId: int, - * updatedDate: string, - * updatedUserId: int, - * } - * - * @phpstan-type TUserSanitizedUpdateInput array{ - * id: int, - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * updatedDate: string, - * updatedUserId: int, - * } - * * @phpstan-type TRequestQuery array * * @phpstan-type TValidationErrors array> diff --git a/src/Domain/Components/DataSource/AbstractRepository.php b/src/Domain/Components/DataSource/AbstractRepository.php index c60fe7e..a5125f4 100644 --- a/src/Domain/Components/DataSource/AbstractRepository.php +++ b/src/Domain/Components/DataSource/AbstractRepository.php @@ -18,7 +18,7 @@ use Phalcon\DataMapper\Query\Delete; /** - * @phpstan-import-type TUserRecord from UserTypes + * @phpstan-import-type TCriteria from UserTypes */ abstract class AbstractRepository { @@ -37,7 +37,7 @@ public function __construct( } /** - * @param array $criteria + * @param TCriteria $criteria * * @return int */ @@ -67,43 +67,4 @@ public function deleteById(int $recordId): int ] ); } - -// /** -// * @param int $recordId -// * -// * @return TUserRecord -// */ -// public function findById(int $recordId): array -// { -// $result = []; -// if ($recordId > 0) { -// return $this->findOneBy( -// [ -// $this->idField => $recordId, -// ] -// ); -// } -// -// return $result; -// } -// -// -// /** -// * @param array $criteria -// * -// * @return TUserRecord -// */ -// public function findOneBy(array $criteria): array -// { -// $select = Select::new($this->connection); -// -// /** @var TUserRecord $result */ -// $result = $select -// ->from($this->table) -// ->whereEquals($criteria) -// ->fetchOne() -// ; -// -// return $result; -// } } diff --git a/src/Domain/Components/DataSource/Auth/AuthInput.php b/src/Domain/Components/DataSource/Auth/AuthInput.php index b4250fb..cda5ced 100644 --- a/src/Domain/Components/DataSource/Auth/AuthInput.php +++ b/src/Domain/Components/DataSource/Auth/AuthInput.php @@ -13,10 +13,19 @@ namespace Phalcon\Api\Domain\Components\DataSource\Auth; +use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +/** + * @phpstan-import-type TAuthInput from InputTypes + */ final class AuthInput { + /** + * @param string|null $email + * @param string|null $password + * @param string|null $token + */ public function __construct( public readonly ?string $email, public readonly ?string $password, @@ -24,14 +33,23 @@ public function __construct( ) { } + /** + * @param SanitizerInterface $sanitizer + * @param TAuthInput $input + * + * @return self + */ public static function new(SanitizerInterface $sanitizer, array $input): self { $sanitized = $sanitizer->sanitize($input); - return new self( - $sanitized['email'] ?? null, - $sanitized['password'] ?? null, - $sanitized['token'] ?? null, - ); + /** @var string|null $email */ + $email = $sanitized['email'] ?? null; + /** @var string|null $password */ + $password = $sanitized['password'] ?? null; + /** @var string|null $token */ + $token = $sanitized['token'] ?? null; + + return new self($email, $password, $token); } } diff --git a/src/Domain/Components/DataSource/Auth/AuthSanitizer.php b/src/Domain/Components/DataSource/Auth/AuthSanitizer.php index 6cbc431..fdf86fd 100644 --- a/src/Domain/Components/DataSource/Auth/AuthSanitizer.php +++ b/src/Domain/Components/DataSource/Auth/AuthSanitizer.php @@ -13,9 +13,13 @@ namespace Phalcon\Api\Domain\Components\DataSource\Auth; +use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; use Phalcon\Filter\Filter; +/** + * @phpstan-import-type TAuthInput from InputTypes + */ final class AuthSanitizer implements SanitizerInterface { public function __construct( @@ -26,12 +30,13 @@ public function __construct( /** * Return a sanitized array of the input * - * @param array $input + * @param TAuthInput $input * - * @return array + * @return TAuthInput */ public function sanitize(array $input): array { + /** @var array $fields */ $fields = [ 'email' => null, 'password' => null, @@ -56,6 +61,7 @@ public function sanitize(array $input): array $sanitized[$name] = $value; } + /** @var TAuthInput $sanitized */ return $sanitized; } diff --git a/src/Domain/Components/DataSource/QueryRepository.php b/src/Domain/Components/DataSource/QueryRepository.php index 120eec3..08dce4d 100644 --- a/src/Domain/Components/DataSource/QueryRepository.php +++ b/src/Domain/Components/DataSource/QueryRepository.php @@ -32,7 +32,7 @@ public function __construct( } /** - * @return UserRepository + * @return UserRepositoryInterface */ public function user(): UserRepositoryInterface { diff --git a/src/Domain/Components/DataSource/SanitizerInterface.php b/src/Domain/Components/DataSource/SanitizerInterface.php index b4fd17b..5620f01 100644 --- a/src/Domain/Components/DataSource/SanitizerInterface.php +++ b/src/Domain/Components/DataSource/SanitizerInterface.php @@ -13,14 +13,17 @@ namespace Phalcon\Api\Domain\Components\DataSource; +/** + * @phpstan-type TInput array + */ interface SanitizerInterface { /** * Return a sanitized array of the input * - * @param array $input + * @param TInput $input * - * @return array + * @return TInput */ public function sanitize(array $input): array; } diff --git a/src/Domain/Components/DataSource/User/UserMapper.php b/src/Domain/Components/DataSource/User/UserMapper.php index 2555b2d..31eebc8 100644 --- a/src/Domain/Components/DataSource/User/UserMapper.php +++ b/src/Domain/Components/DataSource/User/UserMapper.php @@ -13,12 +13,19 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TUser from UserTypes + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + */ final class UserMapper { /** * Map Domain User -> DB row (usr_*) * - * @return array + * @return TUserDomainToDbRecord */ public function db(User $user): array { @@ -46,7 +53,7 @@ public function db(User $user): array /** * Map DB row (usr_*) -> Domain User * - * @param array $row + * @param TUserDbRecord|array{} $row */ public function domain(array $row): User { @@ -74,15 +81,15 @@ public function domain(array $row): User /** * Map input row -> Domain User * - * @param array $row + * @param TUser $row */ public function input(array $row): User { return new User( $row['id'], $row['status'], - $row['email'], - $row['password'], + (string)$row['email'], + (string)$row['password'], $row['namePrefix'], $row['nameFirst'], $row['nameMiddle'], diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index 2b9d846..e075fce 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -24,6 +24,9 @@ use function array_filter; /** + * @phpstan-import-type TCriteria from UserTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes * @phpstan-import-type TUserRecord from UserTypes * * The 'final' keyword was intentionally removed from this class to allow @@ -89,7 +92,7 @@ public function findById(int $recordId): ?User /** - * @param array $criteria + * @param TCriteria $criteria * * @return User|null */ @@ -132,11 +135,6 @@ public function insert(User $user): int $row['usr_updated_date'] = $now; } - /** - * Remove usr_id just in case - */ - unset($row['usr_id']); - /** * Cleanup empty fields if needed */ @@ -158,9 +156,12 @@ public function insert(User $user): int */ public function update(User $user): int { + $row = $this->mapper->db($user); $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + /** @var int $userId */ $userId = $row['usr_id']; + /** * @todo this should not be here - the update should just add data not validate */ @@ -172,15 +173,16 @@ public function update(User $user): int } /** - * Remove createdDate and createdUserId - cannot be changed. This - * needs to be here because we don't want to touch those fields. + * Cleanup empty fields if needed */ - unset($row['usr_created_date'], $row['usr_created_usr_id']); + $columns = $this->cleanupFields($row); /** - * Cleanup empty fields if needed + * Remove createdDate and createdUserId - cannot be changed. This + * needs to be here because we don't want to touch those fields. */ - $columns = $this->cleanupFields($row); + unset($columns['usr_created_date'], $columns['usr_created_usr_id']); + $update = Update::new($this->connection); $update ->table($this->table) @@ -193,9 +195,9 @@ public function update(User $user): int } /** - * @param array $row + * @param TUserDomainToDbRecord $row * - * @return array + * @return TUserDbRecordOptional */ private function cleanupFields(array $row): array { diff --git a/src/Domain/Components/DataSource/User/UserRepositoryInterface.php b/src/Domain/Components/DataSource/User/UserRepositoryInterface.php index 4f6702a..5313ae6 100644 --- a/src/Domain/Components/DataSource/User/UserRepositoryInterface.php +++ b/src/Domain/Components/DataSource/User/UserRepositoryInterface.php @@ -14,14 +14,12 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; /** - * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserInsert from UserTypes - * @phpstan-import-type TUserUpdate from UserTypes + * @phpstan-import-type TCriteria from UserTypes */ interface UserRepositoryInterface { /** - * @param array $criteria + * @param TCriteria $criteria * * @return int */ @@ -49,7 +47,7 @@ public function findByEmail(string $email): ?User; public function findById(int $recordId): ?User; /** - * @param array $criteria + * @param TCriteria $criteria * * @return User|null */ diff --git a/src/Domain/Components/DataSource/User/UserSanitizer.php b/src/Domain/Components/DataSource/User/UserSanitizer.php index 2a1f9c9..bfdf298 100644 --- a/src/Domain/Components/DataSource/User/UserSanitizer.php +++ b/src/Domain/Components/DataSource/User/UserSanitizer.php @@ -16,6 +16,9 @@ use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; use Phalcon\Filter\Filter; +/** + * @phpstan-import-type TUser from UserTypes + */ final class UserSanitizer implements SanitizerInterface { public function __construct( @@ -26,9 +29,9 @@ public function __construct( /** * Return a sanitized array of the input * - * @param array $input + * @param TUser $input * - * @return array + * @return TUser */ public function sanitize(array $input): array { @@ -47,9 +50,9 @@ public function sanitize(array $input): array 'tokenId' => null, 'preferences' => null, 'createdDate' => null, - 'createdUserId' => null, + 'createdUserId' => 0, 'updatedDate' => null, - 'updatedUserId' => null, + 'updatedUserId' => 0, ]; /** @@ -70,6 +73,7 @@ public function sanitize(array $input): array $sanitized[$name] = $value; } + /** @var TUser $sanitized */ return $sanitized; } diff --git a/src/Domain/Components/DataSource/User/UserTypes.php b/src/Domain/Components/DataSource/User/UserTypes.php index d42f2e4..3ccb744 100644 --- a/src/Domain/Components/DataSource/User/UserTypes.php +++ b/src/Domain/Components/DataSource/User/UserTypes.php @@ -47,71 +47,76 @@ * usr_updated_usr_id: int, * } * - * @phpstan-type TUserTokenDbRecord array{ + * @phpstan-type TUserDomainToDbRecord array{ * usr_id: int, - * usr_issuer: string, - * usr_token_password: string, - * usr_token_id: string + * usr_status_flag: int, + * usr_email: ?string, + * usr_password: ?string, + * usr_name_prefix: ?string, + * usr_name_first: ?string, + * usr_name_middle: ?string, + * usr_name_last: ?string, + * usr_name_suffix: ?string, + * usr_issuer: ?string, + * usr_token_password: ?string, + * usr_token_id: ?string, + * usr_preferences: ?string, + * usr_created_date: ?string, + * usr_created_usr_id: ?int, + * usr_updated_date: ?string, + * usr_updated_usr_id: ?int, * } * - * @phpstan-type TUserRecord array{}|TUserDbRecord + * @phpstan-type TUserDbRecordOptional array{ + * usr_id?: int, + * usr_status_flag?: int, + * usr_email?: ?string, + * usr_password?: ?string, + * usr_name_prefix?: ?string, + * usr_name_first?: ?string, + * usr_name_middle?: ?string, + * usr_name_last?: ?string, + * usr_name_suffix?: ?string, + * usr_issuer?: ?string, + * usr_token_password?: ?string, + * usr_token_id?: ?string, + * usr_preferences?: ?string, + * usr_created_date?: ?string, + * usr_created_usr_id?: ?int, + * usr_updated_date?: ?string, + * usr_updated_usr_id?: ?int, + * } * - * @phpstan-type TUserTransport array{ + * @phpstan-type TUser array{ * id: int, * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * createdDate: string, - * createdUserId: int, - * updatedDate: string, - * updatedUserId: int, + * email: ?string, + * password: ?string, + * namePrefix: ?string, + * nameFirst: ?string, + * nameMiddle: ?string, + * nameLast: ?string, + * nameSuffix: ?string, + * issuer: ?string, + * tokenPassword: ?string, + * tokenId: ?string, + * preferences: ?string, + * createdDate: ?string, + * createdUserId: ?int, + * updatedDate: ?string, + * updatedUserId: ?int, * } * - * @phpstan-type TUserInsert array{ - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * createdDate: string, - * createdUserId: int, - * updatedDate: string, - * updatedUserId: int, - * } + * @phpstan-type TUserTokenDbRecord array{ + * usr_id: int, + * usr_issuer: string, + * usr_token_password: string, + * usr_token_id: string + * } + * + * @phpstan-type TUserRecord array{}|TUserDbRecord * - * @phpstan-type TUserUpdate array{ - * id: int, - * status: int, - * email: string, - * password: string, - * namePrefix: string, - * nameFirst: string, - * nameMiddle: string, - * nameLast: string, - * nameSuffix: string, - * issuer: string, - * tokenPassword: string, - * tokenId: string, - * preferences: string, - * updatedDate: string, - * updatedUserId: int, - * } + * @phpstan-type TCriteria array */ final class UserTypes { diff --git a/src/Domain/Components/DataSource/User/UserValidator.php b/src/Domain/Components/DataSource/User/UserValidator.php index 47be3f0..15f2e6b 100644 --- a/src/Domain/Components/DataSource/User/UserValidator.php +++ b/src/Domain/Components/DataSource/User/UserValidator.php @@ -13,6 +13,11 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TValidationErrors from InputTypes + */ final class UserValidator { /** @@ -21,7 +26,7 @@ final class UserValidator * * @param UserInput $input * - * @return array + * @return TValidationErrors|array{} */ public function validate(UserInput $input): array { diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Components/Encryption/JWTToken.php index 47908f6..b1deaf2 100644 --- a/src/Domain/Components/Encryption/JWTToken.php +++ b/src/Domain/Components/Encryption/JWTToken.php @@ -31,8 +31,6 @@ use Phalcon\Support\Helper\Json\Decode; /** - * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserTokenDbRecord from UserTypes * @phpstan-type TValidatorErrors array{}|array * * Removed the final declaration so that this class can be mocked. This @@ -139,15 +137,24 @@ public function validate( $signer = new Hmac(); $now = new DateTimeImmutable(); + /** @var string $tokenId */ + $tokenId = $user->tokenId; + /** @var string $issuer */ + $issuer = $user->issuer; + /** @var string $tokenPassword */ + $tokenPassword = $user->tokenPassword; + /** @var int $userId */ + $userId = $user->id; + $validator - ->validateId($user->tokenId) + ->validateId($tokenId) ->validateAudience($this->getTokenAudience()) - ->validateIssuer($user->issuer) + ->validateIssuer($issuer) ->validateNotBefore($now->getTimestamp()) ->validateIssuedAt($now->getTimestamp()) ->validateExpiration($now->getTimestamp()) - ->validateSignature($signer, $user->tokenPassword) - ->validateClaim(JWTEnum::UserId->value, $user->id) + ->validateSignature($signer, $tokenPassword) + ->validateClaim(JWTEnum::UserId->value, $userId) ; /** @var TValidatorErrors $errors */ @@ -189,7 +196,7 @@ private function generateTokenForUser( $tokenPassword = $user->tokenPassword; /** @var string $tokenId */ $tokenId = $user->tokenId; - /** @var string $userId */ + /** @var int $userId */ $userId = $user->id; $tokenObject = $tokenBuilder diff --git a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php index ea9be43..399adb0 100644 --- a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php @@ -40,7 +40,7 @@ public function call(Micro $application): bool /** @var Registry $registry */ $registry = $application->getSharedService(Container::REGISTRY); - /** @var User $user */ + /** @var User $domainUser */ $domainUser = $registry->get('user'); /** diff --git a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php index eed5801..57b8b9b 100644 --- a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php @@ -14,7 +14,6 @@ namespace Phalcon\Api\Domain\Components\Middleware; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php index bc1b12f..ccb9bf4 100644 --- a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php @@ -15,7 +15,6 @@ use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; @@ -26,8 +25,6 @@ use Phalcon\Support\Registry; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TUserRecord from UserTypes */ final class ValidateTokenUserMiddleware extends AbstractMiddleware { diff --git a/src/Domain/Components/Payload.php b/src/Domain/Components/Payload.php index 203e8d3..860a218 100644 --- a/src/Domain/Components/Payload.php +++ b/src/Domain/Components/Payload.php @@ -14,10 +14,20 @@ namespace Phalcon\Api\Domain\Components; use PayloadInterop\DomainStatus; +use Phalcon\Api\Responder\ResponderTypes; use Phalcon\Domain\Payload as PhalconPayload; +/** + * @phpstan-import-type TData from ResponderTypes + * @phpstan-import-type TErrors from ResponderTypes + */ final class Payload extends PhalconPayload { + /** + * @param string $status + * @param TData $data + * @param TErrors $errors + */ private function __construct( string $status, array $data = [], @@ -40,41 +50,82 @@ private function __construct( parent::__construct($status, $result); } + /** + * @param TData $data + * + * @return self + */ public static function created(array $data): self { return new self(DomainStatus::CREATED, $data); } + /** + * @param TData $data + * + * @return self + */ public static function deleted(array $data): self { return new self(DomainStatus::DELETED, $data); } + /** + * @param TErrors $errors + * + * @return self + */ public static function error(array $errors): self { return new self(status: DomainStatus::ERROR, errors: $errors); } + /** + * @param TErrors $errors + * + * @return self + */ public static function invalid(array $errors): self { return new self(status: DomainStatus::INVALID, errors: $errors); } - public static function notFound(array $errors): self + /** + * @return self + */ + public static function notFound(): self { - return new self(status: DomainStatus::NOT_FOUND, errors: $errors); + return new self( + status: DomainStatus::NOT_FOUND, + errors: [['Record(s) not found']] + ); } + /** + * @param TData $data + * + * @return self + */ public static function success(array $data): self { return new self(DomainStatus::SUCCESS, $data); } + /** + * @param TErrors $errors + * + * @return self + */ public static function unauthorized(array $errors): self { return new self(status: DomainStatus::UNAUTHORIZED, errors: $errors); } + /** + * @param TData $data + * + * @return self + */ public static function updated(array $data): self { return new self(DomainStatus::UPDATED, $data); diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Services/Auth/AbstractAuthService.php index 446b8c8..d55c1ff 100644 --- a/src/Domain/Services/Auth/AbstractAuthService.php +++ b/src/Domain/Services/Auth/AbstractAuthService.php @@ -24,8 +24,6 @@ use Phalcon\Api\Domain\Components\Env\EnvManager; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TLoginInput from InputTypes * @phpstan-import-type TValidationErrors from InputTypes */ abstract class AbstractAuthService implements DomainInterface diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Services/Auth/LoginPostService.php index fe00570..50730c1 100644 --- a/src/Domain/Services/Auth/LoginPostService.php +++ b/src/Domain/Services/Auth/LoginPostService.php @@ -15,19 +15,17 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TLoginInput from InputTypes + * @phpstan-import-type TAuthLoginInput from InputTypes */ final class LoginPostService extends AbstractAuthService { /** - * @param TLoginInput $input + * @param TAuthLoginInput $input * * @return Payload */ diff --git a/src/Domain/Services/Auth/LogoutPostService.php b/src/Domain/Services/Auth/LogoutPostService.php index 0a50524..e217816 100644 --- a/src/Domain/Services/Auth/LogoutPostService.php +++ b/src/Domain/Services/Auth/LogoutPostService.php @@ -15,20 +15,18 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TLogoutInput from InputTypes + * @phpstan-import-type TAuthLogoutInput from InputTypes * @phpstan-import-type TValidationErrors from InputTypes */ final class LogoutPostService extends AbstractAuthService { /** - * @param TLogoutInput $input + * @param TAuthLogoutInput $input * * @return Payload */ diff --git a/src/Domain/Services/Auth/RefreshPostService.php b/src/Domain/Services/Auth/RefreshPostService.php index 63b978f..9fce9d0 100644 --- a/src/Domain/Services/Auth/RefreshPostService.php +++ b/src/Domain/Services/Auth/RefreshPostService.php @@ -15,20 +15,18 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TRefreshInput from InputTypes + * @phpstan-import-type TAuthRefreshInput from InputTypes * @phpstan-import-type TValidationErrors from InputTypes */ final class RefreshPostService extends AbstractAuthService { /** - * @param TRefreshInput $input + * @param TAuthRefreshInput $input * * @return Payload */ diff --git a/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php b/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php index 2424f60..0c99a1c 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php @@ -122,8 +122,7 @@ public function testToArray(): void $actual = $userInput->updatedUserId; $this->assertSame($expected, $actual); - // Verify toArray behaviour: key is the id value per implementation - $expected = [$userInput->id => get_object_vars($userInput)]; + $expected =get_object_vars($userInput); $actual = $userInput->toArray(); $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php b/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php index 8b6ef93..481651f 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php @@ -41,9 +41,9 @@ public function testEmpty(): void 'tokenId' => null, 'preferences' => null, 'createdDate' => null, - 'createdUserId' => null, + 'createdUserId' => 0, 'updatedDate' => null, - 'updatedUserId' => null, + 'updatedUserId' => 0, ]; $actual = $sanitizer->sanitize([]); $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Components/DataSource/User/UserTest.php b/tests/Unit/Domain/Components/DataSource/User/UserTest.php index 0ce4a01..9d35178 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserTest.php @@ -54,25 +54,23 @@ public function testObject(): void $this->assertSame($expected, $actual); $expected = [ - $userData['usr_id'] => [ - 'id' => $userData['usr_id'], - 'status' => $userData['usr_status_flag'], - 'email' => $userData['usr_email'], - 'password' => $userData['usr_password'], - 'namePrefix' => $userData['usr_name_prefix'], - 'nameFirst' => $userData['usr_name_first'], - 'nameMiddle' => $userData['usr_name_middle'], - 'nameLast' => $userData['usr_name_last'], - 'nameSuffix' => $userData['usr_name_suffix'], - 'issuer' => $userData['usr_issuer'], - 'tokenPassword' => $userData['usr_token_password'], - 'tokenId' => $userData['usr_token_id'], - 'preferences' => $userData['usr_preferences'], - 'createdDate' => $userData['usr_created_date'], - 'createdUserId' => $userData['usr_created_usr_id'], - 'updatedDate' => $userData['usr_updated_date'], - 'updatedUserId' => $userData['usr_updated_usr_id'], - ], + 'id' => $userData['usr_id'], + 'status' => $userData['usr_status_flag'], + 'email' => $userData['usr_email'], + 'password' => $userData['usr_password'], + 'namePrefix' => $userData['usr_name_prefix'], + 'nameFirst' => $userData['usr_name_first'], + 'nameMiddle' => $userData['usr_name_middle'], + 'nameLast' => $userData['usr_name_last'], + 'nameSuffix' => $userData['usr_name_suffix'], + 'issuer' => $userData['usr_issuer'], + 'tokenPassword' => $userData['usr_token_password'], + 'tokenId' => $userData['usr_token_id'], + 'preferences' => $userData['usr_preferences'], + 'createdDate' => $userData['usr_created_date'], + 'createdUserId' => $userData['usr_created_usr_id'], + 'updatedDate' => $userData['usr_updated_date'], + 'updatedUserId' => $userData['usr_updated_usr_id'], ]; $actual = $user->toArray(); $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php b/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php index 05807f5..eb3fa4d 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php @@ -76,9 +76,7 @@ public function testServiceWithUserId(): void $errors = $actual['errors']; - $expected = [ - 'Record(s) not found', - ]; + $expected = [['Record(s) not found']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -103,9 +101,7 @@ public function testServiceZeroUserId(): void $errors = $actual['errors']; - $expected = [ - 'Record(s) not found', - ]; + $expected = [['Record(s) not found']]; $actual = $errors; $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php index 0167686..d7fe1a6 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php @@ -15,7 +15,6 @@ use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\TransportRepository; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; @@ -77,7 +76,7 @@ public function testDispatchGet(): void $this->assertSame($expected, $actual); $user = $userMapper->domain($dbUser); - $expected = $user->toArray(); + $expected = [$userId => $user->toArray()]; $actual = $data; $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Services/User/UserServiceGetTest.php b/tests/Unit/Domain/Services/User/UserServiceGetTest.php index d84e820..d062474 100644 --- a/tests/Unit/Domain/Services/User/UserServiceGetTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceGetTest.php @@ -37,9 +37,11 @@ public function testServiceEmptyUserId(): void $actual = $payload->getResult(); $this->assertArrayHasKey('errors', $actual); - $expected = 'Record(s) not found'; - $actual = $actual['errors'][0]; - $this->assertStringContainsString($expected, $actual); + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); } public function testServiceWithUserId(): void @@ -155,8 +157,10 @@ public function testServiceWrongUserId(): void $actual = $payload->getResult(); $this->assertArrayHasKey('errors', $actual); - $expected = 'Record(s) not found'; - $actual = $actual['errors'][0]; - $this->assertStringContainsString($expected, $actual); + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); } } diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Services/User/UserServicePostTest.php index 653de11..a4082ca 100644 --- a/tests/Unit/Domain/Services/User/UserServicePostTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePostTest.php @@ -71,7 +71,6 @@ public function testServiceFailureNoIdReturned(): void */ $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -84,7 +83,7 @@ public function testServiceFailureNoIdReturned(): void $errors = $actual['errors']; - $expected = ['Cannot create database record: No id returned']; + $expected = [['Cannot create database record: No id returned']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -136,7 +135,6 @@ public function testServiceFailurePdoError(): void */ $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -149,7 +147,7 @@ public function testServiceFailurePdoError(): void $errors = $actual['errors']; - $expected = ['Cannot create database record: abcde']; + $expected = [['Cannot create database record: abcde']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -176,7 +174,6 @@ public function testServiceFailureValidation(): void */ $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; $payload = $service->__invoke($domainData); @@ -216,7 +213,6 @@ public function testServiceSuccess(): void */ $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -318,7 +314,6 @@ public function testServiceSuccessEmptyDates(): void */ $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; $payload = $service->__invoke($domainData); diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php index 1f6a945..976afd2 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -77,7 +77,6 @@ public function testServiceFailureRecordNotFound(): void $updateUser = $userMapper->domain($userData); $updateUser = $updateUser->toArray(); - $updateUser = array_shift($updateUser); $payload = $service->__invoke($updateUser); @@ -90,7 +89,7 @@ public function testServiceFailureRecordNotFound(): void $errors = $actual['errors']; - $expected = ['Record(s) not found']; + $expected = [['Record(s) not found']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -144,7 +143,6 @@ public function testServiceFailureNoIdReturned(): void $updateUser = $userMapper->domain($userData); $updateUser = $updateUser->toArray(); - $updateUser = array_shift($updateUser); $payload = $service->__invoke($updateUser); @@ -157,7 +155,7 @@ public function testServiceFailureNoIdReturned(): void $errors = $actual['errors']; - $expected = ['Cannot update database record: No id returned']; + $expected = [['Cannot update database record: No id returned']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -216,7 +214,6 @@ public function testServiceFailurePdoError(): void */ $updateUser = $userMapper->domain($userData); $updateUser = $updateUser->toArray(); - $updateUser = array_shift($updateUser); $payload = $service->__invoke($updateUser); @@ -229,7 +226,7 @@ public function testServiceFailurePdoError(): void $errors = $actual['errors']; - $expected = ['Cannot update database record: abcde']; + $expected = [['Cannot update database record: abcde']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -253,11 +250,10 @@ public function testServiceFailureValidation(): void /** * $userData is a db record. We need a domain object here */ - $domainUser = $userMapper->domain($userData); - $domainData = $domainUser->toArray(); - $domainData = $domainData[0]; + $updateUser = $userMapper->domain($userData); + $updateUser = $updateUser->toArray(); - $payload = $service->__invoke($domainData); + $payload = $service->__invoke($updateUser); $expected = DomainStatus::INVALID; $actual = $payload->getStatus(); @@ -304,7 +300,6 @@ public function testServiceSuccess(): void */ $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -368,10 +363,16 @@ public function testServiceSuccess(): void $actual = $data['preferences']; $this->assertSame($expected, $actual); + /** + * These have to be the same on an update + */ $expected = $dbUser['usr_created_date']; $actual = $data['createdDate']; $this->assertSame($expected, $actual); + /** + * These have to be the same on an update + */ $expected = 0; $actual = $data['createdUserId']; $this->assertSame($expected, $actual); @@ -414,7 +415,6 @@ public function testServiceSuccessEmptyDates(): void */ $domainUser = $userMapper->domain($userData); $domainData = $domainUser->toArray(); - $domainData = array_shift($domainData); $payload = $service->__invoke($domainData); @@ -478,10 +478,16 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['preferences']; $this->assertSame($expected, $actual); + /** + * These have to be the same on an update + */ $expected = $dbUser['usr_created_date']; $actual = $data['createdDate']; $this->assertSame($expected, $actual); + /** + * These have to be the same on an update + */ $expected = 0; $actual = $data['createdUserId']; $this->assertSame($expected, $actual); From cd2ddc9f45c8f1f445441f660ae3c1a2d552291c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 7 Nov 2025 14:31:07 -0600 Subject: [PATCH 171/301] [#.x] - phpcs --- src/Action/ActionHandler.php | 2 +- src/Domain/Components/Container.php | 43 +++++----- .../Components/DataSource/Auth/AuthInput.php | 4 +- .../Components/DataSource/User/UserMapper.php | 2 - .../DataSource/User/UserRepository.php | 7 +- src/Domain/Components/Encryption/JWTToken.php | 3 +- .../Components/Enums/Http/RoutesEnum.php | 6 +- .../ValidateTokenUserMiddleware.php | 1 - .../Services/Auth/AbstractAuthService.php | 1 - src/Domain/Services/Auth/LoginPostService.php | 1 - .../Services/User/AbstractUserService.php | 3 - src/Domain/Services/User/UserPostService.php | 2 +- src/Domain/Services/User/UserPutService.php | 2 +- .../DataSource/User/UserInputTest.php | 2 +- .../Components/Encryption/JWTTokenTest.php | 22 +++--- .../ValidateTokenClaimsMiddlewareTest.php | 10 +-- .../ValidateTokenRevokedMiddlewareTest.php | 12 +-- .../ValidateTokenStructureMiddlewareTest.php | 4 +- .../ValidateTokenUserMiddlewareTest.php | 4 +- .../Services/Auth/LoginPostServiceTest.php | 26 +++---- .../Services/User/UserServicePostTest.php | 1 - .../Services/User/UserServicePutTest.php | 78 +++++++++---------- 22 files changed, 111 insertions(+), 125 deletions(-) diff --git a/src/Action/ActionHandler.php b/src/Action/ActionHandler.php index d9a8533..ffeefe8 100644 --- a/src/Action/ActionHandler.php +++ b/src/Action/ActionHandler.php @@ -38,7 +38,7 @@ public function __invoke(): void { $input = new Input(); /** @var TAuthLoginInput|TUserInput $data */ - $data = $input->__invoke($this->request); + $data = $input->__invoke($this->request); $this->responder->__invoke( $this->response, diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index 861fee6..0df1641 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -107,8 +107,9 @@ class Container extends Di public const REPOSITORY = 'repository'; public const AUTH_SANITIZER = 'auth.sanitizer'; public const USER_MAPPER = UserMapper::class; - public const USER_VALIDATOR = UserValidator::class; public const USER_SANITIZER = 'user.sanitizer'; + public const USER_VALIDATOR = UserValidator::class; + /** * Responders */ @@ -402,7 +403,7 @@ private function getServiceRouter(): Service * * @return Service */ - private function getServiceUser(string $className): Service + private function getServiceSanitizer(string $className): Service { return new Service( [ @@ -410,23 +411,7 @@ private function getServiceUser(string $className): Service 'arguments' => [ [ 'type' => 'service', - 'name' => self::REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::USER_MAPPER, - ], - [ - 'type' => 'service', - 'name' => self::USER_VALIDATOR, - ], - [ - 'type' => 'service', - 'name' => self::USER_SANITIZER, - ], - [ - 'type' => 'service', - 'name' => self::SECURITY, + 'name' => self::FILTER, ], ], ] @@ -438,7 +423,7 @@ private function getServiceUser(string $className): Service * * @return Service */ - private function getServiceSanitizer(string $className): Service + private function getServiceUser(string $className): Service { return new Service( [ @@ -446,7 +431,23 @@ private function getServiceSanitizer(string $className): Service 'arguments' => [ [ 'type' => 'service', - 'name' => self::FILTER, + 'name' => self::REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => self::USER_MAPPER, + ], + [ + 'type' => 'service', + 'name' => self::USER_VALIDATOR, + ], + [ + 'type' => 'service', + 'name' => self::USER_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => self::SECURITY, ], ], ] diff --git a/src/Domain/Components/DataSource/Auth/AuthInput.php b/src/Domain/Components/DataSource/Auth/AuthInput.php index cda5ced..ea53cec 100644 --- a/src/Domain/Components/DataSource/Auth/AuthInput.php +++ b/src/Domain/Components/DataSource/Auth/AuthInput.php @@ -44,11 +44,11 @@ public static function new(SanitizerInterface $sanitizer, array $input): self $sanitized = $sanitizer->sanitize($input); /** @var string|null $email */ - $email = $sanitized['email'] ?? null; + $email = $sanitized['email'] ?? null; /** @var string|null $password */ $password = $sanitized['password'] ?? null; /** @var string|null $token */ - $token = $sanitized['token'] ?? null; + $token = $sanitized['token'] ?? null; return new self($email, $password, $token); } diff --git a/src/Domain/Components/DataSource/User/UserMapper.php b/src/Domain/Components/DataSource/User/UserMapper.php index 31eebc8..7f16302 100644 --- a/src/Domain/Components/DataSource/User/UserMapper.php +++ b/src/Domain/Components/DataSource/User/UserMapper.php @@ -13,8 +13,6 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; -use Phalcon\Api\Domain\ADR\InputTypes; - /** * @phpstan-import-type TUser from UserTypes * @phpstan-import-type TUserDbRecord from UserTypes diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index e075fce..7a525f5 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -156,9 +156,8 @@ public function insert(User $user): int */ public function update(User $user): int { - - $row = $this->mapper->db($user); - $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + $row = $this->mapper->db($user); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); /** @var int $userId */ $userId = $row['usr_id']; @@ -183,7 +182,7 @@ public function update(User $user): int */ unset($columns['usr_created_date'], $columns['usr_created_usr_id']); - $update = Update::new($this->connection); + $update = Update::new($this->connection); $update ->table($this->table) ->columns($columns) diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Components/Encryption/JWTToken.php index b1deaf2..ab2dbfc 100644 --- a/src/Domain/Components/Encryption/JWTToken.php +++ b/src/Domain/Components/Encryption/JWTToken.php @@ -18,7 +18,6 @@ use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; @@ -140,7 +139,7 @@ public function validate( /** @var string $tokenId */ $tokenId = $user->tokenId; /** @var string $issuer */ - $issuer = $user->issuer; + $issuer = $user->issuer; /** @var string $tokenPassword */ $tokenPassword = $user->tokenPassword; /** @var int $userId */ diff --git a/src/Domain/Components/Enums/Http/RoutesEnum.php b/src/Domain/Components/Enums/Http/RoutesEnum.php index e28552c..f703473 100644 --- a/src/Domain/Components/Enums/Http/RoutesEnum.php +++ b/src/Domain/Components/Enums/Http/RoutesEnum.php @@ -31,9 +31,9 @@ enum RoutesEnum: int */ public const EVENT_BEFORE = 'before'; public const EVENT_FINISH = 'finish'; - public const GET = 'get'; - public const POST = 'post'; - public const PUT = 'put'; + public const GET = 'get'; + public const POST = 'post'; + public const PUT = 'put'; case authLoginPost = 11; case authLogoutPost = 12; diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php index ccb9bf4..53ba2e9 100644 --- a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php @@ -15,7 +15,6 @@ use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Services/Auth/AbstractAuthService.php index d55c1ff..e5ecca6 100644 --- a/src/Domain/Services/Auth/AbstractAuthService.php +++ b/src/Domain/Services/Auth/AbstractAuthService.php @@ -18,7 +18,6 @@ use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Env\EnvManager; diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Services/Auth/LoginPostService.php index 50730c1..9e26a68 100644 --- a/src/Domain/Services/Auth/LoginPostService.php +++ b/src/Domain/Services/Auth/LoginPostService.php @@ -15,7 +15,6 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; diff --git a/src/Domain/Services/User/AbstractUserService.php b/src/Domain/Services/User/AbstractUserService.php index a907c59..6a69945 100644 --- a/src/Domain/Services/User/AbstractUserService.php +++ b/src/Domain/Services/User/AbstractUserService.php @@ -25,8 +25,6 @@ use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; -use function array_shift; - /** * @phpstan-import-type TUser from UserTypes */ @@ -47,7 +45,6 @@ public function __construct( protected readonly UserValidator $validator, protected readonly SanitizerInterface $sanitizer, protected readonly Security $security, - ) { } diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Services/User/UserPostService.php index ba39ebb..f29d544 100644 --- a/src/Domain/Services/User/UserPostService.php +++ b/src/Domain/Services/User/UserPostService.php @@ -37,7 +37,7 @@ public function __invoke(array $input): Payload { $inputObject = UserInput::new($this->sanitizer, $input); /** @var TValidationErrors $errors */ - $errors = $this->validator->validate($inputObject); + $errors = $this->validator->validate($inputObject); /** * Errors exist - return early diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php index 82871f8..3f189c0 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Services/User/UserPutService.php @@ -37,7 +37,7 @@ public function __invoke(array $input): Payload { $inputObject = UserInput::new($this->sanitizer, $input); /** @var TValidationErrors $errors */ - $errors = $this->validator->validate($inputObject); + $errors = $this->validator->validate($inputObject); /** * Errors exist - return early diff --git a/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php b/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php index 0c99a1c..5ee9857 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php @@ -122,7 +122,7 @@ public function testToArray(): void $actual = $userInput->updatedUserId; $this->assertSame($expected, $actual); - $expected =get_object_vars($userInput); + $expected = get_object_vars($userInput); $actual = $userInput->toArray(); $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php b/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php index 17b517e..f7a24fa 100644 --- a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php +++ b/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php @@ -22,8 +22,6 @@ use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Encryption\Security\JWT\Token\Token; -use function sleep; - final class JWTTokenTest extends AbstractUnitTestCase { private JWTToken $jwtToken; @@ -39,8 +37,8 @@ public function testGetForUserReturnsTokenString(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getUserData(); - $domainUser = $userMapper->domain($userData); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); $token = $this->jwtToken->getForUser($domainUser); $this->assertIsString($token); @@ -51,8 +49,8 @@ public function testGetObjectReturnsPlainToken(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getUserData(); - $domainUser = $userMapper->domain($userData); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); $tokenString = $this->jwtToken->getForUser($domainUser); @@ -81,8 +79,8 @@ public function testGetUserReturnsUserArray(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getUserData(); - $domainUser = $userMapper->domain($userData); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); $tokenString = $this->jwtToken->getForUser($domainUser); $plain = $this->jwtToken->getObject($tokenString); @@ -126,11 +124,11 @@ public function testValidateSuccess(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getUserData(); - $domainUser = $userMapper->domain($userData); + $userData = $this->getUserData(); + $domainUser = $userMapper->domain($userData); - $tokenString = $this->jwtToken->getForUser($domainUser); - $plain = $this->jwtToken->getObject($tokenString); + $tokenString = $this->jwtToken->getForUser($domainUser); + $plain = $this->jwtToken->getObject($tokenString); $actual = $this->jwtToken->validate($plain, $domainUser); diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php index bcbfec2..0489971 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php @@ -70,8 +70,8 @@ public function testValidateTokenClaimsFailure( ): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); [$micro, $middleware, $jwtToken] = $this->setupTest(); @@ -118,9 +118,9 @@ public function testValidateTokenClaimsSuccess(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); - $tokenUser = $userMapper->domain($user); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $userMapper->domain($user); [$micro, $middleware, $jwtToken] = $this->setupTest(); diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php index 8ce166e..ec74105 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php @@ -30,9 +30,9 @@ public function testValidateTokenRevokedFailureInvalidToken(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); - $tokenUser = $userMapper->domain($user); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $userMapper->domain($user); [$micro, $middleware] = $this->setupTest(); @@ -73,9 +73,9 @@ public function testValidateTokenRevokedSuccess(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); - $tokenUser = $userMapper->domain($user); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); + $tokenUser = $userMapper->domain($user); [$micro, $middleware] = $this->setupTest(); diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php index 4a7ebbf..f7f5f99 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php @@ -85,8 +85,8 @@ public function testValidateTokenStructureSuccess(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); - $domainUser = $userMapper->domain($userData); + $userData = $this->getNewUserData(); + $domainUser = $userMapper->domain($userData); [$micro, $middleware] = $this->setupTest(); /** @var JWTToken $jwtToken */ diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php index a207a99..c3459e0 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php @@ -63,8 +63,8 @@ public function testValidateTokenUserSuccess(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $migration = new UsersMigration($this->getConnection()); - $user = $this->getNewUser($migration); + $migration = new UsersMigration($this->getConnection()); + $user = $this->getNewUser($migration); $domainUser = $userMapper->domain($user); [$micro, $middleware, $jwtToken] = $this->setupTest(); diff --git a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php index b3d60be..fe69a9a 100644 --- a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php @@ -110,22 +110,18 @@ public function testServiceWithCredentials(): void $this->assertNotEmpty($jwt['refreshToken']); } - public function testServiceWrongCredentialsForUser(): void + public function testServiceWrongCredentials(): void { + $faker = Factory::create(); /** @var LoginPostService $service */ - $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); - $migration = new UsersMigration($this->getConnection()); - - $password = 'password'; + $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); /** * Issue a wrong password */ - $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); - $email = $dbUser['usr_email']; $payload = [ - 'email' => $email, - 'password' => $password . '2', + 'email' => $faker->email(), + 'password' => $faker->password(), ]; $payload = $service->__invoke($payload); @@ -142,18 +138,22 @@ public function testServiceWrongCredentialsForUser(): void $this->assertSame($expected, $actual); } - public function testServiceWrongCredentials(): void + public function testServiceWrongCredentialsForUser(): void { - $faker = Factory::create(); /** @var LoginPostService $service */ $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + $migration = new UsersMigration($this->getConnection()); + + $password = 'password'; /** * Issue a wrong password */ + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; $payload = [ - 'email' => $faker->email(), - 'password' => $faker->password(), + 'email' => $email, + 'password' => $password . '2', ]; $payload = $service->__invoke($payload); diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Services/User/UserServicePostTest.php index a4082ca..231523b 100644 --- a/tests/Unit/Domain/Services/User/UserServicePostTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePostTest.php @@ -24,7 +24,6 @@ use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; -use function array_shift; use function htmlspecialchars; #[BackupGlobals(true)] diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php index 976afd2..34fab9a 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -25,16 +25,14 @@ use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use PHPUnit\Framework\Attributes\BackupGlobals; -use function array_shift; - #[BackupGlobals(true)] final class UserServicePutTest extends AbstractUnitTestCase { - public function testServiceFailureRecordNotFound(): void + public function testServiceFailureNoIdReturned(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -45,12 +43,14 @@ public function testServiceFailureRecordNotFound(): void ->disableOriginalConstructor() ->onlyMethods( [ - 'findById' + 'update', + 'findById', ] ) ->getMock() ; - $userRepository->method('findById')->willReturn(null); + $userRepository->method('update')->willReturn(0); + $userRepository->method('findById')->willReturn($findByUser); $repository = $this ->getMockBuilder(QueryRepository::class) @@ -80,7 +80,7 @@ public function testServiceFailureRecordNotFound(): void $payload = $service->__invoke($updateUser); - $expected = DomainStatus::NOT_FOUND; + $expected = DomainStatus::ERROR; $actual = $payload->getStatus(); $this->assertSame($expected, $actual); @@ -89,16 +89,16 @@ public function testServiceFailureRecordNotFound(): void $errors = $actual['errors']; - $expected = [['Record(s) not found']]; + $expected = [['Cannot update database record: No id returned']]; $actual = $errors; $this->assertSame($expected, $actual); } - public function testServiceFailureNoIdReturned(): void + public function testServiceFailurePdoError(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -110,13 +110,16 @@ public function testServiceFailureNoIdReturned(): void ->onlyMethods( [ 'update', - 'findById' + 'findById', ] ) ->getMock() ; - $userRepository->method('update')->willReturn(0); $userRepository->method('findById')->willReturn($findByUser); + $userRepository + ->method('update') + ->willThrowException(new PDOException('abcde')) + ; $repository = $this ->getMockBuilder(QueryRepository::class) @@ -128,19 +131,21 @@ public function testServiceFailureNoIdReturned(): void ) ->getMock() ; - $repository->method('user')->willReturn($userRepository); - - $this->container->setShared(Container::REPOSITORY, $repository); + $repository + ->method('user') + ->willReturn($userRepository) + ; + $this->container->set(Container::REPOSITORY, $repository); /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); - /** - * Update user - */ $userData = $this->getNewUserData(); $userData['usr_id'] = 1; + /** + * $userData is a db record. We need a domain object here + */ $updateUser = $userMapper->domain($userData); $updateUser = $updateUser->toArray(); @@ -155,16 +160,16 @@ public function testServiceFailureNoIdReturned(): void $errors = $actual['errors']; - $expected = [['Cannot update database record: No id returned']]; + $expected = [['Cannot update database record: abcde']]; $actual = $errors; $this->assertSame($expected, $actual); } - public function testServiceFailurePdoError(): void + public function testServiceFailureRecordNotFound(): void { /** @var UserMapper $userMapper */ $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -175,17 +180,12 @@ public function testServiceFailurePdoError(): void ->disableOriginalConstructor() ->onlyMethods( [ - 'update', - 'findById' + 'findById', ] ) ->getMock() ; - $userRepository->method('findById')->willReturn($findByUser); - $userRepository - ->method('update') - ->willThrowException(new PDOException('abcde')) - ; + $userRepository->method('findById')->willReturn(null); $repository = $this ->getMockBuilder(QueryRepository::class) @@ -197,27 +197,25 @@ public function testServiceFailurePdoError(): void ) ->getMock() ; - $repository - ->method('user') - ->willReturn($userRepository) - ; + $repository->method('user')->willReturn($userRepository); + + $this->container->setShared(Container::REPOSITORY, $repository); - $this->container->set(Container::REPOSITORY, $repository); /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); + /** + * Update user + */ $userData = $this->getNewUserData(); $userData['usr_id'] = 1; - /** - * $userData is a db record. We need a domain object here - */ $updateUser = $userMapper->domain($userData); $updateUser = $updateUser->toArray(); $payload = $service->__invoke($updateUser); - $expected = DomainStatus::ERROR; + $expected = DomainStatus::NOT_FOUND; $actual = $payload->getStatus(); $this->assertSame($expected, $actual); @@ -226,7 +224,7 @@ public function testServiceFailurePdoError(): void $errors = $actual['errors']; - $expected = [['Cannot update database record: abcde']]; + $expected = [['Record(s) not found']]; $actual = $errors; $this->assertSame($expected, $actual); } @@ -492,8 +490,8 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['createdUserId']; $this->assertSame($expected, $actual); - $today = date('Y-m-d '); - $actual = $data['updatedDate']; + $today = date('Y-m-d '); + $actual = $data['updatedDate']; $this->assertStringContainsString($today, $actual); $expected = $dbUser['usr_updated_usr_id']; From e2bd5ef9a2210eb35580a18b37cf883c1deea40a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:48:35 -0600 Subject: [PATCH 172/301] [#.x] - removed cache and query repository - major refactoring --- src/Domain/Components/Cache/Cache.php | 134 ------------------ .../Components/DataSource/QueryRepository.php | 45 ------ 2 files changed, 179 deletions(-) delete mode 100644 src/Domain/Components/Cache/Cache.php delete mode 100644 src/Domain/Components/DataSource/QueryRepository.php diff --git a/src/Domain/Components/Cache/Cache.php b/src/Domain/Components/Cache/Cache.php deleted file mode 100644 index e1f38ef..0000000 --- a/src/Domain/Components/Cache/Cache.php +++ /dev/null @@ -1,134 +0,0 @@ - - * - * 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\Components\Cache; - -use DateTimeImmutable; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Cache\Adapter\Redis; -use Phalcon\Cache\Cache as PhalconCache; -use Psr\SimpleCache\InvalidArgumentException; - -use function sha1; - -class Cache extends PhalconCache -{ - /** @var int */ - public const CACHE_LIFETIME_DAY = 86400; - /** @var int */ - public const CACHE_LIFETIME_HOUR = 3600; - /** - * Cache Timeouts - */ - /** @var int */ - public const CACHE_LIFETIME_MINUTE = 60; - /** @var int */ - public const CACHE_LIFETIME_MONTH = 2592000; - /** - * Default token expiry - 4 hours - */ - /** @var int */ - public const CACHE_TOKEN_EXPIRY = 14400; - /** - * Cache masks - */ - /** @var string */ - private const MASK_TOKEN_USER = 'tk-%s-%s'; - - /** - * @param User $domainUser - * @param string $token - * - * @return string - */ - public function getCacheTokenKey(User $domainUser, string $token): string - { - $tokenString = ''; - if (true !== empty($token)) { - $tokenString = sha1($token); - } - - return sprintf( - self::MASK_TOKEN_USER, - $domainUser->id, - $tokenString - ); - } - - /** - * @param EnvManager $env - * @param User $domainUser - * - * @return bool - * @throws InvalidArgumentException - */ - public function invalidateForUser( - EnvManager $env, - User $domainUser - ): bool { - /** - * We could store the tokens in the database but this way is faster - * and Redis also has a TTL which auto expires elements. - * - * To get all the keys for a user, we use the underlying adapter - * of the cache which is Redis and call the `getKeys()` on it. The - * keys will come back with the prefix defined in the adapter. In order - * to delete them, we need to remove the prefix because `delete()` will - * automatically prepend each key with it. - */ - /** @var Redis $redis */ - $redis = $this->getAdapter(); - $pattern = $this->getCacheTokenKey($domainUser, ''); - $keys = $redis->getKeys($pattern); - /** @var string $prefix */ - $prefix = $env->get('CACHE_PREFIX', '-rest-', 'string'); - $newKeys = []; - /** @var string $key */ - foreach ($keys as $key) { - $newKeys[] = str_replace($prefix, '', $key); - } - - return $this->deleteMultiple($newKeys); - } - - /** - * @param EnvManager $env - * @param User $domainUser - * @param string $token - * - * @return bool - * @throws InvalidArgumentException - */ - public function storeTokenInCache( - EnvManager $env, - User $domainUser, - string $token - ): bool { - $cacheKey = $this->getCacheTokenKey($domainUser, $token); - /** @var int $expiration */ - $expiration = $env->get('TOKEN_EXPIRATION', self::CACHE_TOKEN_EXPIRY, 'int'); - $expirationDate = (new DateTimeImmutable()) - ->modify('+' . $expiration . ' seconds') - ->format(Dates::DATE_TIME_FORMAT) - ; - - $payload = [ - 'token' => $token, - 'expiry' => $expirationDate, - ]; - - return $this->set($cacheKey, $payload, $expiration); - } -} diff --git a/src/Domain/Components/DataSource/QueryRepository.php b/src/Domain/Components/DataSource/QueryRepository.php deleted file mode 100644 index 08dce4d..0000000 --- a/src/Domain/Components/DataSource/QueryRepository.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * 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\Components\DataSource; - -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; -use Phalcon\DataMapper\Pdo\Connection; - -class QueryRepository -{ - private ?UserRepositoryInterface $user = null; - - /** - * @param Connection $connection - */ - public function __construct( - private readonly Connection $connection, - private readonly UserMapper $userMapper, - ) { - } - - /** - * @return UserRepositoryInterface - */ - public function user(): UserRepositoryInterface - { - if (null === $this->user) { - $this->user = new UserRepository($this->connection, $this->userMapper); - } - - return $this->user; - } -} From 82126b05ef08eb6701fe364476155a4e08c7447a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:49:12 -0600 Subject: [PATCH 173/301] [#.x] - refactored input classes --- .../Components/DataSource/AbstractInput.php | 55 +++++++++++++ .../Components/DataSource/Auth/AuthInput.php | 22 +++--- .../Components/DataSource/User/UserInput.php | 78 ++++++++++--------- 3 files changed, 105 insertions(+), 50 deletions(-) create mode 100644 src/Domain/Components/DataSource/AbstractInput.php diff --git a/src/Domain/Components/DataSource/AbstractInput.php b/src/Domain/Components/DataSource/AbstractInput.php new file mode 100644 index 0000000..16993b7 --- /dev/null +++ b/src/Domain/Components/DataSource/AbstractInput.php @@ -0,0 +1,55 @@ + + * + * 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\Components\DataSource; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; + +/** + * Base factory for input DTOs. + * + * Concrete DTOs must implement protected static function + * + * `fromArray(array $sanitized): static` + * + * and keep themselves immutable/read\-only. + * + * @phpstan-import-type TInputSanitize from InputTypes + */ +abstract class AbstractInput +{ + /** + * Factory that accepts a SanitizerInterface and returns the concrete DTO. + * + * @param SanitizerInterface $sanitizer + * @param TInputSanitize $input + * + * @return static + */ + public static function new(SanitizerInterface $sanitizer, array $input): static + { + $sanitized = $sanitizer->sanitize($input); + + return static::fromArray($sanitized); + } + + /** + * Build the concrete DTO from a sanitized array. + * + * @param TInputSanitize $sanitized + * + * @return static + */ + abstract protected static function fromArray(array $sanitized): static; +} diff --git a/src/Domain/Components/DataSource/Auth/AuthInput.php b/src/Domain/Components/DataSource/Auth/AuthInput.php index ea53cec..3fe35d6 100644 --- a/src/Domain/Components/DataSource/Auth/AuthInput.php +++ b/src/Domain/Components/DataSource/Auth/AuthInput.php @@ -14,12 +14,12 @@ namespace Phalcon\Api\Domain\Components\DataSource\Auth; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use Phalcon\Api\Domain\Components\DataSource\AbstractInput; /** * @phpstan-import-type TAuthInput from InputTypes */ -final class AuthInput +final class AuthInput extends AbstractInput { /** * @param string|null $email @@ -34,21 +34,17 @@ public function __construct( } /** - * @param SanitizerInterface $sanitizer - * @param TAuthInput $input + * Build the concrete DTO from a sanitized array. * - * @return self + * @param TAuthInput $sanitized + * + * @return static */ - public static function new(SanitizerInterface $sanitizer, array $input): self + protected static function fromArray(array $sanitized): static { - $sanitized = $sanitizer->sanitize($input); - - /** @var string|null $email */ - $email = $sanitized['email'] ?? null; - /** @var string|null $password */ + $email = $sanitized['email'] ?? null; $password = $sanitized['password'] ?? null; - /** @var string|null $token */ - $token = $sanitized['token'] ?? null; + $token = $sanitized['token'] ?? null; return new self($email, $password, $token); } diff --git a/src/Domain/Components/DataSource/User/UserInput.php b/src/Domain/Components/DataSource/User/UserInput.php index e983dc7..de0c63a 100644 --- a/src/Domain/Components/DataSource/User/UserInput.php +++ b/src/Domain/Components/DataSource/User/UserInput.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use Phalcon\Api\Domain\Components\DataSource\AbstractInput; use function get_object_vars; @@ -22,11 +22,11 @@ * @phpstan-import-type TUserInput from InputTypes * @phpstan-import-type TUser from UserTypes */ -final class UserInput +final class UserInput extends AbstractInput { /** - * @param int|null $id - * @param int|null $status + * @param int $id + * @param int $status * @param string|null $email * @param string|null $password * @param string|null $namePrefix @@ -44,8 +44,8 @@ final class UserInput * @param int|null $updatedUserId */ public function __construct( - public readonly ?int $id, - public readonly ?int $status, + public readonly int $id, + public readonly int $status, public readonly ?string $email, public readonly ?string $password, public readonly ?string $namePrefix, @@ -65,45 +65,49 @@ public function __construct( } /** - * @param SanitizerInterface $sanitizer - * @param TUserInput $input - * - * @return self + * @return TUser */ - public static function new(SanitizerInterface $sanitizer, array $input): self + public function toArray(): array { - /** @var TUser $sanitized */ - $sanitized = $sanitizer->sanitize($input); + /** @var TUser $vars */ + $vars = get_object_vars($this); - return new self( - $sanitized['id'], - $sanitized['status'], - $sanitized['email'], - $sanitized['password'], - $sanitized['namePrefix'], - $sanitized['nameFirst'], - $sanitized['nameMiddle'], - $sanitized['nameLast'], - $sanitized['nameSuffix'], - $sanitized['issuer'], - $sanitized['tokenPassword'], - $sanitized['tokenId'], - $sanitized['preferences'], - $sanitized['createdDate'], - $sanitized['createdUserId'], - $sanitized['updatedDate'], - $sanitized['updatedUserId'] - ); + return $vars; } /** - * @return TUser + * Build the concrete DTO from a sanitized array. + * + * @param TUserInput $sanitized + * + * @return static */ - public function toArray(): array + protected static function fromArray(array $sanitized): static { - /** @var TUser $vars */ - $vars = get_object_vars($this); + $id = isset($sanitized['id']) ? (int)$sanitized['id'] : 0; + $status = isset($sanitized['status']) ? (int)$sanitized['status'] : 0; - return $vars; + $createdUserId = isset($sanitized['createdUserId']) ? (int)$sanitized['createdUserId'] : 0; + $updatedUserId = isset($sanitized['updatedUserId']) ? (int)$sanitized['updatedUserId'] : 0; + + return new self( + $id, + $status, + isset($sanitized['email']) ? (string)$sanitized['email'] : null, + isset($sanitized['password']) ? (string)$sanitized['password'] : null, + isset($sanitized['namePrefix']) ? (string)$sanitized['namePrefix'] : null, + isset($sanitized['nameFirst']) ? (string)$sanitized['nameFirst'] : null, + isset($sanitized['nameMiddle']) ? (string)$sanitized['nameMiddle'] : null, + isset($sanitized['nameLast']) ? (string)$sanitized['nameLast'] : null, + isset($sanitized['nameSuffix']) ? (string)$sanitized['nameSuffix'] : null, + isset($sanitized['issuer']) ? (string)$sanitized['issuer'] : null, + isset($sanitized['tokenPassword']) ? (string)$sanitized['tokenPassword'] : null, + isset($sanitized['tokenId']) ? (string)$sanitized['tokenId'] : null, + isset($sanitized['preferences']) ? (string)$sanitized['preferences'] : null, + isset($sanitized['createdDate']) ? (string)$sanitized['createdDate'] : null, + $createdUserId, + isset($sanitized['updatedDate']) ? (string)$sanitized['updatedDate'] : null, + $updatedUserId + ); } } From 9bb15818fab3227fd053e9c0e2b69cf147887429 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:49:37 -0600 Subject: [PATCH 174/301] [#.x] - adjusted sanitizers and introduced interface --- .../Components/DataSource/Auth/AuthSanitizer.php | 5 +++-- .../{ => Interfaces}/SanitizerInterface.php | 10 ++++++---- .../Components/DataSource/User/UserSanitizer.php | 14 ++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) rename src/Domain/Components/DataSource/{ => Interfaces}/SanitizerInterface.php (65%) diff --git a/src/Domain/Components/DataSource/Auth/AuthSanitizer.php b/src/Domain/Components/DataSource/Auth/AuthSanitizer.php index fdf86fd..3fa50de 100644 --- a/src/Domain/Components/DataSource/Auth/AuthSanitizer.php +++ b/src/Domain/Components/DataSource/Auth/AuthSanitizer.php @@ -14,8 +14,9 @@ namespace Phalcon\Api\Domain\Components\DataSource\Auth; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; use Phalcon\Filter\Filter; +use Phalcon\Filter\FilterInterface; /** * @phpstan-import-type TAuthInput from InputTypes @@ -23,7 +24,7 @@ final class AuthSanitizer implements SanitizerInterface { public function __construct( - private readonly Filter $filter, + private readonly FilterInterface $filter, ) { } diff --git a/src/Domain/Components/DataSource/SanitizerInterface.php b/src/Domain/Components/DataSource/Interfaces/SanitizerInterface.php similarity index 65% rename from src/Domain/Components/DataSource/SanitizerInterface.php rename to src/Domain/Components/DataSource/Interfaces/SanitizerInterface.php index 5620f01..bf184b1 100644 --- a/src/Domain/Components/DataSource/SanitizerInterface.php +++ b/src/Domain/Components/DataSource/Interfaces/SanitizerInterface.php @@ -11,19 +11,21 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource; +namespace Phalcon\Api\Domain\Components\DataSource\Interfaces; + +use Phalcon\Api\Domain\ADR\InputTypes; /** - * @phpstan-type TInput array + * @phpstan-import-type TInputSanitize from InputTypes */ interface SanitizerInterface { /** * Return a sanitized array of the input * - * @param TInput $input + * @param TInputSanitize $input * - * @return TInput + * @return TInputSanitize */ public function sanitize(array $input): array; } diff --git a/src/Domain/Components/DataSource/User/UserSanitizer.php b/src/Domain/Components/DataSource/User/UserSanitizer.php index bfdf298..98c073d 100644 --- a/src/Domain/Components/DataSource/User/UserSanitizer.php +++ b/src/Domain/Components/DataSource/User/UserSanitizer.php @@ -13,25 +13,27 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; -use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; use Phalcon\Filter\Filter; +use Phalcon\Filter\FilterInterface; /** - * @phpstan-import-type TUser from UserTypes + * @phpstan-import-type TUserInput from InputTypes */ final class UserSanitizer implements SanitizerInterface { public function __construct( - private readonly Filter $filter, + private readonly FilterInterface $filter, ) { } /** * Return a sanitized array of the input * - * @param TUser $input + * @param TUserInput $input * - * @return TUser + * @return TUserInput */ public function sanitize(array $input): array { @@ -73,7 +75,7 @@ public function sanitize(array $input): array $sanitized[$name] = $value; } - /** @var TUser $sanitized */ + /** @var TUserInput $sanitized */ return $sanitized; } From 2ca3e12910caeb0b80579899de4a808f81fa61ce Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:50:25 -0600 Subject: [PATCH 175/301] [#.x] - validator interface, validator classes for endpoints and tests --- .../DataSource/Auth/AuthLoginValidator.php | 41 +++++++++ .../DataSource/Auth/AuthTokenValidator.php | 90 +++++++++++++++++++ .../DataSource/User/UserValidator.php | 13 ++- .../Validation/ValidatorInterface.php | 29 ++++++ .../DataSource/User/UserValidatorTest.php | 6 +- 5 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 src/Domain/Components/DataSource/Auth/AuthLoginValidator.php create mode 100644 src/Domain/Components/DataSource/Auth/AuthTokenValidator.php create mode 100644 src/Domain/Components/DataSource/Validation/ValidatorInterface.php diff --git a/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php b/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php new file mode 100644 index 0000000..c2a4d78 --- /dev/null +++ b/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php @@ -0,0 +1,41 @@ + + * + * 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\Components\DataSource\Auth; + +use Phalcon\Api\Domain\Components\DataSource\Validation\Result; +use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; + +final class AuthLoginValidator implements ValidatorInterface +{ + /** + * Validate a AuthInput and return an array of errors. + * Empty array means valid. + * + * @param AuthInput $input + * + * @return Result + */ + public function validate(mixed $input): Result + { + /** @var AuthInput $input */ + if (true === empty($input->email) || true === empty($input->password)) { + return Result::error( + [HttpCodesEnum::AppIncorrectCredentials->error()] + ); + } + + return Result::success(); + } +} diff --git a/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php b/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php new file mode 100644 index 0000000..8b8e1d6 --- /dev/null +++ b/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php @@ -0,0 +1,90 @@ + + * + * 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\Components\DataSource\Auth; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; +use Phalcon\Api\Domain\Components\DataSource\Validation\Result; +use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Components\Encryption\TokenManager; +use Phalcon\Api\Domain\Components\Encryption\TokenManagerInterface; +use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Encryption\Security\JWT\Token\Token; + +final class AuthTokenValidator implements ValidatorInterface +{ + public function __construct( + private TokenManagerInterface $tokenManager, + private UserRepositoryInterface $userRepository, + ) { + } + + /** + * Validate a AuthInput and return an array of errors. + * Empty array means valid. + * + * @param AuthInput $input + * + * @return Result + */ + public function validate(mixed $input): Result + { + /** @var AuthInput $input */ + if (true === empty($input->token)) { + return Result::error([HttpCodesEnum::AppTokenNotPresent->error()]); + } + + $token = $input->token; + $tokenObject = $this->tokenManager->getObject($token); + if (null === $tokenObject) { + return Result::error([HttpCodesEnum::AppTokenNotValid->error()]); + } + + if ($this->tokenIsNotRefresh($tokenObject)) { + return Result::error([HttpCodesEnum::AppTokenNotValid->error()]); + } + + $domainUser = $this->tokenManager->getUser($this->userRepository, $tokenObject); + if (null === $domainUser) { + return Result::error([HttpCodesEnum::AppTokenInvalidUser->error()]); + } + + $errors = $this->tokenManager->validate($tokenObject, $domainUser); + if (!empty($errors)) { + return Result::error($errors); + } + + $result = Result::success(); + $result->setMeta('user', $domainUser); + + return $result; + } + + /** + * Return if the token is a refresh one or not + * + * @param Token $tokenObject + * + * @return bool + */ + private function tokenIsNotRefresh(Token $tokenObject): bool + { + $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); + + return false === $isRefresh; + } +} diff --git a/src/Domain/Components/DataSource/User/UserValidator.php b/src/Domain/Components/DataSource/User/UserValidator.php index 15f2e6b..4aae41b 100644 --- a/src/Domain/Components/DataSource/User/UserValidator.php +++ b/src/Domain/Components/DataSource/User/UserValidator.php @@ -14,11 +14,10 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\Validation\Result; +use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; -/** - * @phpstan-import-type TValidationErrors from InputTypes - */ -final class UserValidator +final class UserValidator implements ValidatorInterface { /** * Validate a UserInput and return an array of errors. @@ -26,9 +25,9 @@ final class UserValidator * * @param UserInput $input * - * @return TValidationErrors|array{} + * @return Result */ - public function validate(UserInput $input): array + public function validate(mixed $input): Result { $errors = []; $required = [ @@ -50,6 +49,6 @@ public function validate(UserInput $input): array * @todo add validators */ - return $errors; + return new Result($errors); } } diff --git a/src/Domain/Components/DataSource/Validation/ValidatorInterface.php b/src/Domain/Components/DataSource/Validation/ValidatorInterface.php new file mode 100644 index 0000000..72979d2 --- /dev/null +++ b/src/Domain/Components/DataSource/Validation/ValidatorInterface.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\Domain\Components\DataSource\Validation; + +/** + * Validator contract. Accepts a DTO or input and returns a Result. + */ +interface ValidatorInterface +{ + /** + * Validate a DTO or input structure. + * + * @param mixed $input DTO or array + * + * @return Result + */ + public function validate(mixed $input): Result; +} diff --git a/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php b/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php index a71e7b2..f1ea417 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php @@ -31,7 +31,8 @@ public function testError(): void $userInput = UserInput::new($sanitizer, $input); $validator = new UserValidator(); - $actual = $validator->validate($userInput); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); $expected = [ ['Field email cannot be empty.'], @@ -61,7 +62,8 @@ public function testSuccess(): void $userInput = UserInput::new($sanitizer, $input); $validator = new UserValidator(); - $actual = $validator->validate($userInput); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); $expected = []; $this->assertSame($expected, $actual); From 78064662f3e5a628e2fd9488ca9208b0fffb4407 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:50:53 -0600 Subject: [PATCH 176/301] [#.x] - introducing token manager and token cache for token operations + interfaces --- src/Domain/Components/Encryption/JWTToken.php | 15 +- .../Components/Encryption/TokenCache.php | 114 ++++++++++++++ .../Encryption/TokenCacheInterface.php | 47 ++++++ .../Components/Encryption/TokenManager.php | 140 ++++++++++++++++++ .../Encryption/TokenManagerInterface.php | 89 +++++++++++ .../Components/Encryption/JWTTokenTest.php | 18 +-- 6 files changed, 399 insertions(+), 24 deletions(-) create mode 100644 src/Domain/Components/Encryption/TokenCache.php create mode 100644 src/Domain/Components/Encryption/TokenCacheInterface.php create mode 100644 src/Domain/Components/Encryption/TokenManager.php create mode 100644 src/Domain/Components/Encryption/TokenManagerInterface.php diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Components/Encryption/JWTToken.php index ab2dbfc..749b4e3 100644 --- a/src/Domain/Components/Encryption/JWTToken.php +++ b/src/Domain/Components/Encryption/JWTToken.php @@ -15,9 +15,10 @@ use DateTimeImmutable; use InvalidArgumentException; -use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; @@ -30,7 +31,7 @@ use Phalcon\Support\Helper\Json\Decode; /** - * @phpstan-type TValidatorErrors array{}|array + * @phpstan-import-type TValidatorErrors from InputTypes * * Removed the final declaration so that this class can be mocked. This * class should not be extended @@ -94,13 +95,13 @@ public function getRefreshForUser(User $user): string } /** - * @param QueryRepository $repository + * @param UserRepositoryInterface $repository * @param Token $token * * @return User|null */ public function getUser( - QueryRepository $repository, + UserRepositoryInterface $repository, Token $token, ): ?User { /** @var string $issuer */ @@ -117,7 +118,7 @@ public function getUser( 'usr_token_id' => $tokenId, ]; - return $repository->user()->findOneBy($criteria); + return $repository->findOneBy($criteria); } /** @@ -177,7 +178,7 @@ private function generateTokenForUser( /** @var int $expiration */ $expiration = $this->env->get( 'TOKEN_EXPIRATION', - Cache::CACHE_TOKEN_EXPIRY, + CacheConstants::CACHE_TOKEN_EXPIRY, 'int' ); diff --git a/src/Domain/Components/Encryption/TokenCache.php b/src/Domain/Components/Encryption/TokenCache.php new file mode 100644 index 0000000..fc06d9b --- /dev/null +++ b/src/Domain/Components/Encryption/TokenCache.php @@ -0,0 +1,114 @@ + + * + * 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\Components\Encryption; + +use DateTimeImmutable; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Cache\Adapter\Redis; +use Phalcon\Cache\Cache; +use Phalcon\Encryption\Security\JWT\Token\Token; +use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\InvalidArgumentException; +use Throwable; + +use function str_replace; + +/** + * Small component to issue/rotate/revoke tokens and + * interact with cache. + * + * @phpstan-import-type TTokenIssue from TokenManagerInterface + * @phpstan-import-type TValidatorErrors from InputTypes + */ +final class TokenCache implements TokenCacheInterface +{ + public function __construct( + private readonly Cache $cache, + ) { + } + + /** + * @param EnvManager $env + * @param User $domainUser + * + * @return bool + * @throws InvalidArgumentException + */ + public function invalidateForUser( + EnvManager $env, + User $domainUser + ): bool { + /** + * We could store the tokens in the database but this way is faster + * and Redis also has a TTL which auto expires elements. + * + * To get all the keys for a user, we use the underlying adapter + * of the cache which is Redis and call the `getKeys()` on it. The + * keys will come back with the prefix defined in the adapter. In order + * to delete them, we need to remove the prefix because `delete()` will + * automatically prepend each key with it. + */ + /** @var Redis $redis */ + $redis = $this->cache->getAdapter(); + $pattern = CacheConstants::getCacheTokenKey($domainUser, ''); + $keys = $redis->getKeys($pattern); + /** @var string $prefix */ + $prefix = $env->get('CACHE_PREFIX', '-rest-', 'string'); + $newKeys = []; + /** @var string $key */ + foreach ($keys as $key) { + $newKeys[] = str_replace($prefix, '', $key); + } + + return $this->cache->deleteMultiple($newKeys); + } + + /** + * @param EnvManager $env + * @param User $domainUser + * @param string $token + * + * @return bool + * @throws InvalidArgumentException + */ + public function storeTokenInCache( + EnvManager $env, + User $domainUser, + string $token + ): bool { + $cacheKey = CacheConstants::getCacheTokenKey($domainUser, $token); + /** @var int $expiration */ + $expiration = $env->get( + 'TOKEN_EXPIRATION', + CacheConstants::CACHE_TOKEN_EXPIRY, + 'int' + ); + $expirationDate = (new DateTimeImmutable()) + ->modify('+' . $expiration . ' seconds') + ->format(Dates::DATE_TIME_FORMAT) + ; + + $payload = [ + 'token' => $token, + 'expiry' => $expirationDate, + ]; + + return $this->cache->set($cacheKey, $payload, $expiration); + } +} diff --git a/src/Domain/Components/Encryption/TokenCacheInterface.php b/src/Domain/Components/Encryption/TokenCacheInterface.php new file mode 100644 index 0000000..d73fffd --- /dev/null +++ b/src/Domain/Components/Encryption/TokenCacheInterface.php @@ -0,0 +1,47 @@ + + * + * 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\Components\Encryption; + +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Psr\SimpleCache\InvalidArgumentException; + +interface TokenCacheInterface +{ + /** + * @param EnvManager $env + * @param User $domainUser + * + * @return bool + * @throws InvalidArgumentException + */ + public function invalidateForUser( + EnvManager $env, + User $domainUser + ): bool; + + /** + * @param EnvManager $env + * @param User $domainUser + * @param string $token + * + * @return bool + * @throws InvalidArgumentException + */ + public function storeTokenInCache( + EnvManager $env, + User $domainUser, + string $token + ): bool; +} diff --git a/src/Domain/Components/Encryption/TokenManager.php b/src/Domain/Components/Encryption/TokenManager.php new file mode 100644 index 0000000..e5afad7 --- /dev/null +++ b/src/Domain/Components/Encryption/TokenManager.php @@ -0,0 +1,140 @@ + + * + * 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\Components\Encryption; + +use DateTimeImmutable; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; +use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Cache\Adapter\Redis; +use Phalcon\Encryption\Security\JWT\Token\Token; +use Psr\SimpleCache\InvalidArgumentException; +use Throwable; + +use function str_replace; + +/** + * Small component to issue/rotate/revoke tokens and + * interact with cache. + * + * @phpstan-import-type TTokenIssue from TokenManagerInterface + * @phpstan-import-type TValidatorErrors from InputTypes + */ +final class TokenManager implements TokenManagerInterface +{ + public function __construct( + private readonly TokenCacheInterface $tokenCache, + private readonly EnvManager $env, + private readonly JWTToken $jwtToken, + ) { + } + + /** + * Parse a token and return either null or a Token object + * + * @param string|null $token + * + * @return Token|null + */ + public function getObject(?string $token): ?Token + { + if (true === empty($token)) { + return null; + } + + try { + return $this->jwtToken->getObject($token); + } catch (Throwable) { + return null; + } + } + + /** + * Return the domain user from the database + * + * @param UserRepositoryInterface $repository + * @param Token $tokenObject + * + * @return User|null + */ + public function getUser( + UserRepositoryInterface $repository, + Token $tokenObject + ): ?User { + return $this->jwtToken->getUser($repository, $tokenObject); + } + + /** + * Issue new tokens for the user (token, refreshToken) + * + * @param User $domainUser + * + * @return TTokenIssue + */ + public function issue(User $domainUser): array + { + $token = $this->jwtToken->getForUser($domainUser); + $refresh = $this->jwtToken->getRefreshForUser($domainUser); + + $this->tokenCache->storeTokenInCache($this->env, $domainUser, $token); + $this->tokenCache->storeTokenInCache($this->env, $domainUser, $refresh); + + return [ + 'token' => $token, + 'refreshToken' => $refresh, + ]; + } + + /** + * Revoke old tokens and issue new ones. + * + * @param User $domainUser + * + * @return TTokenIssue + */ + public function refresh(User $domainUser): array + { + $this->revoke($domainUser); + + return $this->issue($domainUser); + } + + /** + * Revoke cached tokens for a user. + * + * @param User $domainUser + * + * @return void + */ + public function revoke(User $domainUser): void + { + $this->tokenCache->invalidateForUser($this->env, $domainUser); + } + + /** + * Validate token claims + * + * @param Token $tokenObject + * @param User $user + * + * @return TValidatorErrors + */ + public function validate(Token $tokenObject, User $user): array + { + return $this->jwtToken->validate($tokenObject, $user); + } +} diff --git a/src/Domain/Components/Encryption/TokenManagerInterface.php b/src/Domain/Components/Encryption/TokenManagerInterface.php new file mode 100644 index 0000000..d3f4c8a --- /dev/null +++ b/src/Domain/Components/Encryption/TokenManagerInterface.php @@ -0,0 +1,89 @@ + + * + * 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\Components\Encryption; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; +use Phalcon\Encryption\Security\JWT\Token\Token; + +/** + * @phpstan-type TTokenIssue array{ + * token: string, + * refreshToken: string + * } + * + * @phpstan-import-type TValidatorErrors from InputTypes + */ +interface TokenManagerInterface +{ + /** + * Return the domain user from the database + * + * @param UserRepositoryInterface $repository + * @param Token $tokenObject + * + * @return User|null + */ + public function getUser( + UserRepositoryInterface $repository, + Token $tokenObject + ): ?User; + + /** + * Parse a token and return either null or a Token object + * + * @param string $token + * + * @return Token|null + */ + public function getObject(string $token): ?Token; + + /** + * Issue new tokens for the user (token, refreshToken) + * + * @param User $domainUser + * + * @return TTokenIssue + */ + public function issue(User $domainUser): array; + + /** + * Revoke old tokens and issue new ones. + * + * @param User $domainUser + * + * @return TTokenIssue + */ + public function refresh(User $domainUser): array; + + /** + * Revoke cached tokens for a user. + * + * @param User $domainUser + * + * @return void + */ + public function revoke(User $domainUser): void; + + /** + * Validate token claims + * + * @param Token $tokenObject + * @param User $user + * + * @return TValidatorErrors + */ + public function validate(Token $tokenObject, User $user): array; +} diff --git a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php b/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php index f7a24fa..5d93a4c 100644 --- a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php +++ b/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php @@ -14,7 +14,6 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Encryption; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Domain\Components\Encryption\JWTToken; @@ -101,22 +100,7 @@ public function testGetUserReturnsUserArray(): void ->willReturn($domainUser) ; - $mockRepository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $mockRepository->expects($this->once()) - ->method('user') - ->willReturn($userRepository) - ; - - $result = $this->jwtToken->getUser($mockRepository, $plain); + $result = $this->jwtToken->getUser($userRepository, $plain); $this->assertEquals($domainUser, $result); } From 0e834e159ca6cf7716dc9360cdb332dcf6057682 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:51:08 -0600 Subject: [PATCH 177/301] [#.x] - new cache constants --- src/Domain/Components/Constants/Cache.php | 63 +++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/Domain/Components/Constants/Cache.php diff --git a/src/Domain/Components/Constants/Cache.php b/src/Domain/Components/Constants/Cache.php new file mode 100644 index 0000000..be360a0 --- /dev/null +++ b/src/Domain/Components/Constants/Cache.php @@ -0,0 +1,63 @@ + + * + * 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\Components\Constants; + +use Phalcon\Api\Domain\Components\DataSource\User\User; + +use function sha1; + +class Cache +{ + /** @var int */ + public const CACHE_LIFETIME_DAY = 86400; + /** @var int */ + public const CACHE_LIFETIME_HOUR = 3600; + /** + * Cache Timeouts + */ + /** @var int */ + public const CACHE_LIFETIME_MINUTE = 60; + /** @var int */ + public const CACHE_LIFETIME_MONTH = 2592000; + /** + * Default token expiry - 4 hours + */ + /** @var int */ + public const CACHE_TOKEN_EXPIRY = 14400; + /** + * Cache masks + */ + /** @var string */ + private const MASK_TOKEN_USER = 'tk-%s-%s'; + + /** + * @param User $domainUser + * @param string $token + * + * @return string + */ + public static function getCacheTokenKey(User $domainUser, string $token): string + { + $tokenString = ''; + if (true !== empty($token)) { + $tokenString = sha1($token); + } + + return sprintf( + self::MASK_TOKEN_USER, + $domainUser->id, + $tokenString + ); + } +} From 71c9d39d6bc56e0925706751ee371ffa2ef56dce Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:51:22 -0600 Subject: [PATCH 178/301] [#.x] - added validation results class --- .../DataSource/Validation/Result.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/Domain/Components/DataSource/Validation/Result.php diff --git a/src/Domain/Components/DataSource/Validation/Result.php b/src/Domain/Components/DataSource/Validation/Result.php new file mode 100644 index 0000000..7fe3223 --- /dev/null +++ b/src/Domain/Components/DataSource/Validation/Result.php @@ -0,0 +1,91 @@ + + * + * 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\Components\DataSource\Validation; + +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TValidatorErrors from InputTypes + * @phpstan-type TResultMeta array + */ +final class Result +{ + /** + * @param TValidatorErrors $errors + * @param TResultMeta $meta + */ + public function __construct( + private array $errors = [], + private array $meta = [], + ) { + } + + /** + * Create a failure result. + * + * @param TValidatorErrors $errors + * + * @return self + */ + public static function error(array $errors): self + { + return new self($errors); + } + + /** + * @return TValidatorErrors + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * @param string $key + * @param mixed|null $defaultValue + * + * @return mixed + */ + public function getMeta(string $key, mixed $defaultValue = null): mixed + { + return $this->meta[$key] ?? $defaultValue; + } + + /** + * @param string $key + * @param mixed $value + * + * @return void + */ + public function setMeta(string $key, mixed $value): void + { + $this->meta[$key] = $value; + } + + /** + * @return bool + */ + public function isValid(): bool + { + return $this->errors === []; + } + + /** + * @return self + */ + public static function success(): self + { + return new self([]); + } +} From 79322494e4b68e2f179ef28c5a15e75232b1dd9d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:51:40 -0600 Subject: [PATCH 179/301] [#.x] - new mapper interface and adjustments --- .../DataSource/Interfaces/MapperInterface.php | 49 +++++++++++++++++++ .../Components/DataSource/User/UserMapper.php | 6 ++- .../DataSource/User/UserMapperTest.php | 3 +- 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/Domain/Components/DataSource/Interfaces/MapperInterface.php diff --git a/src/Domain/Components/DataSource/Interfaces/MapperInterface.php b/src/Domain/Components/DataSource/Interfaces/MapperInterface.php new file mode 100644 index 0000000..9b1c7c7 --- /dev/null +++ b/src/Domain/Components/DataSource/Interfaces/MapperInterface.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\Components\DataSource\Interfaces; + +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; +use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; + +/** + * Contract for mapping between domain DTO/objects and persistence arrays. + * + * @phpstan-import-type TUser from UserTypes + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + */ +interface MapperInterface +{ + /** + * Map Domain User -> DB row (usr_*) + * + * @return TUserDomainToDbRecord + */ + public function db(UserInput $user): array; + + /** + * Map DB row (usr_*) -> Domain User + * + * @param TUserDbRecord|array{} $row + */ + public function domain(array $row): User; + + /** + * Map input row -> Domain User + * + * @param TUser $row + */ + public function input(array $row): User; +} diff --git a/src/Domain/Components/DataSource/User/UserMapper.php b/src/Domain/Components/DataSource/User/UserMapper.php index 7f16302..b996be1 100644 --- a/src/Domain/Components/DataSource/User/UserMapper.php +++ b/src/Domain/Components/DataSource/User/UserMapper.php @@ -13,19 +13,21 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; +use Phalcon\Api\Domain\Components\DataSource\Interfaces\MapperInterface; + /** * @phpstan-import-type TUser from UserTypes * @phpstan-import-type TUserDbRecord from UserTypes * @phpstan-import-type TUserDomainToDbRecord from UserTypes */ -final class UserMapper +final class UserMapper implements MapperInterface { /** * Map Domain User -> DB row (usr_*) * * @return TUserDomainToDbRecord */ - public function db(User $user): array + public function db(UserInput $user): array { return [ 'usr_id' => $user->id, diff --git a/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php b/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php index a44e353..5511a9c 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php @@ -16,6 +16,7 @@ use Faker\Factory as FakerFactory; use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -36,7 +37,7 @@ public function testDb(): void $createdDate = $faker->date(Dates::DATE_TIME_FORMAT); $updatedDate = $faker->date(Dates::DATE_TIME_FORMAT); - $user = new User( + $user = new UserInput( $faker->numberBetween(1, 1000), $faker->numberBetween(0, 9), $faker->safeEmail(), From 91d76f1bb5a8ca1fa0563f02acbd0cd4d9a92698 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:52:15 -0600 Subject: [PATCH 180/301] [#.x] - added auth and user facades for orchestration --- .../Components/DataSource/Auth/AuthFacade.php | 193 ++++++++++ .../Components/DataSource/User/UserFacade.php | 346 ++++++++++++++++++ 2 files changed, 539 insertions(+) create mode 100644 src/Domain/Components/DataSource/Auth/AuthFacade.php create mode 100644 src/Domain/Components/DataSource/User/UserFacade.php diff --git a/src/Domain/Components/DataSource/Auth/AuthFacade.php b/src/Domain/Components/DataSource/Auth/AuthFacade.php new file mode 100644 index 0000000..6460a73 --- /dev/null +++ b/src/Domain/Components/DataSource/Auth/AuthFacade.php @@ -0,0 +1,193 @@ + + * + * 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\Components\DataSource\Auth; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; +use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Components\Encryption\TokenManagerInterface; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Payload; + +/** + * @phpstan-import-type TAuthLoginInput from InputTypes + * @phpstan-import-type TAuthLogoutInput from InputTypes + * @phpstan-import-type TAuthRefreshInput from InputTypes + */ +final class AuthFacade +{ + /** + * @param UserRepositoryInterface $repository + * @param SanitizerInterface $sanitizer + * @param TokenManagerInterface $tokenManager + * @param Security $security + */ + public function __construct( + private readonly UserRepositoryInterface $repository, + private readonly SanitizerInterface $sanitizer, + private readonly TokenManagerInterface $tokenManager, + private readonly Security $security, + ) { + } + + /** + * Authenticates users (login) + * + * @param TAuthLoginInput $input + * @param ValidatorInterface $validator + * + * @return Payload + */ + public function authenticate( + array $input, + ValidatorInterface $validator + ): Payload { + /** + * Data Transfer Object + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Validate + */ + $validation = $validator->validate($dto); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * Find the user by email + */ + /** @var string $email */ + $email = $dto->email; + $domainUser = $this->repository->findByEmail($email); + if (null === $domainUser) { + return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); + } + + /** + * Verify the password + */ + /** @var string $suppliedPassword */ + $suppliedPassword = $dto->password; + /** @var string $dbPassword */ + $dbPassword = $domainUser->password; + if (true !== $this->security->verify($suppliedPassword, $dbPassword)) { + return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); + } + + /** + * Issue a new set of tokens + */ + $tokens = $this->tokenManager->issue($domainUser); + + /** + * Construct the response + */ + $results = [ + 'authenticated' => true, + 'user' => [ + 'id' => $domainUser->id, + 'name' => $domainUser->fullName(), + 'email' => $domainUser->email, + ], + 'jwt' => [ + 'token' => $tokens['token'], + 'refreshToken' => $tokens['refreshToken'], + ], + ]; + + return Payload::success($results); + } + + /** + * Logout: revoke refresh token after parsing/validation. + * + * @param TAuthLogoutInput $input + * @param ValidatorInterface $validator + * + * @return Payload + */ + public function logout( + array $input, + ValidatorInterface $validator + ): Payload { + /** + * Data Transfer Object + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Validate + */ + $validation = $validator->validate($dto); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * If we are here validation has passed and the Result object + * has the user in the meta store + */ + /** @var User $domainUser */ + $domainUser = $validation->getMeta('user'); + + $this->tokenManager->revoke($domainUser); + + return Payload::success(['authenticated' => false]); + } + + /** + * Refresh: validate refresh token, issue new tokens via TokenManager. + * + * @param TAuthLogoutInput $input + * @param ValidatorInterface $validator + * + * @return Payload + */ + public function refresh( + array $input, + ValidatorInterface $validator + ): Payload { + /** + * Data Transfer Object + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Validate + */ + $validation = $validator->validate($dto); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * If we are here validation has passed and the Result object + * has the user in the meta store + */ + /** @var User $domainUser */ + $domainUser = $validation->getMeta('user'); + + $tokens = $this->tokenManager->refresh($domainUser); + + return Payload::success([ + 'token' => $tokens['token'], + 'refreshToken' => $tokens['refreshToken'], + ]); + } +} diff --git a/src/Domain/Components/DataSource/User/UserFacade.php b/src/Domain/Components/DataSource/User/UserFacade.php new file mode 100644 index 0000000..1b36969 --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserFacade.php @@ -0,0 +1,346 @@ + + * + * 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\Components\DataSource\User; + +use PDOException; +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Components\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Payload; + +use function array_filter; + +/** + * Orchestration for workflow + * + * - Sanitization + * - DTO creation + * - Validation + * - Pre-operation checks (when necessary) + * - Repository operation + * + * @phpstan-import-type TUserInput from InputTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + */ +final class UserFacade +{ + /** + * @param SanitizerInterface $sanitizer + * @param ValidatorInterface $validator + * @param MapperInterface $mapper + * @param UserRepositoryInterface $repository + * @param Security $security + */ + public function __construct( + private readonly SanitizerInterface $sanitizer, + private readonly ValidatorInterface $validator, + private readonly MapperInterface $mapper, + private readonly UserRepositoryInterface $repository, + private readonly Security $security, + ) { + } + + /** + * Delete a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function delete(array $input): Payload + { + $dto = UserInput::new($this->sanitizer, $input); + $userId = $dto->id; + + /** + * Success + */ + if ($userId > 0) { + $rowCount = $this->repository->deleteById($userId); + + if (0 !== $rowCount) { + return Payload::deleted( + [ + 'Record deleted successfully [#' . $userId . '].', + ], + ); + } + } + + /** + * 404 + */ + return Payload::notFound(); + } + + /** + * Get a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function get(array $input): Payload + { + $dto = UserInput::new($this->sanitizer, $input); + $userId = $dto->id; + + /** + * Success + */ + if ($userId > 0) { + $user = $this->repository->findById($userId); + + if (null !== $user) { + return Payload::success([$user->id => $user->toArray()]); + } + } + + /** + * 404 + */ + return Payload::notFound(); + } + + /** + * Create a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function insert(array $input): Payload + { + $dto = UserInput::new($this->sanitizer, $input); + + $validation = $this->validator->validate($dto); + if (!$validation->isValid()) { + return Payload::invalid($validation->getErrors()); + } + + /** + * Array for inserting + */ + $user = $this->mapper->db($dto); + + /** + * Pre-insert checks and manipulations + */ + $user = $this->preInsert($user); + + /** + * Insert the record + */ + try { + $userId = $this->repository->insert($user); + } catch (PDOException $ex) { + /** + * @todo send generic response and log the error + */ + return $this->getErrorPayload( + HttpCodesEnum::AppCannotCreateDatabaseRecord, + $ex->getMessage() + ); + } + + if ($userId < 1) { + return $this->getErrorPayload( + HttpCodesEnum::AppCannotCreateDatabaseRecord, + 'No id returned' + ); + } + + /** + * Get the user from the database + */ + /** @var User $domainUser */ + $domainUser = $this->repository->findById($userId); + + /** + * Return the user back + */ + return Payload::created([$domainUser->id => $domainUser->toArray()]); + } + + /** + * Create a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function update(array $input): Payload + { + $dto = UserInput::new($this->sanitizer, $input); + + $validation = $this->validator->validate($dto); + if (!$validation->isValid()) { + return Payload::invalid($validation->getErrors()); + } + + /** + * Check if the user exists, If not, return an error + */ + /** @var int $userId */ + $userId = $dto->id; + $domainUser = $this->repository->findById($userId); + + if (null === $domainUser) { + return Payload::notFound(); + } + + /** + * Array for updating + */ + $user = $this->mapper->db($dto); + + /** + * Pre-update checks and manipulations + */ + $user = $this->preUpdate($user); + + /** + * Update the record + */ + try { + $userId = $this->repository->update($userId, $user); + } catch (PDOException $ex) { + /** + * @todo send generic response and log the error + */ + return $this->getErrorPayload( + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + $ex->getMessage() + ); + } + + if ($userId < 1) { + return $this->getErrorPayload( + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + 'No id returned' + ); + } + + /** + * Get the user from the database + */ + /** @var User $domainUser */ + $domainUser = $this->repository->findById($userId); + + /** + * Return the user back + */ + return Payload::updated([$domainUser->id => $domainUser->toArray()]); + } + + /** + * @param string $message + * + * @return Payload + */ + private function getErrorPayload( + HttpCodesEnum $item, + string $message): Payload + { + return Payload::error([[$item->text() . $message]]); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDbRecordOptional + */ + private function preInsert(array $input): array + { + $result = $this->processPassword($input); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + + /** + * Set the created/updated dates if need be + */ + if (true === empty($result['usr_created_date'])) { + $result['usr_created_date'] = $now; + } + if (true === empty($result['usr_updated_date'])) { + $result['usr_updated_date'] = $now; + } + + /** @var TUserDbRecordOptional $result */ + return $this->cleanupFields($result); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDbRecordOptional + */ + private function preUpdate(array $input): array + { + $result = $this->processPassword($input); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + + /** + * Set updated date to now if it has not been set + */ + if (true === empty($result['usr_updated_date'])) { + $result['usr_updated_date'] = $now; + } + + /** + * Remove createdDate and createdUserId - cannot be changed. This + * needs to be here because we don't want to touch those fields. + */ + unset($result['usr_created_date'], $result['usr_created_usr_id']); + + + return $this->cleanupFields($result); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDomainToDbRecord + */ + private function processPassword(array $input): array + { + if (null !== $input['usr_password']) { + $plain = $input['usr_password']; + $hashed = $this->security->hash($plain); + + $input['usr_password'] = $hashed; + } + + return $input; + } + + /** + * @param TUserDbRecordOptional $row + * + * @return TUserDbRecordOptional + */ + private function cleanupFields(array $row): array + { + unset($row['usr_id']); + + return array_filter( + $row, + static fn($v) => $v !== null && $v !== '' + ); + } +} From 6abfde8038079d3d75c90b4327c3657037bdafef Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:52:49 -0600 Subject: [PATCH 181/301] [#.x] - correcting references and phpstan after refactoring --- .../Components/Middleware/ValidateTokenClaimsMiddleware.php | 1 + .../Middleware/ValidateTokenRevokedMiddleware.php | 5 +++-- .../Components/Middleware/ValidateTokenUserMiddleware.php | 6 ++++-- .../Middleware/ValidateTokenRevokedMiddlewareTest.php | 5 +++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php index bb72854..4580e87 100644 --- a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php @@ -60,6 +60,7 @@ public function call(Micro $application): bool * claims of the token, we will still check the claims against the * user stored in the session */ + /** @var array $errors */ $errors = $jwtToken->validate($tokenObject, $sessionUser); if (true !== empty($errors)) { diff --git a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php index 399adb0..cd6e782 100644 --- a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php @@ -13,11 +13,12 @@ namespace Phalcon\Api\Domain\Components\Middleware; -use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Cache\Cache; use Phalcon\Http\RequestInterface; use Phalcon\Mvc\Micro; use Phalcon\Support\Registry; @@ -47,7 +48,7 @@ public function call(Micro $application): bool * Get the token object */ $token = $this->getBearerTokenFromHeader($request, $env); - $cacheKey = $cache->getCacheTokenKey($domainUser, $token); + $cacheKey = CacheConstants::getCacheTokenKey($domainUser, $token); $exists = $cache->has($cacheKey); if (true !== $exists) { diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php index 53ba2e9..97c2fe2 100644 --- a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php @@ -15,6 +15,8 @@ use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; @@ -38,8 +40,8 @@ public function call(Micro $application): bool { /** @var JWTToken $jwtToken */ $jwtToken = $application->getSharedService(Container::JWT_TOKEN); - /** @var QueryRepository $repository */ - $repository = $application->getSharedService(Container::REPOSITORY); + /** @var UserRepository $repository */ + $repository = $application->getSharedService(Container::USER_REPOSITORY); /** @var Registry $registry */ $registry = $application->getSharedService(Container::REGISTRY); diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php b/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php index ec74105..5f2de88 100644 --- a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php +++ b/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php @@ -13,12 +13,13 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; -use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Cache\Cache; use Phalcon\Mvc\Micro; use Phalcon\Support\Registry; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -91,7 +92,7 @@ public function testValidateTokenRevokedSuccess(): void /** @var Cache $cache */ $cache = $micro->getSharedService(Container::CACHE); $sessionUser = $registry->get('user'); - $cacheKey = $cache->getCacheTokenKey($sessionUser, $token); + $cacheKey = CacheConstants::getCacheTokenKey($sessionUser, $token); $payload = [ 'token' => $token, ]; From 460db6b1a98fb92b22b9af19c76d1294c90d95ae Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:53:09 -0600 Subject: [PATCH 182/301] [#.x] - correcting references and phpstan after refactoring --- .../Services/Auth/AbstractAuthService.php | 16 +--- src/Domain/Services/Auth/LoginPostService.php | 71 +----------------- .../Services/Auth/LogoutPostService.php | 71 +----------------- .../Services/Auth/RefreshPostService.php | 73 +------------------ .../Services/User/AbstractUserService.php | 62 +--------------- .../Services/User/UserDeleteService.php | 24 +----- src/Domain/Services/User/UserGetService.php | 20 +---- src/Domain/Services/User/UserPostService.php | 54 +------------- src/Domain/Services/User/UserPutService.php | 64 +--------------- .../Services/Auth/LogoutPostServiceTest.php | 7 +- 10 files changed, 18 insertions(+), 444 deletions(-) diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Services/Auth/AbstractAuthService.php index e5ecca6..645f629 100644 --- a/src/Domain/Services/Auth/AbstractAuthService.php +++ b/src/Domain/Services/Auth/AbstractAuthService.php @@ -16,24 +16,16 @@ use Phalcon\Api\Domain\ADR\DomainInterface; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\Cache\Cache; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthFacade; +use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Env\EnvManager; -/** - * @phpstan-import-type TValidationErrors from InputTypes - */ abstract class AbstractAuthService implements DomainInterface { public function __construct( - protected readonly QueryRepository $repository, - protected readonly Cache $cache, - protected readonly EnvManager $env, - protected readonly JWTToken $jwtToken, - protected readonly SanitizerInterface $sanitizer, - protected readonly Security $security, + protected readonly AuthFacade $facade, + protected readonly ValidatorInterface $validator ) { } } diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Services/Auth/LoginPostService.php index 9e26a68..50749a3 100644 --- a/src/Domain/Services/Auth/LoginPostService.php +++ b/src/Domain/Services/Auth/LoginPostService.php @@ -14,8 +14,6 @@ namespace Phalcon\Api\Domain\Services\Auth; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** @@ -30,73 +28,6 @@ final class LoginPostService extends AbstractAuthService */ public function __invoke(array $input): Payload { - /** - * Get email and password from the input and sanitize them - */ - $inputObject = AuthInput::new($this->sanitizer, $input); - $email = $inputObject->email; - $password = $inputObject->password; - - /** - * Check if email or password are empty - */ - if (true === empty($email) || true === empty($password)) { - return Payload::unauthorized( - [HttpCodesEnum::AppIncorrectCredentials->error()] - ); - } - - /** - * Find the user in the database - */ - $domainUser = $this->repository->user()->findByEmail($email); - - /** - * Check if the user exists - */ - if (null === $domainUser) { - return Payload::unauthorized( - [HttpCodesEnum::AppIncorrectCredentials->error()] - ); - } - - /** - * Check if the password matches - */ - if (true !== $this->security->verify($password, $domainUser->password)) { - return Payload::unauthorized( - [HttpCodesEnum::AppIncorrectCredentials->error()] - ); - } - - /** - * Get a new token for this user - */ - $token = $this->jwtToken->getForUser($domainUser); - $refreshToken = $this->jwtToken->getRefreshForUser($domainUser); - - /** - * Store the token in cache - */ - $this->cache->storeTokenInCache($this->env, $domainUser, $token); - $this->cache->storeTokenInCache($this->env, $domainUser, $refreshToken); - - /** - * Send the payload back - */ - $results = [ - 'authenticated' => true, - 'user' => [ - 'id' => $domainUser->id, - 'name' => $domainUser->fullName(), - 'email' => $domainUser->email, - ], - 'jwt' => [ - 'token' => $token, - 'refreshToken' => $refreshToken, - ], - ]; - - return Payload::success($results); + return $this->facade->authenticate($input, $this->validator); } } diff --git a/src/Domain/Services/Auth/LogoutPostService.php b/src/Domain/Services/Auth/LogoutPostService.php index e217816..90ad2c1 100644 --- a/src/Domain/Services/Auth/LogoutPostService.php +++ b/src/Domain/Services/Auth/LogoutPostService.php @@ -14,14 +14,10 @@ namespace Phalcon\Api\Domain\Services\Auth; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TAuthLogoutInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes */ final class LogoutPostService extends AbstractAuthService { @@ -32,71 +28,6 @@ final class LogoutPostService extends AbstractAuthService */ public function __invoke(array $input): Payload { - /** - * @todo common code with refresh - */ - /** - * Get the token - */ - $inputObject = AuthInput::new($this->sanitizer, $input); - $token = $inputObject->token; - - /** - * Validation - * - * Empty token - */ - if (true === empty($token)) { - return Payload::unauthorized( - [HttpCodesEnum::AppTokenNotPresent->error()] - ); - } - - /** - * @todo catch any exceptions here - * - * Is this the refresh token - */ - $tokenObject = $this->jwtToken->getObject($token); - $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); - if (false === $isRefresh) { - return Payload::unauthorized( - [HttpCodesEnum::AppTokenNotValid->error()] - ); - } - - /** - * Get the user - if empty return error - */ - $domainUser = $this - ->jwtToken - ->getUser($this->repository, $tokenObject) - ; - - if (null === $domainUser) { - return Payload::unauthorized( - [HttpCodesEnum::AppTokenInvalidUser->error()] - ); - } - - /** @var TValidationErrors $errors */ - $errors = $this->jwtToken->validate($tokenObject, $domainUser); - if (true !== empty($errors)) { - return Payload::unauthorized($errors); - } - - /** - * Invalidate old tokens - */ - $this->cache->invalidateForUser($this->env, $domainUser); - - /** - * Send the payload back - */ - return Payload::success( - [ - 'authenticated' => false, - ], - ); + return $this->facade->logout($input, $this->validator); } } diff --git a/src/Domain/Services/Auth/RefreshPostService.php b/src/Domain/Services/Auth/RefreshPostService.php index 9fce9d0..d30a53f 100644 --- a/src/Domain/Services/Auth/RefreshPostService.php +++ b/src/Domain/Services/Auth/RefreshPostService.php @@ -14,14 +14,10 @@ namespace Phalcon\Api\Domain\Services\Auth; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TAuthRefreshInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes */ final class RefreshPostService extends AbstractAuthService { @@ -32,73 +28,6 @@ final class RefreshPostService extends AbstractAuthService */ public function __invoke(array $input): Payload { - /** - * Get email and password from the input and sanitize them - */ - $inputObject = AuthInput::new($this->sanitizer, $input); - $token = $inputObject->token; - - /** - * Validation - * - * Empty token - */ - if (true === empty($token)) { - return Payload::unauthorized( - [HttpCodesEnum::AppTokenNotPresent->error()] - ); - } - - /** - * @todo catch any exceptions here - * - * Is this the refresh token - */ - $tokenObject = $this->jwtToken->getObject($token); - $isRefresh = $tokenObject->getClaims()->get(JWTEnum::Refresh->value); - if (false === $isRefresh) { - return Payload::unauthorized( - [HttpCodesEnum::AppTokenNotValid->error()] - ); - } - - /** - * Get the user - if empty return error - */ - $domainUser = $this - ->jwtToken - ->getUser($this->repository, $tokenObject) - ; - if (null === $domainUser) { - return Payload::unauthorized( - [HttpCodesEnum::AppTokenInvalidUser->error()] - ); - } - - /** @var TValidationErrors $errors */ - $errors = $this->jwtToken->validate($tokenObject, $domainUser); - if (true !== empty($errors)) { - return Payload::unauthorized($errors); - } - - $newToken = $this->jwtToken->getForUser($domainUser); - $newRefreshToken = $this->jwtToken->getRefreshForUser($domainUser); - - /** - * Invalidate old tokens, store new tokens in cache - */ - $this->cache->invalidateForUser($this->env, $domainUser); - $this->cache->storeTokenInCache($this->env, $domainUser, $newToken); - $this->cache->storeTokenInCache($this->env, $domainUser, $newRefreshToken); - - /** - * Send the payload back - */ - return Payload::success( - [ - 'token' => $newToken, - 'refreshToken' => $newRefreshToken, - ], - ); + return $this->facade->refresh($input, $this->validator); } } diff --git a/src/Domain/Services/User/AbstractUserService.php b/src/Domain/Services/User/AbstractUserService.php index 6a69945..c3368c4 100644 --- a/src/Domain/Services/User/AbstractUserService.php +++ b/src/Domain/Services/User/AbstractUserService.php @@ -14,71 +14,15 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\SanitizerInterface; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\Components\DataSource\User\UserFacade; -/** - * @phpstan-import-type TUser from UserTypes - */ abstract class AbstractUserService implements DomainInterface { - protected HttpCodesEnum $errorMessage; - /** - * @param QueryRepository $repository - * @param UserMapper $mapper - * @param UserValidator $validator - * @param SanitizerInterface $sanitizer - * @param Security $security + * @param UserFacade $facade */ public function __construct( - protected readonly QueryRepository $repository, - protected readonly UserMapper $mapper, - protected readonly UserValidator $validator, - protected readonly SanitizerInterface $sanitizer, - protected readonly Security $security, + protected readonly UserFacade $facade, ) { } - - /** - * @param string $message - * - * @return Payload - */ - protected function getErrorPayload(string $message): Payload - { - return Payload::error( - [ - [$this->errorMessage->text() . $message], - ] - ); - } - - /** - * @param UserInput $inputObject - * - * @return User - */ - protected function processPassword(UserInput $inputObject): User - { - /** @var TUser $inputData */ - $inputData = $inputObject->toArray(); - - if (null !== $inputData['password']) { - $plain = $inputData['password']; - $hashed = $this->security->hash($plain); - - $inputData['password'] = $hashed; - } - - return $this->mapper->input($inputData); - } } diff --git a/src/Domain/Services/User/UserDeleteService.php b/src/Domain/Services/User/UserDeleteService.php index b380eff..27e7f00 100644 --- a/src/Domain/Services/User/UserDeleteService.php +++ b/src/Domain/Services/User/UserDeleteService.php @@ -14,7 +14,6 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\Payload; /** @@ -29,27 +28,6 @@ final class UserDeleteService extends AbstractUserService */ public function __invoke(array $input): Payload { - $inputObject = UserInput::new($this->sanitizer, $input); - $userId = $inputObject->id; - - /** - * Success - */ - if ($userId > 0) { - $rows = $this->repository->user()->deleteById($userId); - - if ($rows > 0) { - return Payload::deleted( - [ - 'Record deleted successfully [#' . $userId . '].', - ], - ); - } - } - - /** - * 404 - */ - return Payload::notFound(); + return $this->facade->delete($input); } } diff --git a/src/Domain/Services/User/UserGetService.php b/src/Domain/Services/User/UserGetService.php index 9950ec6..6fe3716 100644 --- a/src/Domain/Services/User/UserGetService.php +++ b/src/Domain/Services/User/UserGetService.php @@ -14,7 +14,6 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\Payload; /** @@ -29,23 +28,6 @@ final class UserGetService extends AbstractUserService */ public function __invoke(array $input): Payload { - $inputObject = UserInput::new($this->sanitizer, $input); - $userId = $inputObject->id; - - /** - * Success - */ - if ($userId > 0) { - $user = $this->repository->user()->findById($userId); - - if (null !== $user) { - return Payload::success([$user->id => $user->toArray()]); - } - } - - /** - * 404 - */ - return Payload::notFound(); + return $this->facade->get($input); } } diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Services/User/UserPostService.php index f29d544..ea77145 100644 --- a/src/Domain/Services/User/UserPostService.php +++ b/src/Domain/Services/User/UserPostService.php @@ -13,21 +13,14 @@ namespace Phalcon\Api\Domain\Services\User; -use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes */ final class UserPostService extends AbstractUserService { - protected HttpCodesEnum $errorMessage = HttpCodesEnum::AppCannotCreateDatabaseRecord; - /** * @param TUserInput $input * @@ -35,51 +28,6 @@ final class UserPostService extends AbstractUserService */ public function __invoke(array $input): Payload { - $inputObject = UserInput::new($this->sanitizer, $input); - /** @var TValidationErrors $errors */ - $errors = $this->validator->validate($inputObject); - - /** - * Errors exist - return early - */ - if (true !== empty($errors)) { - return Payload::invalid($errors); - } - - /** - * The password needs to be hashed - */ - $domainUser = $this->processPassword($inputObject); - - /** - * Insert the record - */ - try { - $userId = $this - ->repository - ->user() - ->insert($domainUser) - ; - } catch (PDOException $ex) { - /** - * @todo send generic response and log the error - */ - return $this->getErrorPayload($ex->getMessage()); - } - - if ($userId < 1) { - return $this->getErrorPayload('No id returned'); - } - - /** - * Get the user from the database - */ - /** @var User $domainUser */ - $domainUser = $this->repository->user()->findById($userId); - - /** - * Return the user back - */ - return Payload::created([$domainUser->id => $domainUser->toArray()]); + return $this->facade->insert($input); } } diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php index 3f189c0..ad596a8 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Services/User/UserPutService.php @@ -13,21 +13,15 @@ namespace Phalcon\Api\Domain\Services\User; -use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TUserInput from InputTypes - * @phpstan-import-type TValidationErrors from InputTypes */ final class UserPutService extends AbstractUserService { - protected HttpCodesEnum $errorMessage = HttpCodesEnum::AppCannotUpdateDatabaseRecord; - /** * @param TUserInput $input * @@ -35,62 +29,6 @@ final class UserPutService extends AbstractUserService */ public function __invoke(array $input): Payload { - $inputObject = UserInput::new($this->sanitizer, $input); - /** @var TValidationErrors $errors */ - $errors = $this->validator->validate($inputObject); - - /** - * Errors exist - return early - */ - if (true !== empty($errors)) { - return Payload::invalid($errors); - } - - /** - * Check if the user exists, If not, return an error - */ - /** @var int $userId */ - $userId = $inputObject->id; - $domainUser = $this->repository->user()->findById($userId); - - if (null === $domainUser) { - return Payload::notFound(); - } - - /** - * The password needs to be hashed - */ - $domainUser = $this->processPassword($inputObject); - - /** - * Update the record - */ - try { - $userId = $this - ->repository - ->user() - ->update($domainUser) - ; - } catch (PDOException $ex) { - /** - * @todo send generic response and log the error - */ - return $this->getErrorPayload($ex->getMessage()); - } - - if ($userId < 1) { - return $this->getErrorPayload('No id returned'); - } - - /** - * Get the user from the database - */ - /** @var User $domainUser */ - $domainUser = $this->repository->user()->findById($userId); - - /** - * Return the user back - */ - return Payload::updated([$domainUser->id => $domainUser->toArray()]); + return $this->facade->update($input); } } diff --git a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php index 57b9be8..cf2e648 100644 --- a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Encryption\JWTToken; @@ -23,6 +23,7 @@ use Phalcon\Api\Domain\Services\Auth\LogoutPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Cache\Cache; use Phalcon\Encryption\Security\JWT\Token\Item; use Phalcon\Encryption\Security\JWT\Token\Token; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -237,13 +238,13 @@ public function testServiceSuccess(): void $token = $data['jwt']['token']; $domainUser = $userMapper->domain($dbUser); - $tokenKey = $cache->getCacheTokenKey($domainUser, $token); + $tokenKey = CacheConstants::getCacheTokenKey($domainUser, $token); $actual = $cache->has($tokenKey); $this->assertTrue($actual); $refreshToken = $data['jwt']['refreshToken']; - $tokenKey = $cache->getCacheTokenKey($domainUser, $refreshToken); + $tokenKey = CacheConstants::getCacheTokenKey($domainUser, $refreshToken); $actual = $cache->has($tokenKey); $this->assertTrue($actual); From 51b037117c43bfaf77c649f42348ff4917378300 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:53:23 -0600 Subject: [PATCH 183/301] [#.x] - adjusting tests --- .../Providers/CacheDataProviderTest.php | 2 +- .../Services/User/UserServiceDispatchTest.php | 24 +++++++++- .../Services/User/UserServicePostTest.php | 32 +------------ .../Services/User/UserServicePutTest.php | 45 ++----------------- 4 files changed, 28 insertions(+), 75 deletions(-) diff --git a/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php b/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php index 041baea..a7b8887 100644 --- a/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php +++ b/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php @@ -13,9 +13,9 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; -use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Cache\Cache; final class CacheDataProviderTest extends AbstractUnitTestCase { diff --git a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php index d7fe1a6..9741943 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php @@ -13,13 +13,16 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; -use Phalcon\Api\Domain\Components\Cache\Cache; +use DateTimeImmutable; +use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\Container; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Cache\Cache; use PHPUnit\Framework\Attributes\BackupGlobals; #[BackupGlobals(true)] @@ -43,7 +46,24 @@ public function testDispatchGet(): void /** * Store the token in the cache */ - $cache->storeTokenInCache($env, $domainUser, $token); + $cacheKey = CacheConstants::getCacheTokenKey($domainUser, $token); + /** @var int $expiration */ + $expiration = $env->get( + 'TOKEN_EXPIRATION', + CacheConstants::CACHE_TOKEN_EXPIRY, + 'int' + ); + $expirationDate = (new DateTimeImmutable()) + ->modify('+' . $expiration . ' seconds') + ->format(Dates::DATE_TIME_FORMAT) + ; + + $payload = [ + 'token' => $token, + 'expiry' => $expirationDate, + ]; + + $cache->set($cacheKey, $payload, $expiration); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Services/User/UserServicePostTest.php index 231523b..006c795 100644 --- a/tests/Unit/Domain/Services/User/UserServicePostTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePostTest.php @@ -17,7 +17,6 @@ use PayloadInterop\DomainStatus; use PDOException; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Domain\Services\User\UserPostService; @@ -43,19 +42,7 @@ public function testServiceFailureNoIdReturned(): void ; $userRepository->method('insert')->willReturn(0); - $repository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $repository->method('user')->willReturn($userRepository); - - $this->container->setShared(Container::REPOSITORY, $repository); + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); @@ -104,22 +91,7 @@ public function testServiceFailurePdoError(): void ->willThrowException(new PDOException('abcde')) ; - $repository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $repository - ->method('user') - ->willReturn($userRepository) - ; - - $this->container->set(Container::REPOSITORY, $repository); + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); /** @var UserPostService $service */ $service = $this->container->get(Container::USER_POST_SERVICE); diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php index 34fab9a..0aa5de6 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -17,7 +17,6 @@ use PayloadInterop\DomainStatus; use PDOException; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Domain\Services\User\UserPutService; @@ -52,19 +51,7 @@ public function testServiceFailureNoIdReturned(): void $userRepository->method('update')->willReturn(0); $userRepository->method('findById')->willReturn($findByUser); - $repository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $repository->method('user')->willReturn($userRepository); - - $this->container->setShared(Container::REPOSITORY, $repository); + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); @@ -121,22 +108,8 @@ public function testServiceFailurePdoError(): void ->willThrowException(new PDOException('abcde')) ; - $repository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $repository - ->method('user') - ->willReturn($userRepository) - ; + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); - $this->container->set(Container::REPOSITORY, $repository); /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); @@ -187,19 +160,7 @@ public function testServiceFailureRecordNotFound(): void ; $userRepository->method('findById')->willReturn(null); - $repository = $this - ->getMockBuilder(QueryRepository::class) - ->disableOriginalConstructor() - ->onlyMethods( - [ - 'user', - ] - ) - ->getMock() - ; - $repository->method('user')->willReturn($userRepository); - - $this->container->setShared(Container::REPOSITORY, $repository); + $this->container->setShared(Container::USER_REPOSITORY, $userRepository); /** @var UserPutService $service */ $service = $this->container->get(Container::USER_PUT_SERVICE); From 02461590608ae14b5046eca346285ab3f45332d5 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:53:48 -0600 Subject: [PATCH 184/301] [#.x] - user repository cleanup and added interface --- .../DataSource/User/UserRepository.php | 85 ++----------------- .../User/UserRepositoryInterface.php | 10 ++- .../DataSource/User/UserRepositoryTest.php | 18 ++-- 3 files changed, 24 insertions(+), 89 deletions(-) diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Components/DataSource/User/UserRepository.php index 7a525f5..11c5bf9 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Components/DataSource/User/UserRepository.php @@ -13,7 +13,6 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; -use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\DataSource\AbstractRepository; use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; use Phalcon\DataMapper\Pdo\Connection; @@ -21,18 +20,10 @@ use Phalcon\DataMapper\Query\Select; use Phalcon\DataMapper\Query\Update; -use function array_filter; - /** * @phpstan-import-type TCriteria from UserTypes - * @phpstan-import-type TUserDomainToDbRecord from UserTypes - * @phpstan-import-type TUserDbRecordOptional from UserTypes * @phpstan-import-type TUserRecord from UserTypes - * - * The 'final' keyword was intentionally removed from this class to allow - * extension for testing purposes (e.g., mocking in unit tests). - * - * Please avoid extending this class in production code unless absolutely necessary. + * @phpstan-import-type TUserDbRecordOptional from UserTypes */ class UserRepository extends AbstractRepository implements UserRepositoryInterface { @@ -63,7 +54,7 @@ public function findByEmail(string $email): ?User if (true !== empty($email)) { return $this->findOneBy( [ - 'usr_email' => $email, + 'usr_email' => $email, 'usr_status_flag' => FlagsEnum::Active->value, ] ); @@ -116,33 +107,16 @@ public function findOneBy(array $criteria): ?User /** - * @param User $user + * @param TUserDbRecordOptional $user * * @return int */ - public function insert(User $user): int + public function insert(array $user): int { - $row = $this->mapper->db($user); - $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); - - /** - * @todo this should not be here - the insert should just add data not validate - */ - if (true === empty($row['usr_created_date'])) { - $row['usr_created_date'] = $now; - } - if (true === empty($row['usr_updated_date'])) { - $row['usr_updated_date'] = $now; - } - - /** - * Cleanup empty fields if needed - */ - $columns = $this->cleanupFields($row); - $insert = Insert::new($this->connection); + $insert = Insert::new($this->connection); $insert ->into($this->table) - ->columns($columns) + ->columns($user) ->perform() ; @@ -150,61 +124,20 @@ public function insert(User $user): int } /** - * @param User $user + * @param TUserDbRecordOptional $user * * @return int */ - public function update(User $user): int + public function update(int $userId, array $user): int { - $row = $this->mapper->db($user); - $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); - /** @var int $userId */ - $userId = $row['usr_id']; - - /** - * @todo this should not be here - the update should just add data not validate - */ - /** - * Set updated date to now if it has not been set - */ - if (true === empty($row['usr_updated_date'])) { - $row['usr_updated_date'] = $now; - } - - /** - * Cleanup empty fields if needed - */ - $columns = $this->cleanupFields($row); - - /** - * Remove createdDate and createdUserId - cannot be changed. This - * needs to be here because we don't want to touch those fields. - */ - unset($columns['usr_created_date'], $columns['usr_created_usr_id']); - $update = Update::new($this->connection); $update ->table($this->table) - ->columns($columns) + ->columns($user) ->where('usr_id = ', $userId) ->perform() ; return $userId; } - - /** - * @param TUserDomainToDbRecord $row - * - * @return TUserDbRecordOptional - */ - private function cleanupFields(array $row): array - { - unset($row['usr_id']); - - return array_filter( - $row, - static fn($v) => $v !== null && $v !== '' - ); - } } diff --git a/src/Domain/Components/DataSource/User/UserRepositoryInterface.php b/src/Domain/Components/DataSource/User/UserRepositoryInterface.php index 5313ae6..5afb71b 100644 --- a/src/Domain/Components/DataSource/User/UserRepositoryInterface.php +++ b/src/Domain/Components/DataSource/User/UserRepositoryInterface.php @@ -15,6 +15,7 @@ /** * @phpstan-import-type TCriteria from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes */ interface UserRepositoryInterface { @@ -54,16 +55,17 @@ public function findById(int $recordId): ?User; public function findOneBy(array $criteria): ?User; /** - * @param User $user + * @param TUserDbRecordOptional $user * * @return int */ - public function insert(User $user): int; + public function insert(array $user): int; /** - * @param User $user + * @param int $userId + * @param TUserDbRecordOptional $user * * @return int */ - public function update(User $user): int; + public function update(int $userId, array $user): int; } diff --git a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php b/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php index 569a63c..6c377bc 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php @@ -14,8 +14,8 @@ namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource\User; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; @@ -23,36 +23,36 @@ final class UserRepositoryTest extends AbstractUnitTestCase { public function testFindByEmail(): void { - /** @var QueryRepository $repository */ - $repository = $this->container->get(Container::REPOSITORY); + /** @var UserRepository $repository */ + $repository = $this->container->get(Container::USER_REPOSITORY); $migration = new UsersMigration($this->getConnection()); - $repositoryUser = $repository->user()->findByEmail(''); + $repositoryUser = $repository->findByEmail(''); $this->assertEmpty($repositoryUser); $migrationUser = $this->getNewUser($migration); $email = $migrationUser['usr_email']; - $repositoryUser = $repository->user()->findByEmail($email); + $repositoryUser = $repository->findByEmail($email); $this->runAssertions($migrationUser, $repositoryUser); } public function testFindById(): void { - /** @var QueryRepository $repository */ - $repository = $this->container->get(Container::REPOSITORY); + /** @var UserRepository $repository */ + $repository = $this->container->get(Container::USER_REPOSITORY); $migration = new UsersMigration($this->getConnection()); - $repositoryUser = $repository->user()->findById(0); + $repositoryUser = $repository->findById(0); $this->assertEmpty($repositoryUser); $migrationUser = $this->getNewUser($migration); $userId = $migrationUser['usr_id']; - $repositoryUser = $repository->user()->findById($userId); + $repositoryUser = $repository->findById($userId); $this->runAssertions($migrationUser, $repositoryUser); } From 0db320aaed3c165915b701c9d31434245d4ef312 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:54:05 -0600 Subject: [PATCH 185/301] [#.x] - new array shapes for phpstan --- src/Domain/ADR/InputTypes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Domain/ADR/InputTypes.php b/src/Domain/ADR/InputTypes.php index 14021fd..43a1471 100644 --- a/src/Domain/ADR/InputTypes.php +++ b/src/Domain/ADR/InputTypes.php @@ -50,8 +50,8 @@ * } * * @phpstan-type TRequestQuery array - * - * @phpstan-type TValidationErrors array> + * @phpstan-type TValidatorErrors array{}|array> + * @phpstan-type TInputSanitize TUserInput|TAuthInput */ final class InputTypes { From e87f2da7480cf80a2b06c74e1749f8c8483bebe0 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 11:54:13 -0600 Subject: [PATCH 186/301] [#.x] - more registrations --- src/Domain/Components/Container.php | 266 ++++++++++++++++++++++------ 1 file changed, 211 insertions(+), 55 deletions(-) diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index 0df1641..cca1e4a 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -13,14 +13,20 @@ namespace Phalcon\Api\Domain\Components; -use Phalcon\Api\Domain\Components\Cache\Cache; +use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthFacade; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthLoginValidator; use Phalcon\Api\Domain\Components\DataSource\Auth\AuthSanitizer; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthTokenValidator; +use Phalcon\Api\Domain\Components\DataSource\User\UserFacade; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Components\Encryption\TokenCache; +use Phalcon\Api\Domain\Components\Encryption\TokenManager; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; @@ -38,6 +44,7 @@ use Phalcon\Api\Domain\Services\User\UserPutService; use Phalcon\Api\Responder\JsonResponder; use Phalcon\Cache\AdapterFactory; +use Phalcon\Cache\Cache; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Di\Di; use Phalcon\Di\Service; @@ -68,6 +75,10 @@ class Container extends Di /** @var string */ public const JWT_TOKEN = 'jwt.token'; /** @var string */ + public const JWT_TOKEN_CACHE = 'jwt.token.cache'; + /** @var string */ + public const JWT_TOKEN_MANAGER = 'jwt.token.manager'; + /** @var string */ public const LOGGER = 'logger'; /** @var string */ public const REGISTRY = 'registry'; @@ -101,14 +112,32 @@ class Container extends Di public const MIDDLEWARE_VALIDATE_TOKEN_REVOKED = ValidateTokenRevokedMiddleware::class; public const MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE = ValidateTokenStructureMiddleware::class; public const MIDDLEWARE_VALIDATE_TOKEN_USER = ValidateTokenUserMiddleware::class; + + /** + * Facades + */ + public const AUTH_FACADE = 'auth.facade'; + public const USER_FACADE = 'user.facade'; + /** + * Mappers + */ + public const USER_MAPPER = UserMapper::class; + /** + * Repositories + */ + public const USER_REPOSITORY = 'user.repository'; + /** + * Sanitizers + */ + public const AUTH_SANITIZER = 'auth.sanitizer'; + public const USER_SANITIZER = 'user.sanitizer'; + /** - * Repositories/Sanitizers/Validators + * Validators */ - public const REPOSITORY = 'repository'; - public const AUTH_SANITIZER = 'auth.sanitizer'; - public const USER_MAPPER = UserMapper::class; - public const USER_SANITIZER = 'user.sanitizer'; - public const USER_VALIDATOR = UserValidator::class; + public const AUTH_LOGIN_VALIDATOR = AuthLoginValidator::class; + public const AUTH_TOKEN_VALIDATOR = 'auth.validator.token'; + public const USER_VALIDATOR = UserValidator::class; /** * Responders @@ -118,25 +147,32 @@ class Container extends Di public function __construct() { $this->services = [ - self::CACHE => $this->getServiceCache(), - self::CONNECTION => $this->getServiceConnection(), - self::ENV => $this->getServiceEnv(), - self::EVENTS_MANAGER => $this->getServiceEventsManger(), - self::FILTER => $this->getServiceFilter(), - self::JWT_TOKEN => $this->getServiceJWTToken(), - self::LOGGER => $this->getServiceLogger(), - self::REGISTRY => new Service(Registry::class, true), - self::REQUEST => new Service(Request::class, true), - self::RESPONSE => new Service(Response::class, true), - self::ROUTER => $this->getServiceRouter(), - - self::REPOSITORY => $this->getServiceRepository(), + self::CACHE => $this->getServiceCache(), + self::CONNECTION => $this->getServiceConnection(), + self::ENV => $this->getServiceEnv(), + self::EVENTS_MANAGER => $this->getServiceEventsManger(), + self::FILTER => $this->getServiceFilter(), + self::JWT_TOKEN => $this->getServiceJWTToken(), + self::JWT_TOKEN_CACHE => $this->getServiceJWTTokenCache(), + self::JWT_TOKEN_MANAGER => $this->getServiceJWTTokenManager(), + self::LOGGER => $this->getServiceLogger(), + self::REGISTRY => new Service(Registry::class, true), + self::REQUEST => new Service(Request::class, true), + self::RESPONSE => new Service(Response::class, true), + self::ROUTER => $this->getServiceRouter(), + + self::AUTH_FACADE => $this->getServiceFacadeAuth(), + self::USER_FACADE => $this->getServiceFacadeUser(), + self::USER_REPOSITORY => $this->getServiceRepositoryUser(), + self::AUTH_SANITIZER => $this->getServiceSanitizer(AuthSanitizer::class), self::USER_SANITIZER => $this->getServiceSanitizer(UserSanitizer::class), - self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthPost(LoginPostService::class), - self::AUTH_LOGOUT_POST_SERVICE => $this->getServiceAuthPost(LogoutPostService::class), - self::AUTH_REFRESH_POST_SERVICE => $this->getServiceAuthPost(RefreshPostService::class), + self::AUTH_TOKEN_VALIDATOR => $this->getServiceValidatorAuthToken(), + + self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthLoginPost(), + self::AUTH_LOGOUT_POST_SERVICE => $this->getServiceAuthTokenPost(LogoutPostService::class), + self::AUTH_REFRESH_POST_SERVICE => $this->getServiceAuthTokenPost(RefreshPostService::class), self::USER_DELETE_SERVICE => $this->getServiceUser(UserDeleteService::class), self::USER_GET_SERVICE => $this->getServiceUser(UserGetService::class), self::USER_POST_SERVICE => $this->getServiceUser(UserPostService::class), @@ -147,39 +183,45 @@ public function __construct() } /** - * @param class-string $className - * * @return Service */ - private function getServiceAuthPost(string $className): Service + private function getServiceAuthLoginPost(): Service { return new Service( [ - 'className' => $className, + 'className' => LoginPostService::class, 'arguments' => [ [ 'type' => 'service', - 'name' => self::REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::CACHE, - ], - [ - 'type' => 'service', - 'name' => self::ENV, + 'name' => self::AUTH_FACADE, ], [ 'type' => 'service', - 'name' => self::JWT_TOKEN, + 'name' => self::AUTH_LOGIN_VALIDATOR, ], + ], + ] + ); + } + + /** + * @param class-string $className + * + * @return Service + */ + private function getServiceAuthTokenPost(string $className): Service + { + return new Service( + [ + 'className' => $className, + 'arguments' => [ [ 'type' => 'service', - 'name' => self::AUTH_SANITIZER, + 'name' => self::AUTH_FACADE, ], [ 'type' => 'service', - 'name' => self::SECURITY, + 'name' => self::AUTH_TOKEN_VALIDATOR, ], ], ] @@ -201,7 +243,7 @@ function () { /** @var string $host */ $host = $env->get('CACHE_HOST', 'localhost'); /** @var int $lifetime */ - $lifetime = $env->get('CACHE_LIFETIME', Cache::CACHE_LIFETIME_DAY, 'int'); + $lifetime = $env->get('CACHE_LIFETIME', CacheConstants::CACHE_LIFETIME_DAY, 'int'); /** @var int $index */ $index = $env->get('CACHE_INDEX', 0, 'int'); /** @var int $port */ @@ -301,6 +343,70 @@ function () { ); } + /** + * @return Service + */ + private function getServiceFacadeAuth(): Service + { + return new Service( + [ + 'className' => AuthFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => self::AUTH_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => self::JWT_TOKEN_MANAGER, + ], + [ + 'type' => 'service', + 'name' => self::SECURITY, + ], + ] + ] + ); + } + + /** + * @return Service + */ + private function getServiceFacadeUser(): Service + { + return new Service( + [ + 'className' => UserFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::USER_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => self::USER_VALIDATOR, + ], + [ + 'type' => 'service', + 'name' => self::USER_MAPPER, + ], + [ + 'type' => 'service', + 'name' => self::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => self::SECURITY, + ], + ] + ] + ); + } + /** * @return Service */ @@ -332,6 +438,50 @@ private function getServiceJWTToken(): Service ); } + /** + * @return Service + */ + private function getServiceJWTTokenCache(): Service + { + return new Service( + [ + 'className' => TokenCache::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::CACHE, + ], + ], + ] + ); + } + + /** + * @return Service + */ + private function getServiceJWTTokenManager(): Service + { + return new Service( + [ + 'className' => TokenManager::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::JWT_TOKEN_CACHE, + ], + [ + 'type' => 'service', + 'name' => self::ENV, + ], + [ + 'type' => 'service', + 'name' => self::JWT_TOKEN, + ], + ], + ] + ); + } + /** * @return Service */ @@ -361,11 +511,11 @@ function () { /** * @return Service */ - private function getServiceRepository(): Service + private function getServiceRepositoryUser(): Service { return new Service( [ - 'className' => QueryRepository::class, + 'className' => UserRepository::class, 'arguments' => [ [ 'type' => 'service', @@ -375,7 +525,7 @@ private function getServiceRepository(): Service 'type' => 'service', 'name' => self::USER_MAPPER, ], - ], + ] ] ); } @@ -431,23 +581,29 @@ private function getServiceUser(string $className): Service 'arguments' => [ [ 'type' => 'service', - 'name' => self::REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::USER_MAPPER, - ], - [ - 'type' => 'service', - 'name' => self::USER_VALIDATOR, + 'name' => self::USER_FACADE, ], + ], + ] + ); + } + + /** + * @return Service + */ + private function getServiceValidatorAuthToken(): Service + { + return new Service( + [ + 'className' => AuthTokenValidator::class, + 'arguments' => [ [ 'type' => 'service', - 'name' => self::USER_SANITIZER, + 'name' => self::JWT_TOKEN_MANAGER, ], [ 'type' => 'service', - 'name' => self::SECURITY, + 'name' => self::USER_REPOSITORY, ], ], ] From ca09a44f3c979aabd6cbd5ff839776ca0c06dfd3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 12:16:59 -0600 Subject: [PATCH 187/301] [#.x] - phpcs --- src/Domain/Components/Container.php | 53 +++++++++---------- .../Components/DataSource/Auth/AuthFacade.php | 2 +- .../DataSource/Auth/AuthTokenValidator.php | 6 +-- .../Components/DataSource/User/UserFacade.php | 34 ++++++------ .../DataSource/User/UserValidator.php | 1 - .../DataSource/Validation/Result.php | 16 +++--- src/Domain/Components/Encryption/JWTToken.php | 2 +- .../Components/Encryption/TokenCache.php | 4 -- .../Components/Encryption/TokenManager.php | 7 --- .../Encryption/TokenManagerInterface.php | 18 +++---- .../ValidateTokenUserMiddleware.php | 2 - .../Services/Auth/AbstractAuthService.php | 4 -- src/Domain/Services/User/UserPutService.php | 1 - .../DataSource/User/UserMapperTest.php | 1 - 14 files changed, 62 insertions(+), 89 deletions(-) diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index cca1e4a..7ced6c9 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -92,16 +92,6 @@ class Container extends Di public const SECURITY = Security::class; /** @var string */ public const TIME = 'time'; - /** - * Services - */ - public const AUTH_LOGIN_POST_SERVICE = 'service.auth.login.post'; - public const AUTH_LOGOUT_POST_SERVICE = 'service.auth.logout.post'; - public const AUTH_REFRESH_POST_SERVICE = 'service.auth.refresh.post'; - public const USER_DELETE_SERVICE = 'service.user.delete'; - public const USER_GET_SERVICE = 'service.user.get'; - public const USER_POST_SERVICE = 'service.user.post'; - public const USER_PUT_SERVICE = 'service.user.put'; /** * Middleware */ @@ -112,16 +102,29 @@ class Container extends Di public const MIDDLEWARE_VALIDATE_TOKEN_REVOKED = ValidateTokenRevokedMiddleware::class; public const MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE = ValidateTokenStructureMiddleware::class; public const MIDDLEWARE_VALIDATE_TOKEN_USER = ValidateTokenUserMiddleware::class; - /** * Facades */ - public const AUTH_FACADE = 'auth.facade'; - public const USER_FACADE = 'user.facade'; + public const AUTH_FACADE = 'auth.facade'; + public const USER_FACADE = 'user.facade'; + /** + * Services + */ + public const AUTH_LOGIN_POST_SERVICE = 'service.auth.login.post'; + public const AUTH_LOGOUT_POST_SERVICE = 'service.auth.logout.post'; + public const AUTH_REFRESH_POST_SERVICE = 'service.auth.refresh.post'; + public const USER_DELETE_SERVICE = 'service.user.delete'; + public const USER_GET_SERVICE = 'service.user.get'; + public const USER_POST_SERVICE = 'service.user.post'; + public const USER_PUT_SERVICE = 'service.user.put'; /** * Mappers */ - public const USER_MAPPER = UserMapper::class; + public const USER_MAPPER = UserMapper::class; + /** + * Responders + */ + public const RESPONDER_JSON = JsonResponder::class; /** * Repositories */ @@ -129,20 +132,14 @@ class Container extends Di /** * Sanitizers */ - public const AUTH_SANITIZER = 'auth.sanitizer'; - public const USER_SANITIZER = 'user.sanitizer'; - + public const AUTH_SANITIZER = 'auth.sanitizer'; + public const USER_SANITIZER = 'user.sanitizer'; /** * Validators */ - public const AUTH_LOGIN_VALIDATOR = AuthLoginValidator::class; - public const AUTH_TOKEN_VALIDATOR = 'auth.validator.token'; - public const USER_VALIDATOR = UserValidator::class; - - /** - * Responders - */ - public const RESPONDER_JSON = JsonResponder::class; + public const AUTH_LOGIN_VALIDATOR = AuthLoginValidator::class; + public const AUTH_TOKEN_VALIDATOR = 'auth.validator.token'; + public const USER_VALIDATOR = UserValidator::class; public function __construct() { @@ -368,7 +365,7 @@ private function getServiceFacadeAuth(): Service 'type' => 'service', 'name' => self::SECURITY, ], - ] + ], ] ); } @@ -402,7 +399,7 @@ private function getServiceFacadeUser(): Service 'type' => 'service', 'name' => self::SECURITY, ], - ] + ], ] ); } @@ -525,7 +522,7 @@ private function getServiceRepositoryUser(): Service 'type' => 'service', 'name' => self::USER_MAPPER, ], - ] + ], ] ); } diff --git a/src/Domain/Components/DataSource/Auth/AuthFacade.php b/src/Domain/Components/DataSource/Auth/AuthFacade.php index 6460a73..60997c0 100644 --- a/src/Domain/Components/DataSource/Auth/AuthFacade.php +++ b/src/Domain/Components/DataSource/Auth/AuthFacade.php @@ -85,7 +85,7 @@ public function authenticate( /** @var string $suppliedPassword */ $suppliedPassword = $dto->password; /** @var string $dbPassword */ - $dbPassword = $domainUser->password; + $dbPassword = $domainUser->password; if (true !== $this->security->verify($suppliedPassword, $dbPassword)) { return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); } diff --git a/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php b/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php index 8b8e1d6..7cb7257 100644 --- a/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php +++ b/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php @@ -13,16 +13,12 @@ namespace Phalcon\Api\Domain\Components\DataSource\Auth; -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; use Phalcon\Api\Domain\Components\DataSource\Validation\Result; use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; -use Phalcon\Api\Domain\Components\Encryption\TokenManager; use Phalcon\Api\Domain\Components\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Payload; use Phalcon\Encryption\Security\JWT\Token\Token; final class AuthTokenValidator implements ValidatorInterface @@ -48,7 +44,7 @@ public function validate(mixed $input): Result return Result::error([HttpCodesEnum::AppTokenNotPresent->error()]); } - $token = $input->token; + $token = $input->token; $tokenObject = $this->tokenManager->getObject($token); if (null === $tokenObject) { return Result::error([HttpCodesEnum::AppTokenNotValid->error()]); diff --git a/src/Domain/Components/DataSource/User/UserFacade.php b/src/Domain/Components/DataSource/User/UserFacade.php index 1b36969..03af5c3 100644 --- a/src/Domain/Components/DataSource/User/UserFacade.php +++ b/src/Domain/Components/DataSource/User/UserFacade.php @@ -249,6 +249,21 @@ public function update(array $input): Payload return Payload::updated([$domainUser->id => $domainUser->toArray()]); } + /** + * @param TUserDbRecordOptional $row + * + * @return TUserDbRecordOptional + */ + private function cleanupFields(array $row): array + { + unset($row['usr_id']); + + return array_filter( + $row, + static fn($v) => $v !== null && $v !== '' + ); + } + /** * @param string $message * @@ -256,8 +271,8 @@ public function update(array $input): Payload */ private function getErrorPayload( HttpCodesEnum $item, - string $message): Payload - { + string $message + ): Payload { return Payload::error([[$item->text() . $message]]); } @@ -328,19 +343,4 @@ private function processPassword(array $input): array return $input; } - - /** - * @param TUserDbRecordOptional $row - * - * @return TUserDbRecordOptional - */ - private function cleanupFields(array $row): array - { - unset($row['usr_id']); - - return array_filter( - $row, - static fn($v) => $v !== null && $v !== '' - ); - } } diff --git a/src/Domain/Components/DataSource/User/UserValidator.php b/src/Domain/Components/DataSource/User/UserValidator.php index 4aae41b..3ee0bd9 100644 --- a/src/Domain/Components/DataSource/User/UserValidator.php +++ b/src/Domain/Components/DataSource/User/UserValidator.php @@ -13,7 +13,6 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; -use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Components\DataSource\Validation\Result; use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; diff --git a/src/Domain/Components/DataSource/Validation/Result.php b/src/Domain/Components/DataSource/Validation/Result.php index 7fe3223..4a81296 100644 --- a/src/Domain/Components/DataSource/Validation/Result.php +++ b/src/Domain/Components/DataSource/Validation/Result.php @@ -62,6 +62,14 @@ public function getMeta(string $key, mixed $defaultValue = null): mixed return $this->meta[$key] ?? $defaultValue; } + /** + * @return bool + */ + public function isValid(): bool + { + return $this->errors === []; + } + /** * @param string $key * @param mixed $value @@ -73,14 +81,6 @@ public function setMeta(string $key, mixed $value): void $this->meta[$key] = $value; } - /** - * @return bool - */ - public function isValid(): bool - { - return $this->errors === []; - } - /** * @return self */ diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Components/Encryption/JWTToken.php index 749b4e3..dc7be76 100644 --- a/src/Domain/Components/Encryption/JWTToken.php +++ b/src/Domain/Components/Encryption/JWTToken.php @@ -96,7 +96,7 @@ public function getRefreshForUser(User $user): string /** * @param UserRepositoryInterface $repository - * @param Token $token + * @param Token $token * * @return User|null */ diff --git a/src/Domain/Components/Encryption/TokenCache.php b/src/Domain/Components/Encryption/TokenCache.php index fc06d9b..b384f14 100644 --- a/src/Domain/Components/Encryption/TokenCache.php +++ b/src/Domain/Components/Encryption/TokenCache.php @@ -18,14 +18,10 @@ use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Cache\Adapter\Redis; use Phalcon\Cache\Cache; -use Phalcon\Encryption\Security\JWT\Token\Token; -use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\InvalidArgumentException; -use Throwable; use function str_replace; diff --git a/src/Domain/Components/Encryption/TokenManager.php b/src/Domain/Components/Encryption/TokenManager.php index e5afad7..50ef6c0 100644 --- a/src/Domain/Components/Encryption/TokenManager.php +++ b/src/Domain/Components/Encryption/TokenManager.php @@ -13,20 +13,13 @@ namespace Phalcon\Api\Domain\Components\Encryption; -use DateTimeImmutable; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Cache\Adapter\Redis; use Phalcon\Encryption\Security\JWT\Token\Token; -use Psr\SimpleCache\InvalidArgumentException; use Throwable; -use function str_replace; - /** * Small component to issue/rotate/revoke tokens and * interact with cache. diff --git a/src/Domain/Components/Encryption/TokenManagerInterface.php b/src/Domain/Components/Encryption/TokenManagerInterface.php index d3f4c8a..589dead 100644 --- a/src/Domain/Components/Encryption/TokenManagerInterface.php +++ b/src/Domain/Components/Encryption/TokenManagerInterface.php @@ -28,6 +28,15 @@ */ interface TokenManagerInterface { + /** + * Parse a token and return either null or a Token object + * + * @param string $token + * + * @return Token|null + */ + public function getObject(string $token): ?Token; + /** * Return the domain user from the database * @@ -41,15 +50,6 @@ public function getUser( Token $tokenObject ): ?User; - /** - * Parse a token and return either null or a Token object - * - * @param string $token - * - * @return Token|null - */ - public function getObject(string $token): ?Token; - /** * Issue new tokens for the user (token, refreshToken) * diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php index 97c2fe2..04503ee 100644 --- a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php @@ -14,8 +14,6 @@ namespace Phalcon\Api\Domain\Components\Middleware; use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\QueryRepository; -use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Services/Auth/AbstractAuthService.php index 645f629..ac877dd 100644 --- a/src/Domain/Services/Auth/AbstractAuthService.php +++ b/src/Domain/Services/Auth/AbstractAuthService.php @@ -14,12 +14,8 @@ namespace Phalcon\Api\Domain\Services\Auth; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Cache\Cache; use Phalcon\Api\Domain\Components\DataSource\Auth\AuthFacade; use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Env\EnvManager; abstract class AbstractAuthService implements DomainInterface { diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php index ad596a8..d1a5691 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Services/User/UserPutService.php @@ -14,7 +14,6 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Payload; /** diff --git a/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php b/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php index 5511a9c..6052b43 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php @@ -15,7 +15,6 @@ use Faker\Factory as FakerFactory; use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\DataSource\User\UserInput; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; use Phalcon\Api\Tests\AbstractUnitTestCase; From 9a6622345f52f607c9c99bd7b2e4df9fdb601bfe Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 13:10:58 -0600 Subject: [PATCH 188/301] [#.x] - adjusted the readme --- README.md | 153 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 122 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 2f77ef5..ac5552a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,41 @@ # REST API with Phalcon v6 -A REST API developed with Phalcon v6 +A REST API developed with Phalcon v6. This document explains how the project is organised, how the main components interact, and the important design decisions to keep in mind when extending the codebase. + +## Introduction + +Our goal is to build a REST API that has: +- Slim/efficient design +- Middleware +- JSON (or other) responses +- Action/Domain/Responder implementation +- JWT token authentication + +> This is not **THE** way to build a REST API with Phalcon. It is simply **A** way to do that. You can adopt this implementation if you wish, parts of it or none of it. + +This application has evolved significantly with every video release. Several areas were implemented in one way and later refactored to demonstrate the design trade-offs and how earlier choices affect the codebase. + +The main takeaways that we want to convey to developers are: +- The code has to be easy to read and understand +- Each component must do one thing and one thing only +- Components can be swapped out with others so the use of interfaces is essential +- Static analysis tools (PHPStan) must not produce errors +- Code coverage for tests must be at 100% + +Part 1 https://youtu.be/f3wP_M_NFKc +Part 2 https://youtu.be/VEZvUf_PdSY +Part 3 https://youtu.be/LP64Doh0t4g +Part 4 https://youtu.be/jCEZ2WMil8Q +Part 5 https://youtu.be/syU_3cIXFMM +Part 6 https://youtu.be/AgCbqW-leCM +Part 7 https://youtu.be/tGV4pSyVLdI +Part 8 https://youtu.be/GaJhNnw_1cE +Part 9 https://youtu.be/CWofDyTdToI + +## Directory Structure The directory structure for this projects follows the recommendations of [pds/skeleton][pds_skeleton] -# Folders The folders contain: - `bin`: empty for now, we might use it later on @@ -16,23 +47,37 @@ The folders contain: - `storage`: various storage data such as application logs - `tests`: tests -# Code organization +## High-level architecture + The application follows the [ADR pattern][adr_pattern] where the application is split into an `Action` layer, the `Domain` layer and a `Responder` layer. -## Action -The folder (under `src/`) contains the handler that is responsible for receiving data from the Domain and injecting it to the Responder so that the response can be generated. It also collects all the Input supplied by the user and injects it into the Domain for further processing. +- `Action` — receives HTTP input, collects and sanitizes request data, and calls a Domain service. +- `Domain` — contains the application logic. Implements small components, services that map to endpoints, validators, repositories and helpers. +- `Responder` — builds and emits the HTTP response from a `Payload`. + +Core files live under `src/` and are registered in the DI container in `src/Domain/Components/Container.php`. + +## Main components + +### `Action` layer -## Domain +Contains a handler that translate HTTP requests into Domain calls. For example, actions route requests to `LoginPostService`, `LogoutPostService`, `RefreshPostService` etc. -The Domain is organized in folders based on their use. The `Components` folder contains components that are essential to the operation of the application, while the `Services` folder contains classes that map to endpoints of the application +### `Domain` layer -### Container +#### `Components` + +##### `Container` The application uses the `Phalcon\Di\Di` container with minimal components lazy loaded. Each non "core" component is also registered there (i.e. domain services, responder etc.) and all necessary dependencies are injected based on the service definitions. Additionally there are two `Providers` that are also registered in the DI container for further functionality. The `ErrorHandlerProvider` which caters for the starting up/shut down of the application and error logging, and the very important `RoutesProvider` which handles registering all the routes that the application serves. -### Enums +##### `Payload` + +A uniform result object used across Domain → Responder. + +##### `Enums` There are several enumerations present in the application. Those help with common values for tasks. For example the `FlagsEnum` holds the values for the `co_users.usr_status_flag` field. We could certainly introduce a lookup table in the database for "status" and hold the values there, joining it to the `co_users` table with a lookup table. However, this will introduce an extra join in our query which will inevitable reduce performance. Since the `FlagsEnum` can keep the various statuses, we keep everything in code instead of the database. Thorough tests for enumerations ensure that if a change is made in the future, tests will fail, so that database integrity can be kept. @@ -40,9 +85,74 @@ The `RoutesEnum` holds the various routes of the application. Every route is rep Finally, the `RoutesEnum` also holds the middleware array, which defines their execution and the "hook" they will execute in (before/after). -### Middleware +##### `Env` + +The environment manager and adapters. It reads environment variables using [DotEnv][dotenv] as the main adapter but can be extended if necessary. + +##### `Validation` + +Contains the `ValidatorInterface` for all validators and a `Result` object for returning back validation results/errors. + +##### `Encryption` + +Contains components for JWT handling and passwords. The `Security` component is a wrapper for the `password_*` PHP classes, which are used for password hashing and verification. + +The `TokenManager` offers methods to issue, refresh and revoke tokens. It works in conjunction with the `TokenCache` to store or invalidate tokens stored in Cache (Redis) + +##### `DataSource`: + +The `User` / `Auth` folders contain repositories, sanitizers, mappers, validators and facades for each area. + +#### `Services`: + +Separated also in `User` and `Auth` it contains the classes that the action handler will invoke. The naming of these services shows what endpoint they are targeting and what HTTP method will invoke them. For example the `LoginPostService` will be a `POST` to the `/auth/login`. + +### `Responder` + +The `JsonResponder` responder is responsible for constructing the response with the desired output, and emitting it back to the caller. For the moment we have only implemented a JSON response with a specified array as the payload to be sent back. + +The responder receives the outcome of the Domain, by means of a `Payload` object. The object contains all the data necessary to inject in the response. -There are several middleware registered for this application and they are being executed one after another (order matters) before the action is executed. As a result, the application will stop executing if an error occurs, or if certain validations fail. +#### Response payload + +The application responds always with a specific JSON payload. The payload contains the following nodes: +- `data` - contains any data that are returned back (can be empty) +- `errors` - contains any errors occurred (can be empty) +- `meta` - array of information regarding the payload + - `code` - the application code returned + - `hash` - a `sha1` hash of the `data`, `errors` and timestamp + - `message` - `success` or `error` + - `timestamp` - the time in UTC format + + +## Request flow (example: login) + +1. Route matches and middleware runs (see Middleware section below). +2. `Action` extracts request body and calls `LoginPostService->handle($data)`. +3. `LoginPostService` calls the `AuthFacade->authenticate($input, $loginValidator)` (method injection). +4. `AuthFacade`: + - Builds DTO via `AuthInput`. + - Calls the supplied validator (`AuthLoginValidator`) which returns a `Result`. + - On success, fetches user via repository and verifies credentials (`Security`). + - Issues tokens via `TokenManager`. + - Returns a `Payload::success(...)`. +5. `Responder` builds JSON and returns HTTP response. + +## Validators + +- Specific validators exist for each potential input that needs to be validated +- Method injection is used for validators: the `AuthFacade` does not require a single validator in its constructor. Instead, callers pass the appropriate validator to each method: login uses `AuthLoginValidator`, logout/refresh use `AuthTokenValidator`. +- The validation `Result` supports `meta` data. Token validators may perform repository lookups and attach the resolved `User` to `ValidationResult->meta['user']` to avoid repeating DB queries. The facade reads that meta on success. + +## Token management and cache + +- `TokenManager` depends on a domain-specific `TokenCacheInterface` rather than a raw PSR cache. This keeps token-specific operations discoverable and testable. +- `TokenCache` enhances the Cache operations by providing token specific operations for storing and invalidating tokens. +- `TokenCacheInterface` defines token operations like `storeTokenInCache` and `invalidateForUser`. + +## Middleware sequence + +There are several middleware registered for this application and they are being executed one after another (order matters) before the action is executed. As a result, the application will stop executing if an error occurs, or if certain validations fail. Middleware returns early with a `Payload` error when validation fails. The middleware execution order is defined in the `RoutesEnum`. The available middleware is: @@ -54,13 +164,10 @@ The middleware execution order is defined in the `RoutesEnum`. The available mid - [ValidateTokenStructureMiddleware.php](src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php) - [ValidateTokenUserMiddleware.php](src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php) - - **NotFoundMiddleware** Checks if the route has been matched. If not, it will return a `Resource Not Found` payload - **HealthMiddleware** Invoked when the `/health` endpoint is called and returns a `OK` payload @@ -86,22 +193,6 @@ Checks all the claims of the JWT token to ensure that it validates. For instance Checks if the token has been revoked. If it has, an error is returned -## Responder -The responder is responsible for constructing the response with the desired output, and emitting it back to the caller. For the moment we have only implemented a JSON response with a specified array as the payload to be sent back. - -The responder receives the outcome of the Domain, by means of a `Payload` object. The object contains all the data necessary to inject in the response. - -### Response payload - -The application responds always with a specific JSON payload. The payload contains the following nodes: -- `data` - contains any data that are returned back (can be empty) -- `errors` - contains any errors occurred (can be empty) -- `meta` - array of information regarding the payload - - `code` - the application code returned - - `hash` - a `sha1` hash of the `data`, `errors` and timestamp - - `message` - `success` or `error` - - `timestamp` - the time in UTC format - - [adr_pattern]: https://github.com/pmjones/adr [pds_skeleton]: https://github.com/php-pds/skeleton +[dotenv]: https://github.com/vlucas/phpdotenv From 37e2e822c711f411bd2ec2bfc5f9a3645fdcd254 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:03:16 -0600 Subject: [PATCH 189/301] [#.x] - added validator enums for different inputs --- .../Validation/AbstractValidator.php | 63 +++++++++++++++++++ .../Enums/Input/AuthLoginInputEnum.php | 34 ++++++++++ .../Enums/Input/AuthTokenInputEnum.php | 26 ++++++++ .../Enums/Input/UserInputInsertEnum.php | 37 +++++++++++ .../Enums/Input/UserInputUpdateEnum.php | 43 +++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 src/Domain/Components/DataSource/Validation/AbstractValidator.php create mode 100644 src/Domain/Components/Enums/Input/AuthLoginInputEnum.php create mode 100644 src/Domain/Components/Enums/Input/AuthTokenInputEnum.php create mode 100644 src/Domain/Components/Enums/Input/UserInputInsertEnum.php create mode 100644 src/Domain/Components/Enums/Input/UserInputUpdateEnum.php diff --git a/src/Domain/Components/DataSource/Validation/AbstractValidator.php b/src/Domain/Components/DataSource/Validation/AbstractValidator.php new file mode 100644 index 0000000..f433cfa --- /dev/null +++ b/src/Domain/Components/DataSource/Validation/AbstractValidator.php @@ -0,0 +1,63 @@ + + * + * 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\Components\DataSource\Validation; + +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; +use Phalcon\Api\Domain\Components\Enums\Input\ValidatorEnumInterface; +use Phalcon\Filter\Validation\ValidationInterface; +use Phalcon\Filter\Validation\ValidatorInterface as PhalconValidator; +use UnitEnum; + +abstract class AbstractValidator implements ValidatorInterface +{ + protected string $fields = ''; + + public function __construct( + private ValidationInterface $validation + ) { + } + + /** + * @param AuthInput $input + * + * @return list> + */ + protected function runValidations(mixed $input): array + { + $enum = $this->fields; + /** @var ValidatorEnumInterface[] $elements */ + $elements = $enum::cases(); + + /** @var ValidatorEnumInterface $element */ + foreach ($elements as $element) { + $validators = $element->validators(); + foreach ($validators as $validator) { + /** @var PhalconValidator $validatorObject */ + $validatorObject = new $validator(); + $this->validation->add($element->name, $validatorObject); + } + } + + $this->validation->validate($input); + $messages = $this->validation->getMessages(); + + $results = []; + foreach ($messages as $message) { + $results[] = [$message->getMessage()]; + } + + + return $results; + } +} diff --git a/src/Domain/Components/Enums/Input/AuthLoginInputEnum.php b/src/Domain/Components/Enums/Input/AuthLoginInputEnum.php new file mode 100644 index 0000000..560c39f --- /dev/null +++ b/src/Domain/Components/Enums/Input/AuthLoginInputEnum.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\Components\Enums\Input; + +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum AuthLoginInputEnum implements ValidatorEnumInterface +{ + case email; + case password; + + public function validators(): array + { + return match ($this) { + self::email => [ + PresenceOf::class, + Email::class + ], + self::password => [PresenceOf::class], + }; + } +} diff --git a/src/Domain/Components/Enums/Input/AuthTokenInputEnum.php b/src/Domain/Components/Enums/Input/AuthTokenInputEnum.php new file mode 100644 index 0000000..af4d9bf --- /dev/null +++ b/src/Domain/Components/Enums/Input/AuthTokenInputEnum.php @@ -0,0 +1,26 @@ + + * + * 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\Components\Enums\Input; + +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum AuthTokenInputEnum implements ValidatorEnumInterface +{ + case token; + + public function validators(): array + { + return [PresenceOf::class]; + } +} diff --git a/src/Domain/Components/Enums/Input/UserInputInsertEnum.php b/src/Domain/Components/Enums/Input/UserInputInsertEnum.php new file mode 100644 index 0000000..69b35a3 --- /dev/null +++ b/src/Domain/Components/Enums/Input/UserInputInsertEnum.php @@ -0,0 +1,37 @@ + + * + * 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\Components\Enums\Input; + +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum UserInputInsertEnum implements ValidatorEnumInterface +{ + case email; + case password; + case issuer; + case tokenPassword; + case tokenId; + + public function validators(): array + { + return match ($this) { + self::email => [ + PresenceOf::class, + Email::class + ], + default => [PresenceOf::class], + }; + } +} diff --git a/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php b/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php new file mode 100644 index 0000000..11c7132 --- /dev/null +++ b/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php @@ -0,0 +1,43 @@ + + * + * 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\Components\Enums\Input; + +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\Numericality; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum UserInputUpdateEnum implements ValidatorEnumInterface +{ + case id; + case email; + case password; + case issuer; + case tokenPassword; + case tokenId; + + public function validators(): array + { + return match ($this) { + self::id => [ + PresenceOf::class, + Numericality::class + ], + self::email => [ + PresenceOf::class, + Email::class + ], + default => [PresenceOf::class], + }; + } +} From db39e933ce771eb1cf09bd9f1fea242d69b55f7c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:03:27 -0600 Subject: [PATCH 190/301] [#.x] - added validator enum interface --- .../Enums/Input/ValidatorEnumInterface.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Domain/Components/Enums/Input/ValidatorEnumInterface.php diff --git a/src/Domain/Components/Enums/Input/ValidatorEnumInterface.php b/src/Domain/Components/Enums/Input/ValidatorEnumInterface.php new file mode 100644 index 0000000..41633d5 --- /dev/null +++ b/src/Domain/Components/Enums/Input/ValidatorEnumInterface.php @@ -0,0 +1,24 @@ + + * + * 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\Components\Enums\Input; + +use UnitEnum; + +interface ValidatorEnumInterface extends UnitEnum +{ + /** + * @return array + */ + public function validators(): array; +} From 133cb8573d7d48b67dad055ddee9c9746183dd5c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:03:40 -0600 Subject: [PATCH 191/301] [#.x] - setting up cache folder for phpstan --- phpstan.neon | 1 + 1 file changed, 1 insertion(+) diff --git a/phpstan.neon b/phpstan.neon index 776ccd8..92dcb57 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,4 @@ parameters: level: max paths: - src + tmpDir: tests/_output/ From d444bfb282d96f8408ce9b13fa57af9c4f860b03 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:04:05 -0600 Subject: [PATCH 192/301] [#.x] - separated validators for user insert and update --- .../DataSource/User/UserValidator.php | 39 ++------------- .../DataSource/User/UserValidatorTrait.php | 47 +++++++++++++++++++ .../DataSource/User/UserValidatorUpdate.php | 24 ++++++++++ 3 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 src/Domain/Components/DataSource/User/UserValidatorTrait.php create mode 100644 src/Domain/Components/DataSource/User/UserValidatorUpdate.php diff --git a/src/Domain/Components/DataSource/User/UserValidator.php b/src/Domain/Components/DataSource/User/UserValidator.php index 3ee0bd9..331b1b7 100644 --- a/src/Domain/Components/DataSource/User/UserValidator.php +++ b/src/Domain/Components/DataSource/User/UserValidator.php @@ -13,41 +13,12 @@ namespace Phalcon\Api\Domain\Components\DataSource\User; -use Phalcon\Api\Domain\Components\DataSource\Validation\Result; -use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Components\Enums\Input\UserInputInsertEnum; -final class UserValidator implements ValidatorInterface +final class UserValidator extends AbstractValidator { - /** - * Validate a UserInput and return an array of errors. - * Empty array means valid. - * - * @param UserInput $input - * - * @return Result - */ - public function validate(mixed $input): Result - { - $errors = []; - $required = [ - 'email', - 'password', - 'issuer', - 'tokenPassword', - 'tokenId', - ]; + use UserValidatorTrait; - foreach ($required as $name) { - $value = $input->$name; - if (true === empty($value)) { - $errors[] = ['Field ' . $name . ' cannot be empty.']; - } - } - - /** - * @todo add validators - */ - - return new Result($errors); - } + protected string $fields = UserInputInsertEnum::class; } diff --git a/src/Domain/Components/DataSource/User/UserValidatorTrait.php b/src/Domain/Components/DataSource/User/UserValidatorTrait.php new file mode 100644 index 0000000..1bae490 --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserValidatorTrait.php @@ -0,0 +1,47 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; +use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Components\DataSource\Validation\Result; +use Phalcon\Api\Domain\Components\Enums\Input\UserInputInsertEnum; + +trait UserValidatorTrait +{ + /** + * Validate a AuthInput and return an array of errors. + * Empty array means valid. + * + * @param AuthInput $input + * + * @return Result + */ + public function validate(mixed $input): Result + { + $errors = $this->runValidations($input); + if (true !== empty($errors)) { + return Result::error($errors); + } + + return Result::success(); + } + + /** + * @param AuthInput $input + * + * @return array + */ + abstract protected function runValidations(mixed $input): array; +} diff --git a/src/Domain/Components/DataSource/User/UserValidatorUpdate.php b/src/Domain/Components/DataSource/User/UserValidatorUpdate.php new file mode 100644 index 0000000..2f1d0a0 --- /dev/null +++ b/src/Domain/Components/DataSource/User/UserValidatorUpdate.php @@ -0,0 +1,24 @@ + + * + * 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\Components\DataSource\User; + +use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Components\Enums\Input\UserInputUpdateEnum; + +final class UserValidatorUpdate extends AbstractValidator +{ + use UserValidatorTrait; + + protected string $fields = UserInputUpdateEnum::class; +} From a12f37892b507f67e21184c2746a89254e2dc2f2 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:04:35 -0600 Subject: [PATCH 193/301] [#.x] - refactored validators for auth to use the new format --- .../DataSource/Auth/AuthLoginValidator.php | 11 +++++++---- .../DataSource/Auth/AuthTokenValidator.php | 15 +++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php b/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php index c2a4d78..d6bf761 100644 --- a/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php +++ b/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php @@ -13,12 +13,15 @@ namespace Phalcon\Api\Domain\Components\DataSource\Auth; +use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; use Phalcon\Api\Domain\Components\DataSource\Validation\Result; -use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Enums\Input\AuthLoginInputEnum; -final class AuthLoginValidator implements ValidatorInterface +final class AuthLoginValidator extends AbstractValidator { + protected string $fields = AuthLoginInputEnum::class; + /** * Validate a AuthInput and return an array of errors. * Empty array means valid. @@ -29,8 +32,8 @@ final class AuthLoginValidator implements ValidatorInterface */ public function validate(mixed $input): Result { - /** @var AuthInput $input */ - if (true === empty($input->email) || true === empty($input->password)) { + $errors = $this->runValidations($input); + if (true !== empty($errors)) { return Result::error( [HttpCodesEnum::AppIncorrectCredentials->error()] ); diff --git a/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php b/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php index 7cb7257..4425a35 100644 --- a/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php +++ b/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php @@ -14,19 +14,25 @@ namespace Phalcon\Api\Domain\Components\DataSource\Auth; use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; +use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; use Phalcon\Api\Domain\Components\DataSource\Validation\Result; -use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; use Phalcon\Api\Domain\Components\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Components\Enums\Input\AuthTokenInputEnum; use Phalcon\Encryption\Security\JWT\Token\Token; +use Phalcon\Filter\Validation\ValidationInterface; -final class AuthTokenValidator implements ValidatorInterface +final class AuthTokenValidator extends AbstractValidator { + protected string $fields = AuthTokenInputEnum::class; + public function __construct( private TokenManagerInterface $tokenManager, private UserRepositoryInterface $userRepository, + ValidationInterface $validator, ) { + parent::__construct($validator); } /** @@ -39,11 +45,12 @@ public function __construct( */ public function validate(mixed $input): Result { - /** @var AuthInput $input */ - if (true === empty($input->token)) { + $errors = $this->runValidations($input); + if (true !== empty($errors)) { return Result::error([HttpCodesEnum::AppTokenNotPresent->error()]); } + /** @var string $token */ $token = $input->token; $tokenObject = $this->tokenManager->getObject($token); if (null === $tokenObject) { From 34a894fd605d2b7fa1df6f787917a0ef4c4996a7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:04:47 -0600 Subject: [PATCH 194/301] [#.x] - adjusted tests --- .../DataSource/User/UserValidatorTest.php | 20 ++++++++++++------- .../Services/User/UserServicePostTest.php | 11 +++++----- .../Services/User/UserServicePutTest.php | 11 +++++----- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php b/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php index f1ea417..0019146 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php +++ b/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php @@ -19,6 +19,7 @@ use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\ValidationInterface; final class UserValidatorTest extends AbstractUnitTestCase { @@ -26,20 +27,23 @@ public function testError(): void { /** @var UserSanitizer $sanitizer */ $sanitizer = $this->container->get(Container::USER_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); $input = []; $userInput = UserInput::new($sanitizer, $input); - $validator = new UserValidator(); + $validator = new UserValidator($validation); $result = $validator->validate($userInput); $actual = $result->getErrors(); $expected = [ - ['Field email cannot be empty.'], - ['Field password cannot be empty.'], - ['Field issuer cannot be empty.'], - ['Field tokenPassword cannot be empty.'], - ['Field tokenId cannot be empty.'], + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], ]; $this->assertSame($expected, $actual); @@ -49,6 +53,8 @@ public function testSuccess(): void { /** @var UserSanitizer $sanitizer */ $sanitizer = $this->container->get(Container::USER_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); $faker = FakerFactory::create(); $input = [ @@ -61,7 +67,7 @@ public function testSuccess(): void $userInput = UserInput::new($sanitizer, $input); - $validator = new UserValidator(); + $validator = new UserValidator($validation); $result = $validator->validate($userInput); $actual = $result->getErrors(); diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Services/User/UserServicePostTest.php index 006c795..b5dd795 100644 --- a/tests/Unit/Domain/Services/User/UserServicePostTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePostTest.php @@ -158,11 +158,12 @@ public function testServiceFailureValidation(): void $errors = $actual['errors']; $expected = [ - ['Field email cannot be empty.'], - ['Field password cannot be empty.'], - ['Field issuer cannot be empty.'], - ['Field tokenPassword cannot be empty.'], - ['Field tokenId cannot be empty.'], + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], ]; $actual = $errors; $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php index 0aa5de6..e24300c 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -224,11 +224,12 @@ public function testServiceFailureValidation(): void $errors = $actual['errors']; $expected = [ - ['Field email cannot be empty.'], - ['Field password cannot be empty.'], - ['Field issuer cannot be empty.'], - ['Field tokenPassword cannot be empty.'], - ['Field tokenId cannot be empty.'], + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], ]; $actual = $errors; $this->assertSame($expected, $actual); From fe8341fed11cfb566a5f1036ec3d3e72ffa56598 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:05:08 -0600 Subject: [PATCH 195/301] [#.x] - new registrations --- src/Domain/Components/Container.php | 106 +++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index 7ced6c9..12a16ad 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -23,6 +23,7 @@ use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; +use Phalcon\Api\Domain\Components\DataSource\User\UserValidatorUpdate; use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Encryption\TokenCache; @@ -50,6 +51,7 @@ use Phalcon\Di\Service; use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\FilterFactory; +use Phalcon\Filter\Validation; use Phalcon\Http\Request; use Phalcon\Http\Response; use Phalcon\Logger\Adapter\Stream; @@ -92,6 +94,8 @@ class Container extends Di public const SECURITY = Security::class; /** @var string */ public const TIME = 'time'; + /** @var string */ + public const VALIDATION = Validation::class; /** * Middleware */ @@ -105,8 +109,9 @@ class Container extends Di /** * Facades */ - public const AUTH_FACADE = 'auth.facade'; - public const USER_FACADE = 'user.facade'; + public const AUTH_FACADE = 'auth.facade'; + public const USER_FACADE = 'user.facade'; + public const USER_FACADE_UPDATE = 'user.facade.update'; /** * Services */ @@ -137,9 +142,10 @@ class Container extends Di /** * Validators */ - public const AUTH_LOGIN_VALIDATOR = AuthLoginValidator::class; - public const AUTH_TOKEN_VALIDATOR = 'auth.validator.token'; - public const USER_VALIDATOR = UserValidator::class; + public const AUTH_LOGIN_VALIDATOR = 'auth.validator.login'; + public const AUTH_TOKEN_VALIDATOR = 'auth.validator.token'; + public const USER_VALIDATOR = 'user.validator.insert'; + public const USER_VALIDATOR_UPDATE = 'user.validator.update'; public function __construct() { @@ -158,14 +164,18 @@ public function __construct() self::RESPONSE => new Service(Response::class, true), self::ROUTER => $this->getServiceRouter(), - self::AUTH_FACADE => $this->getServiceFacadeAuth(), - self::USER_FACADE => $this->getServiceFacadeUser(), - self::USER_REPOSITORY => $this->getServiceRepositoryUser(), + self::AUTH_FACADE => $this->getServiceFacadeAuth(), + self::USER_FACADE => $this->getServiceFacadeUser(), + self::USER_FACADE_UPDATE => $this->getServiceFacadeUserUpdate(), + self::USER_REPOSITORY => $this->getServiceRepositoryUser(), self::AUTH_SANITIZER => $this->getServiceSanitizer(AuthSanitizer::class), self::USER_SANITIZER => $this->getServiceSanitizer(UserSanitizer::class), - self::AUTH_TOKEN_VALIDATOR => $this->getServiceValidatorAuthToken(), + self::AUTH_LOGIN_VALIDATOR => $this->getServiceValidator(AuthLoginValidator::class), + self::AUTH_TOKEN_VALIDATOR => $this->getServiceValidatorAuthToken(), + self::USER_VALIDATOR => $this->getServiceValidator(UserValidator::class), + self::USER_VALIDATOR_UPDATE => $this->getServiceValidator(UserValidatorUpdate::class), self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthLoginPost(), self::AUTH_LOGOUT_POST_SERVICE => $this->getServiceAuthTokenPost(LogoutPostService::class), @@ -173,7 +183,7 @@ public function __construct() self::USER_DELETE_SERVICE => $this->getServiceUser(UserDeleteService::class), self::USER_GET_SERVICE => $this->getServiceUser(UserGetService::class), self::USER_POST_SERVICE => $this->getServiceUser(UserPostService::class), - self::USER_PUT_SERVICE => $this->getServiceUser(UserPutService::class), + self::USER_PUT_SERVICE => $this->getServiceUserUpdate(), ]; parent::__construct(); @@ -404,6 +414,40 @@ private function getServiceFacadeUser(): Service ); } + /** + * @return Service + */ + private function getServiceFacadeUserUpdate(): Service + { + return new Service( + [ + 'className' => UserFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::USER_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => self::USER_VALIDATOR_UPDATE, + ], + [ + 'type' => 'service', + 'name' => self::USER_MAPPER, + ], + [ + 'type' => 'service', + 'name' => self::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => self::SECURITY, + ], + ], + ] + ); + } + /** * @return Service */ @@ -585,6 +629,44 @@ private function getServiceUser(string $className): Service ); } + /** + * @return Service + */ + private function getServiceUserUpdate(): Service + { + return new Service( + [ + 'className' => UserPutService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::USER_FACADE_UPDATE, + ], + ], + ] + ); + } + + /** + * @param class-string $className + * + * @return Service + */ + private function getServiceValidator(string $className): Service + { + return new Service( + [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => self::VALIDATION, + ], + ], + ] + ); + } + /** * @return Service */ @@ -602,6 +684,10 @@ private function getServiceValidatorAuthToken(): Service 'type' => 'service', 'name' => self::USER_REPOSITORY, ], + [ + 'type' => 'service', + 'name' => self::VALIDATION, + ], ], ] ); From 302364962d22509f1e0267c730a1d54e32a92869 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:23:55 -0600 Subject: [PATCH 196/301] [#.x] - new absint validator --- .../DataSource/Validation/AbsInt.php | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/Domain/Components/DataSource/Validation/AbsInt.php diff --git a/src/Domain/Components/DataSource/Validation/AbsInt.php b/src/Domain/Components/DataSource/Validation/AbsInt.php new file mode 100644 index 0000000..0618698 --- /dev/null +++ b/src/Domain/Components/DataSource/Validation/AbsInt.php @@ -0,0 +1,61 @@ + + * + * 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\Components\DataSource\Validation; + +use Phalcon\Filter\Validation; +use Phalcon\Filter\Validation\AbstractValidator; + +use Phalcon\Filter\Validation\Validator\Numericality; + +use function preg_match; + +class AbsInt extends Numericality +{ + /** + * @var string|null + */ + protected string | null $template = "Field :field is not a valid absolute integer and greater than 0"; + + /** + * Executes the validation + * + * @param Validation $validation + * @param string $field + * + * @return bool + * @throws Validation\Exception + */ + public function validate(Validation $validation, string $field): bool + { + $result = parent::validate($validation, $field); + + if (false === $result) { + return false; + } + + // Dump spaces in the string if we have any + $value = $validation->getValue($field); + $value = abs((int)$value); + + if ($value <= 0) { + $validation->appendMessage( + $this->messageFactory($validation, $field) + ); + + return false; + } + + return true; + } +} From 64e96cba2a2bd03f6e62d6b7c0b2b7b671c82078 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:24:13 -0600 Subject: [PATCH 197/301] [#.x] - changed user put to use the absint validator --- .../Components/DataSource/Validation/AbstractValidator.php | 1 + src/Domain/Components/Enums/Input/UserInputUpdateEnum.php | 4 ++-- tests/Unit/Domain/Services/User/UserServicePutTest.php | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Domain/Components/DataSource/Validation/AbstractValidator.php b/src/Domain/Components/DataSource/Validation/AbstractValidator.php index f433cfa..dda1b32 100644 --- a/src/Domain/Components/DataSource/Validation/AbstractValidator.php +++ b/src/Domain/Components/DataSource/Validation/AbstractValidator.php @@ -35,6 +35,7 @@ public function __construct( */ protected function runValidations(mixed $input): array { + /** @var UnitEnum $enum */ $enum = $this->fields; /** @var ValidatorEnumInterface[] $elements */ $elements = $enum::cases(); diff --git a/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php b/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php index 11c7132..3d09aeb 100644 --- a/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php +++ b/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php @@ -13,8 +13,8 @@ namespace Phalcon\Api\Domain\Components\Enums\Input; +use Phalcon\Api\Domain\Components\DataSource\Validation\AbsInt; use Phalcon\Filter\Validation\Validator\Email; -use Phalcon\Filter\Validation\Validator\Numericality; use Phalcon\Filter\Validation\Validator\PresenceOf; enum UserInputUpdateEnum implements ValidatorEnumInterface @@ -31,7 +31,7 @@ public function validators(): array return match ($this) { self::id => [ PresenceOf::class, - Numericality::class + AbsInt::class ], self::email => [ PresenceOf::class, diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php index e24300c..3513747 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -199,6 +199,7 @@ public function testServiceFailureValidation(): void $userData = $this->getNewUserData(); unset( + $userData['usr_id'], $userData['usr_email'], $userData['usr_password'], $userData['usr_issuer'], @@ -224,6 +225,7 @@ public function testServiceFailureValidation(): void $errors = $actual['errors']; $expected = [ + ['Field id is not a valid absolute integer and greater than 0'], ['Field email is required'], ['Field email must be an email address'], ['Field password is required'], From 51e7ba41e02b8a796b17de8f2e501554f28e12e9 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 17:25:39 -0600 Subject: [PATCH 198/301] [#.x] - phpstan corrections --- src/Domain/Components/DataSource/Validation/AbsInt.php | 2 +- .../Components/DataSource/Validation/AbstractValidator.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Domain/Components/DataSource/Validation/AbsInt.php b/src/Domain/Components/DataSource/Validation/AbsInt.php index 0618698..4bfbb1a 100644 --- a/src/Domain/Components/DataSource/Validation/AbsInt.php +++ b/src/Domain/Components/DataSource/Validation/AbsInt.php @@ -44,7 +44,7 @@ public function validate(Validation $validation, string $field): bool return false; } - // Dump spaces in the string if we have any + /** @var string $value */ $value = $validation->getValue($field); $value = abs((int)$value); diff --git a/src/Domain/Components/DataSource/Validation/AbstractValidator.php b/src/Domain/Components/DataSource/Validation/AbstractValidator.php index dda1b32..bd94e0d 100644 --- a/src/Domain/Components/DataSource/Validation/AbstractValidator.php +++ b/src/Domain/Components/DataSource/Validation/AbstractValidator.php @@ -17,7 +17,6 @@ use Phalcon\Api\Domain\Components\Enums\Input\ValidatorEnumInterface; use Phalcon\Filter\Validation\ValidationInterface; use Phalcon\Filter\Validation\ValidatorInterface as PhalconValidator; -use UnitEnum; abstract class AbstractValidator implements ValidatorInterface { @@ -35,7 +34,6 @@ public function __construct( */ protected function runValidations(mixed $input): array { - /** @var UnitEnum $enum */ $enum = $this->fields; /** @var ValidatorEnumInterface[] $elements */ $elements = $enum::cases(); From bc3fc9a4a43820381762df9e262c62aaca3409c0 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 8 Nov 2025 21:44:40 -0600 Subject: [PATCH 199/301] [#.x] - moved Payload in ADR --- src/Domain/ADR/DomainInterface.php | 2 -- src/Domain/{Components => ADR}/Payload.php | 2 +- src/Domain/Components/DataSource/Auth/AuthFacade.php | 2 +- src/Domain/Components/DataSource/User/UserFacade.php | 2 +- src/Domain/Components/Encryption/TokenCache.php | 7 ++++++- .../Middleware/ValidateTokenRevokedMiddleware.php | 4 ++-- src/Domain/Services/Auth/LoginPostService.php | 2 +- src/Domain/Services/Auth/LogoutPostService.php | 2 +- src/Domain/Services/Auth/RefreshPostService.php | 2 +- src/Domain/Services/User/UserDeleteService.php | 2 +- src/Domain/Services/User/UserGetService.php | 2 +- src/Domain/Services/User/UserPostService.php | 2 +- src/Domain/Services/User/UserPutService.php | 2 +- tests/Fixtures/Domain/Services/ServiceFixture.php | 2 +- 14 files changed, 19 insertions(+), 16 deletions(-) rename src/Domain/{Components => ADR}/Payload.php (98%) diff --git a/src/Domain/ADR/DomainInterface.php b/src/Domain/ADR/DomainInterface.php index 346a756..d6c1d2b 100644 --- a/src/Domain/ADR/DomainInterface.php +++ b/src/Domain/ADR/DomainInterface.php @@ -13,8 +13,6 @@ namespace Phalcon\Api\Domain\ADR; -use Phalcon\Api\Domain\Components\Payload; - /** * @phpstan-import-type TAuthLoginInput from InputTypes * @phpstan-import-type TUserInput from InputTypes diff --git a/src/Domain/Components/Payload.php b/src/Domain/ADR/Payload.php similarity index 98% rename from src/Domain/Components/Payload.php rename to src/Domain/ADR/Payload.php index 860a218..7f1d7b7 100644 --- a/src/Domain/Components/Payload.php +++ b/src/Domain/ADR/Payload.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components; +namespace Phalcon\Api\Domain\ADR; use PayloadInterop\DomainStatus; use Phalcon\Api\Responder\ResponderTypes; diff --git a/src/Domain/Components/DataSource/Auth/AuthFacade.php b/src/Domain/Components/DataSource/Auth/AuthFacade.php index 60997c0..35007b1 100644 --- a/src/Domain/Components/DataSource/Auth/AuthFacade.php +++ b/src/Domain/Components/DataSource/Auth/AuthFacade.php @@ -14,6 +14,7 @@ namespace Phalcon\Api\Domain\Components\DataSource\Auth; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; @@ -21,7 +22,6 @@ use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Payload; /** * @phpstan-import-type TAuthLoginInput from InputTypes diff --git a/src/Domain/Components/DataSource/User/UserFacade.php b/src/Domain/Components/DataSource/User/UserFacade.php index 03af5c3..b372a11 100644 --- a/src/Domain/Components/DataSource/User/UserFacade.php +++ b/src/Domain/Components/DataSource/User/UserFacade.php @@ -15,13 +15,13 @@ use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Api\Domain\Components\Constants\Dates; use Phalcon\Api\Domain\Components\DataSource\Interfaces\MapperInterface; use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; use Phalcon\Api\Domain\Components\Encryption\Security; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Payload; use function array_filter; diff --git a/src/Domain/Components/Encryption/TokenCache.php b/src/Domain/Components/Encryption/TokenCache.php index b384f14..cef096e 100644 --- a/src/Domain/Components/Encryption/TokenCache.php +++ b/src/Domain/Components/Encryption/TokenCache.php @@ -59,7 +59,12 @@ public function invalidateForUser( * keys will come back with the prefix defined in the adapter. In order * to delete them, we need to remove the prefix because `delete()` will * automatically prepend each key with it. + * + * NOTE: This code will work with other adapters also, since + * `getKeys()` returns the keys of the storage adapter. This method + * exists in the Cache/Storage AdapterInterface */ + /** @var Redis $redis */ $redis = $this->cache->getAdapter(); $pattern = CacheConstants::getCacheTokenKey($domainUser, ''); @@ -69,7 +74,7 @@ public function invalidateForUser( $newKeys = []; /** @var string $key */ foreach ($keys as $key) { - $newKeys[] = str_replace($prefix, '', $key); + $newKeys[] = substr($key, strlen($prefix)); } return $this->cache->deleteMultiple($newKeys); diff --git a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php index cd6e782..780d22c 100644 --- a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php +++ b/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php @@ -18,10 +18,10 @@ use Phalcon\Api\Domain\Components\DataSource\User\User; use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Cache\Cache; use Phalcon\Http\RequestInterface; use Phalcon\Mvc\Micro; use Phalcon\Support\Registry; +use Psr\SimpleCache\CacheInterface; final class ValidateTokenRevokedMiddleware extends AbstractMiddleware { @@ -34,7 +34,7 @@ public function call(Micro $application): bool { /** @var RequestInterface $request */ $request = $application->getSharedService(Container::REQUEST); - /** @var Cache $cache */ + /** @var CacheInterface $cache */ $cache = $application->getSharedService(Container::CACHE); /** @var EnvManager $env */ $env = $application->getSharedService(Container::ENV); diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Services/Auth/LoginPostService.php index 50749a3..34b29ec 100644 --- a/src/Domain/Services/Auth/LoginPostService.php +++ b/src/Domain/Services/Auth/LoginPostService.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Services\Auth; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TAuthLoginInput from InputTypes diff --git a/src/Domain/Services/Auth/LogoutPostService.php b/src/Domain/Services/Auth/LogoutPostService.php index 90ad2c1..2ac289b 100644 --- a/src/Domain/Services/Auth/LogoutPostService.php +++ b/src/Domain/Services/Auth/LogoutPostService.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Services\Auth; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TAuthLogoutInput from InputTypes diff --git a/src/Domain/Services/Auth/RefreshPostService.php b/src/Domain/Services/Auth/RefreshPostService.php index d30a53f..7d50910 100644 --- a/src/Domain/Services/Auth/RefreshPostService.php +++ b/src/Domain/Services/Auth/RefreshPostService.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Services\Auth; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TAuthRefreshInput from InputTypes diff --git a/src/Domain/Services/User/UserDeleteService.php b/src/Domain/Services/User/UserDeleteService.php index 27e7f00..4ed07d0 100644 --- a/src/Domain/Services/User/UserDeleteService.php +++ b/src/Domain/Services/User/UserDeleteService.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TUserInput from InputTypes diff --git a/src/Domain/Services/User/UserGetService.php b/src/Domain/Services/User/UserGetService.php index 6fe3716..433ea29 100644 --- a/src/Domain/Services/User/UserGetService.php +++ b/src/Domain/Services/User/UserGetService.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TUserInput from InputTypes diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Services/User/UserPostService.php index ea77145..91928c0 100644 --- a/src/Domain/Services/User/UserPostService.php +++ b/src/Domain/Services/User/UserPostService.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TUserInput from InputTypes diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Services/User/UserPutService.php index d1a5691..cb74dd7 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Services/User/UserPutService.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\ADR\Payload; /** * @phpstan-import-type TUserInput from InputTypes diff --git a/tests/Fixtures/Domain/Services/ServiceFixture.php b/tests/Fixtures/Domain/Services/ServiceFixture.php index fc9dee9..c47933b 100644 --- a/tests/Fixtures/Domain/Services/ServiceFixture.php +++ b/tests/Fixtures/Domain/Services/ServiceFixture.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Fixtures\Domain\Services; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Components\Payload; +use Phalcon\Api\Domain\ADR\Payload; final readonly class ServiceFixture implements DomainInterface { From 2b92021ea7628ce7309b4ad8c769067db5697029 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 9 Nov 2025 07:39:31 -0600 Subject: [PATCH 200/301] [#.x] - phpcs and refactoring the container --- src/Domain/Components/Container.php | 449 ++---------------- .../DataSource/Validation/AbsInt.php | 4 - .../Enums/Container/AuthDefinitionsEnum.php | 137 ++++++ .../Enums/Container/CommonDefinitionsEnum.php | 84 ++++ .../Enums/Container/UserDefinitionsEnum.php | 173 +++++++ 5 files changed, 438 insertions(+), 409 deletions(-) create mode 100644 src/Domain/Components/Enums/Container/AuthDefinitionsEnum.php create mode 100644 src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php create mode 100644 src/Domain/Components/Enums/Container/UserDefinitionsEnum.php diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index 12a16ad..3461fe8 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -14,20 +14,11 @@ namespace Phalcon\Api\Domain\Components; use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthFacade; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthLoginValidator; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthSanitizer; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthTokenValidator; -use Phalcon\Api\Domain\Components\DataSource\User\UserFacade; use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; -use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; -use Phalcon\Api\Domain\Components\DataSource\User\UserValidatorUpdate; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Encryption\TokenCache; -use Phalcon\Api\Domain\Components\Encryption\TokenManager; +use Phalcon\Api\Domain\Components\Enums\Container\AuthDefinitionsEnum; +use Phalcon\Api\Domain\Components\Enums\Container\CommonDefinitionsEnum; +use Phalcon\Api\Domain\Components\Enums\Container\UserDefinitionsEnum; use Phalcon\Api\Domain\Components\Env\EnvManager; use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; @@ -36,13 +27,6 @@ use Phalcon\Api\Domain\Components\Middleware\ValidateTokenRevokedMiddleware; use Phalcon\Api\Domain\Components\Middleware\ValidateTokenStructureMiddleware; use Phalcon\Api\Domain\Components\Middleware\ValidateTokenUserMiddleware; -use Phalcon\Api\Domain\Services\Auth\LoginPostService; -use Phalcon\Api\Domain\Services\Auth\LogoutPostService; -use Phalcon\Api\Domain\Services\Auth\RefreshPostService; -use Phalcon\Api\Domain\Services\User\UserDeleteService; -use Phalcon\Api\Domain\Services\User\UserGetService; -use Phalcon\Api\Domain\Services\User\UserPostService; -use Phalcon\Api\Domain\Services\User\UserPutService; use Phalcon\Api\Responder\JsonResponder; use Phalcon\Cache\AdapterFactory; use Phalcon\Cache\Cache; @@ -56,10 +40,27 @@ use Phalcon\Http\Response; use Phalcon\Logger\Adapter\Stream; use Phalcon\Logger\Logger; -use Phalcon\Mvc\Router; use Phalcon\Storage\SerializerFactory; use Phalcon\Support\Registry; +use function sprintf; + +/** + * @phpstan-type TServiceService array{ + * type: string, + * name: string + * } + * + * @phpstan-type TServiceParameter array{ + * type: string, + * value: mixed + * } + * + * @phpstan-type TService array{ + * className: string, + * arguments: array + * } + */ class Container extends Di { /** @var string */ @@ -152,89 +153,44 @@ public function __construct() $this->services = [ self::CACHE => $this->getServiceCache(), self::CONNECTION => $this->getServiceConnection(), - self::ENV => $this->getServiceEnv(), + self::ENV => new Service(EnvManager::class, true), self::EVENTS_MANAGER => $this->getServiceEventsManger(), self::FILTER => $this->getServiceFilter(), - self::JWT_TOKEN => $this->getServiceJWTToken(), - self::JWT_TOKEN_CACHE => $this->getServiceJWTTokenCache(), - self::JWT_TOKEN_MANAGER => $this->getServiceJWTTokenManager(), + self::JWT_TOKEN => new Service(CommonDefinitionsEnum::JWTToken->definition(), true), + self::JWT_TOKEN_CACHE => new Service(CommonDefinitionsEnum::JWTTokenCache->definition(), true), + self::JWT_TOKEN_MANAGER => new Service(CommonDefinitionsEnum::JWTTokenManager->definition(), true), self::LOGGER => $this->getServiceLogger(), self::REGISTRY => new Service(Registry::class, true), self::REQUEST => new Service(Request::class, true), self::RESPONSE => new Service(Response::class, true), - self::ROUTER => $this->getServiceRouter(), + self::ROUTER => new Service(CommonDefinitionsEnum::Router->definition(), true), + + self::AUTH_FACADE => new Service(AuthDefinitionsEnum::AuthFacade->definition()), + self::USER_FACADE => new Service(UserDefinitionsEnum::UserFacade->definition()), + self::USER_FACADE_UPDATE => new Service(UserDefinitionsEnum::UserFacadeUpdate->definition()), - self::AUTH_FACADE => $this->getServiceFacadeAuth(), - self::USER_FACADE => $this->getServiceFacadeUser(), - self::USER_FACADE_UPDATE => $this->getServiceFacadeUserUpdate(), - self::USER_REPOSITORY => $this->getServiceRepositoryUser(), + self::USER_REPOSITORY => new Service(UserDefinitionsEnum::UserRepository->definition()), - self::AUTH_SANITIZER => $this->getServiceSanitizer(AuthSanitizer::class), - self::USER_SANITIZER => $this->getServiceSanitizer(UserSanitizer::class), + self::AUTH_SANITIZER => new Service(AuthDefinitionsEnum::AuthSanitizer->definition()), + self::USER_SANITIZER => new Service(UserDefinitionsEnum::UserSanitizer->definition()), - self::AUTH_LOGIN_VALIDATOR => $this->getServiceValidator(AuthLoginValidator::class), - self::AUTH_TOKEN_VALIDATOR => $this->getServiceValidatorAuthToken(), - self::USER_VALIDATOR => $this->getServiceValidator(UserValidator::class), - self::USER_VALIDATOR_UPDATE => $this->getServiceValidator(UserValidatorUpdate::class), + self::AUTH_LOGIN_VALIDATOR => new Service(AuthDefinitionsEnum::AuthLoginValidator->definition()), + self::AUTH_TOKEN_VALIDATOR => new Service(AuthDefinitionsEnum::AuthTokenValidator->definition()), + self::USER_VALIDATOR => new Service(UserDefinitionsEnum::UserValidator->definition()), + self::USER_VALIDATOR_UPDATE => new Service(UserDefinitionsEnum::UserValidatorUpdate->definition()), - self::AUTH_LOGIN_POST_SERVICE => $this->getServiceAuthLoginPost(), - self::AUTH_LOGOUT_POST_SERVICE => $this->getServiceAuthTokenPost(LogoutPostService::class), - self::AUTH_REFRESH_POST_SERVICE => $this->getServiceAuthTokenPost(RefreshPostService::class), - self::USER_DELETE_SERVICE => $this->getServiceUser(UserDeleteService::class), - self::USER_GET_SERVICE => $this->getServiceUser(UserGetService::class), - self::USER_POST_SERVICE => $this->getServiceUser(UserPostService::class), - self::USER_PUT_SERVICE => $this->getServiceUserUpdate(), + self::AUTH_LOGIN_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthLoginPost->definition()), + self::AUTH_LOGOUT_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthLogoutPost->definition()), + self::AUTH_REFRESH_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthRefreshPost->definition()), + self::USER_DELETE_SERVICE => new Service(UserDefinitionsEnum::UserDelete->definition()), + self::USER_GET_SERVICE => new Service(UserDefinitionsEnum::UserGet->definition()), + self::USER_POST_SERVICE => new Service(UserDefinitionsEnum::UserPost->definition()), + self::USER_PUT_SERVICE => new Service(UserDefinitionsEnum::UserPut->definition()), ]; parent::__construct(); } - /** - * @return Service - */ - private function getServiceAuthLoginPost(): Service - { - return new Service( - [ - 'className' => LoginPostService::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::AUTH_FACADE, - ], - [ - 'type' => 'service', - 'name' => self::AUTH_LOGIN_VALIDATOR, - ], - ], - ] - ); - } - - /** - * @param class-string $className - * - * @return Service - */ - private function getServiceAuthTokenPost(string $className): Service - { - return new Service( - [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::AUTH_FACADE, - ], - [ - 'type' => 'service', - 'name' => self::AUTH_TOKEN_VALIDATOR, - ], - ], - ] - ); - } - /** * @return Service */ @@ -321,19 +277,6 @@ function () { ); } - /** - * @return Service - */ - private function getServiceEnv(): Service - { - return new Service( - function () { - return new EnvManager(); - }, - true - ); - } - /** * @return Service */ @@ -350,104 +293,6 @@ function () { ); } - /** - * @return Service - */ - private function getServiceFacadeAuth(): Service - { - return new Service( - [ - 'className' => AuthFacade::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::USER_REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::AUTH_SANITIZER, - ], - [ - 'type' => 'service', - 'name' => self::JWT_TOKEN_MANAGER, - ], - [ - 'type' => 'service', - 'name' => self::SECURITY, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceFacadeUser(): Service - { - return new Service( - [ - 'className' => UserFacade::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::USER_SANITIZER, - ], - [ - 'type' => 'service', - 'name' => self::USER_VALIDATOR, - ], - [ - 'type' => 'service', - 'name' => self::USER_MAPPER, - ], - [ - 'type' => 'service', - 'name' => self::USER_REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::SECURITY, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceFacadeUserUpdate(): Service - { - return new Service( - [ - 'className' => UserFacade::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::USER_SANITIZER, - ], - [ - 'type' => 'service', - 'name' => self::USER_VALIDATOR_UPDATE, - ], - [ - 'type' => 'service', - 'name' => self::USER_MAPPER, - ], - [ - 'type' => 'service', - 'name' => self::USER_REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::SECURITY, - ], - ], - ] - ); - } - /** * @return Service */ @@ -461,68 +306,6 @@ function () { ); } - /** - * @return Service - */ - private function getServiceJWTToken(): Service - { - return new Service( - [ - 'className' => JWTToken::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::ENV, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceJWTTokenCache(): Service - { - return new Service( - [ - 'className' => TokenCache::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::CACHE, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceJWTTokenManager(): Service - { - return new Service( - [ - 'className' => TokenManager::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::JWT_TOKEN_CACHE, - ], - [ - 'type' => 'service', - 'name' => self::ENV, - ], - [ - 'type' => 'service', - 'name' => self::JWT_TOKEN, - ], - ], - ] - ); - } - /** * @return Service */ @@ -548,148 +331,4 @@ function () { } ); } - - /** - * @return Service - */ - private function getServiceRepositoryUser(): Service - { - return new Service( - [ - 'className' => UserRepository::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::CONNECTION, - ], - [ - 'type' => 'service', - 'name' => self::USER_MAPPER, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceRouter(): Service - { - return new Service( - [ - 'className' => Router::class, - 'arguments' => [ - [ - 'type' => 'parameter', - 'value' => false, - ], - ], - ] - ); - } - - /** - * @param class-string $className - * - * @return Service - */ - private function getServiceSanitizer(string $className): Service - { - return new Service( - [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::FILTER, - ], - ], - ] - ); - } - - /** - * @param class-string $className - * - * @return Service - */ - private function getServiceUser(string $className): Service - { - return new Service( - [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::USER_FACADE, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceUserUpdate(): Service - { - return new Service( - [ - 'className' => UserPutService::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::USER_FACADE_UPDATE, - ], - ], - ] - ); - } - - /** - * @param class-string $className - * - * @return Service - */ - private function getServiceValidator(string $className): Service - { - return new Service( - [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::VALIDATION, - ], - ], - ] - ); - } - - /** - * @return Service - */ - private function getServiceValidatorAuthToken(): Service - { - return new Service( - [ - 'className' => AuthTokenValidator::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => self::JWT_TOKEN_MANAGER, - ], - [ - 'type' => 'service', - 'name' => self::USER_REPOSITORY, - ], - [ - 'type' => 'service', - 'name' => self::VALIDATION, - ], - ], - ] - ); - } } diff --git a/src/Domain/Components/DataSource/Validation/AbsInt.php b/src/Domain/Components/DataSource/Validation/AbsInt.php index 4bfbb1a..6575b70 100644 --- a/src/Domain/Components/DataSource/Validation/AbsInt.php +++ b/src/Domain/Components/DataSource/Validation/AbsInt.php @@ -14,12 +14,8 @@ namespace Phalcon\Api\Domain\Components\DataSource\Validation; use Phalcon\Filter\Validation; -use Phalcon\Filter\Validation\AbstractValidator; - use Phalcon\Filter\Validation\Validator\Numericality; -use function preg_match; - class AbsInt extends Numericality { /** diff --git a/src/Domain/Components/Enums/Container/AuthDefinitionsEnum.php b/src/Domain/Components/Enums/Container/AuthDefinitionsEnum.php new file mode 100644 index 0000000..c87e8b9 --- /dev/null +++ b/src/Domain/Components/Enums/Container/AuthDefinitionsEnum.php @@ -0,0 +1,137 @@ + + * + * 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\Components\Enums\Container; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthFacade; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthLoginValidator; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthSanitizer; +use Phalcon\Api\Domain\Components\DataSource\Auth\AuthTokenValidator; +use Phalcon\Api\Domain\Services\Auth\LoginPostService; +use Phalcon\Api\Domain\Services\Auth\LogoutPostService; +use Phalcon\Api\Domain\Services\Auth\RefreshPostService; + +/** + * @phpstan-import-type TService from Container + */ +enum AuthDefinitionsEnum +{ + case AuthLoginPost; + case AuthLogoutPost; + case AuthRefreshPost; + case AuthFacade; + case AuthSanitizer; + case AuthLoginValidator; + case AuthTokenValidator; + + /** + * @return TService + */ + public function definition(): array + { + return match ($this) { + self::AuthLoginPost => [ + 'className' => LoginPostService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::AUTH_FACADE, + ], + [ + 'type' => 'service', + 'name' => Container::AUTH_LOGIN_VALIDATOR, + ], + ], + ], + self::AuthLogoutPost => $this->getService(LogoutPostService::class), + self::AuthRefreshPost => $this->getService(RefreshPostService::class), + self::AuthFacade => [ + 'className' => AuthFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => Container::AUTH_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => Container::JWT_TOKEN_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Container::SECURITY, + ], + ], + ], + self::AuthSanitizer => [ + 'className' => AuthSanitizer::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::FILTER, + ], + ], + ], + self::AuthLoginValidator => [ + 'className' => AuthLoginValidator::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::VALIDATION, + ], + ], + ], + self::AuthTokenValidator => [ + 'className' => AuthTokenValidator::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::JWT_TOKEN_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => Container::VALIDATION, + ], + ], + ], + }; + } + + /** + * @return TService + */ + private function getService(string $className): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::AUTH_FACADE, + ], + [ + 'type' => 'service', + 'name' => Container::AUTH_TOKEN_VALIDATOR, + ], + ], + ]; + } +} diff --git a/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php b/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php new file mode 100644 index 0000000..6d09463 --- /dev/null +++ b/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php @@ -0,0 +1,84 @@ + + * + * 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\Components\Enums\Container; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Components\Encryption\TokenCache; +use Phalcon\Api\Domain\Components\Encryption\TokenManager; +use Phalcon\Mvc\Router; + +/** + * @phpstan-import-type TService from Container + */ +enum CommonDefinitionsEnum +{ + case JWTToken; + case JWTTokenCache; + case JWTTokenManager; + case Router; + + /** + * @return TService + */ + public function definition(): array + { + return match ($this) { + self::JWTToken => [ + 'className' => JWTToken::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::ENV, + ], + ], + ], + self::JWTTokenCache => [ + 'className' => TokenCache::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::CACHE, + ], + ], + ], + self::JWTTokenManager => [ + 'className' => TokenManager::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::JWT_TOKEN_CACHE, + ], + [ + 'type' => 'service', + 'name' => Container::ENV, + ], + [ + 'type' => 'service', + 'name' => Container::JWT_TOKEN, + ], + ], + ], + self::Router => [ + 'className' => Router::class, + 'arguments' => [ + [ + 'type' => 'parameter', + 'value' => false, + ], + ], + ] + }; + } +} diff --git a/src/Domain/Components/Enums/Container/UserDefinitionsEnum.php b/src/Domain/Components/Enums/Container/UserDefinitionsEnum.php new file mode 100644 index 0000000..36c950f --- /dev/null +++ b/src/Domain/Components/Enums/Container/UserDefinitionsEnum.php @@ -0,0 +1,173 @@ + + * + * 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\Components\Enums\Container; + +use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Components\DataSource\User\UserFacade; +use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; +use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; +use Phalcon\Api\Domain\Components\DataSource\User\UserValidatorUpdate; +use Phalcon\Api\Domain\Services\User\UserDeleteService; +use Phalcon\Api\Domain\Services\User\UserGetService; +use Phalcon\Api\Domain\Services\User\UserPostService; +use Phalcon\Api\Domain\Services\User\UserPutService; + +/** + * @phpstan-import-type TService from Container + */ +enum UserDefinitionsEnum +{ + case UserDelete; + case UserGet; + case UserPost; + case UserPut; + case UserFacade; + case UserFacadeUpdate; + case UserRepository; + case UserSanitizer; + case UserValidator; + case UserValidatorUpdate; + + /** + * @return TService + */ + public function definition(): array + { + return match ($this) { + self::UserDelete => $this->getService(UserDeleteService::class), + self::UserGet => $this->getService(UserGetService::class), + self::UserPost => $this->getService(UserPostService::class), + self::UserPut => [ + 'className' => UserPutService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_FACADE_UPDATE, + ], + ], + ], + self::UserFacade => [ + 'className' => UserFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_VALIDATOR, + ], + [ + 'type' => 'service', + 'name' => Container::USER_MAPPER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => Container::SECURITY, + ], + ], + ], + self::UserFacadeUpdate => [ + 'className' => UserFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_SANITIZER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_VALIDATOR_UPDATE, + ], + [ + 'type' => 'service', + 'name' => Container::USER_MAPPER, + ], + [ + 'type' => 'service', + 'name' => Container::USER_REPOSITORY, + ], + [ + 'type' => 'service', + 'name' => Container::SECURITY, + ], + ], + ], + self::UserRepository => [ + 'className' => UserRepository::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::CONNECTION, + ], + [ + 'type' => 'service', + 'name' => Container::USER_MAPPER, + ], + ], + ], + self::UserSanitizer => [ + 'className' => UserSanitizer::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::FILTER, + ], + ], + ], + self::UserValidator => $this->getServiceValidator(UserValidator::class), + self::UserValidatorUpdate => $this->getServiceValidator(UserValidatorUpdate::class), + }; + } + + /** + * @param class-string $className + * + * @return TService + */ + private function getService(string $className): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::USER_FACADE, + ], + ], + ]; + } + + /** + * @param class-string $className + * + * @return TService + */ + private function getServiceValidator(string $className): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::VALIDATION, + ], + ], + ]; + } +} From 94ae56b09081e885f4d756cf571fddd973a9bc29 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 9 Nov 2025 15:13:20 -0600 Subject: [PATCH 201/301] [#.x] - added evmanager to definitions and phpstan --- src/Domain/Components/Container.php | 55 +++++++++++-------- .../Enums/Container/CommonDefinitionsEnum.php | 16 ++++++ 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/Domain/Components/Container.php b/src/Domain/Components/Container.php index 3461fe8..34a6f94 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Components/Container.php @@ -33,7 +33,6 @@ use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Di\Di; use Phalcon\Di\Service; -use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\FilterFactory; use Phalcon\Filter\Validation; use Phalcon\Http\Request; @@ -46,19 +45,25 @@ use function sprintf; /** - * @phpstan-type TServiceService array{ - * type: string, - * name: string - * } * * @phpstan-type TServiceParameter array{ - * type: string, + * type: 'parameter', * value: mixed * } + * @phpstan-type TServiceService array{ + * type: 'service', + * name: string + * } + * @phpstan-type TServiceArguments array + * @phpstan-type TServiceCall array{ + * method: string, + * arguments: TServiceArguments + * } * * @phpstan-type TService array{ * className: string, - * arguments: array + * arguments?: TServiceArguments, + * calls?: array * } */ class Container extends Di @@ -151,10 +156,13 @@ class Container extends Di public function __construct() { $this->services = [ + /** + * Base services + */ self::CACHE => $this->getServiceCache(), self::CONNECTION => $this->getServiceConnection(), self::ENV => new Service(EnvManager::class, true), - self::EVENTS_MANAGER => $this->getServiceEventsManger(), + self::EVENTS_MANAGER => new Service(CommonDefinitionsEnum::EventsManager->definition(), true), self::FILTER => $this->getServiceFilter(), self::JWT_TOKEN => new Service(CommonDefinitionsEnum::JWTToken->definition(), true), self::JWT_TOKEN_CACHE => new Service(CommonDefinitionsEnum::JWTTokenCache->definition(), true), @@ -165,20 +173,35 @@ public function __construct() self::RESPONSE => new Service(Response::class, true), self::ROUTER => new Service(CommonDefinitionsEnum::Router->definition(), true), + /** + * Facades + */ self::AUTH_FACADE => new Service(AuthDefinitionsEnum::AuthFacade->definition()), self::USER_FACADE => new Service(UserDefinitionsEnum::UserFacade->definition()), self::USER_FACADE_UPDATE => new Service(UserDefinitionsEnum::UserFacadeUpdate->definition()), + /** + * Repositories + */ self::USER_REPOSITORY => new Service(UserDefinitionsEnum::UserRepository->definition()), + /** + * Sanitizers + */ self::AUTH_SANITIZER => new Service(AuthDefinitionsEnum::AuthSanitizer->definition()), self::USER_SANITIZER => new Service(UserDefinitionsEnum::UserSanitizer->definition()), + /** + * Validators + */ self::AUTH_LOGIN_VALIDATOR => new Service(AuthDefinitionsEnum::AuthLoginValidator->definition()), self::AUTH_TOKEN_VALIDATOR => new Service(AuthDefinitionsEnum::AuthTokenValidator->definition()), self::USER_VALIDATOR => new Service(UserDefinitionsEnum::UserValidator->definition()), self::USER_VALIDATOR_UPDATE => new Service(UserDefinitionsEnum::UserValidatorUpdate->definition()), + /** + * Services + */ self::AUTH_LOGIN_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthLoginPost->definition()), self::AUTH_LOGOUT_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthLogoutPost->definition()), self::AUTH_REFRESH_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthRefreshPost->definition()), @@ -277,22 +300,6 @@ function () { ); } - /** - * @return Service - */ - private function getServiceEventsManger(): Service - { - return new Service( - function () { - $evm = new EventsManager(); - $evm->enablePriorities(true); - - return $evm; - }, - true - ); - } - /** * @return Service */ diff --git a/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php b/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php index 6d09463..b022647 100644 --- a/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php +++ b/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php @@ -17,6 +17,7 @@ use Phalcon\Api\Domain\Components\Encryption\JWTToken; use Phalcon\Api\Domain\Components\Encryption\TokenCache; use Phalcon\Api\Domain\Components\Encryption\TokenManager; +use Phalcon\Events\Manager as EventsManager; use Phalcon\Mvc\Router; /** @@ -24,6 +25,7 @@ */ enum CommonDefinitionsEnum { + case EventsManager; case JWTToken; case JWTTokenCache; case JWTTokenManager; @@ -35,6 +37,20 @@ enum CommonDefinitionsEnum public function definition(): array { return match ($this) { + self::EventsManager => [ + 'className' => EventsManager::class, + 'calls' => [ + [ + 'method' => 'enablePriorities', + 'arguments' => [ + [ + 'type' => 'parameter', + 'value' => true, + ] + ] + ] + ] + ], self::JWTToken => [ 'className' => JWTToken::class, 'arguments' => [ From 721590a56f1d96e56f6e2b0d1bb3904c27939e2c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 11 Nov 2025 11:06:36 -0600 Subject: [PATCH 202/301] [#.x] - renamed folders, moved files to logical positions --- .../Constants/Cache.php | 4 +-- .../Constants/Dates.php | 2 +- .../Container.php | 30 +++++++++---------- .../DataSource/AbstractRepository.php | 4 +-- .../DataSource/AbstractValueObject.php} | 8 ++--- .../DataSource/Auth/DTO}/AuthInput.php | 6 ++-- .../DataSource/Auth/Facades}/AuthFacade.php | 17 ++++++----- .../Auth/Sanitizers}/AuthSanitizer.php | 4 +-- .../Auth/Validators}/AuthLoginValidator.php | 11 +++---- .../Auth/Validators}/AuthTokenValidator.php | 17 ++++++----- .../DataSource/Interfaces/MapperInterface.php | 8 ++--- .../Interfaces/SanitizerInterface.php | 2 +- .../DataSource/User/DTO}/User.php | 4 ++- .../DataSource/User/DTO}/UserInput.php | 9 ++++-- .../DataSource/User/Facades}/UserFacade.php | 18 ++++++----- .../DataSource/User/Mappers}/UserMapper.php | 7 +++-- .../User/Repositories}/UserRepository.php | 9 ++++-- .../Repositories}/UserRepositoryInterface.php | 5 +++- .../User/Sanitizers}/UserSanitizer.php | 4 +-- .../DataSource/User/UserTypes.php | 2 +- .../User/Validators}/UserValidator.php | 6 ++-- .../User/Validators}/UserValidatorTrait.php | 8 ++--- .../User/Validators}/UserValidatorUpdate.php | 6 ++-- .../DataSource/Validation/AbsInt.php | 3 +- .../Validation/AbstractValidator.php | 6 ++-- .../DataSource/Validation/Result.php | 2 +- .../Validation/ValidatorInterface.php | 2 +- .../Encryption/JWTToken.php | 16 +++++----- .../Encryption/Security.php | 2 +- .../Encryption/TokenCache.php | 12 ++++---- .../Encryption/TokenCacheInterface.php | 6 ++-- .../Encryption/TokenManager.php | 8 ++--- .../Encryption/TokenManagerInterface.php | 6 ++-- .../Enums/Common/FlagsEnum.php | 4 +-- .../Enums/Common/JWTEnum.php | 2 +- .../Enums/Container/AuthDefinitionsEnum.php | 12 ++++---- .../Enums/Container/CommonDefinitionsEnum.php | 10 +++---- .../Enums/Container/UserDefinitionsEnum.php | 14 ++++----- .../Enums/EnumsInterface.php | 2 +- .../Enums/Http/HttpCodesEnum.php | 4 +-- .../Enums/Http/RoutesEnum.php | 4 +-- .../Enums/Input/AuthLoginInputEnum.php | 2 +- .../Enums/Input/AuthTokenInputEnum.php | 2 +- .../Enums/Input/UserInputInsertEnum.php | 2 +- .../Enums/Input/UserInputUpdateEnum.php | 4 +-- .../Enums/Input/ValidatorEnumInterface.php | 2 +- .../Env/Adapters/AdapterInterface.php | 4 +-- .../Env/Adapters/DotEnv.php | 6 ++-- .../Env/EnvFactory.php | 8 ++--- .../Env/EnvManager.php | 4 +-- .../Env/EnvManagerTypes.php | 2 +- .../Exceptions/ExceptionTrait.php | 2 +- .../InvalidConfigurationArgumentException.php | 2 +- .../Exceptions/TokenValidationException.php | 2 +- .../Middleware/AbstractMiddleware.php | 6 ++-- .../Middleware/HealthMiddleware.php | 6 ++-- .../Middleware/NotFoundMiddleware.php | 4 +-- .../ValidateTokenClaimsMiddleware.php | 10 +++---- .../ValidateTokenPresenceMiddleware.php | 8 ++--- .../ValidateTokenRevokedMiddleware.php | 12 ++++---- .../ValidateTokenStructureMiddleware.php | 12 ++++---- .../ValidateTokenUserMiddleware.php | 10 +++---- .../Providers/ErrorHandlerProvider.php | 6 ++-- .../Providers/RouterProvider.php | 6 ++-- .../Services/Auth/AbstractAuthService.php | 4 +-- .../Services/User/AbstractUserService.php | 2 +- src/Responder/JsonResponder.php | 4 +-- .../ContainerTest.php | 0 .../DataSource/User/DTO}/UserInputTest.php | 8 ++--- .../DataSource/User/DTO}/UserTest.php | 4 +-- .../User/Mappers}/UserMapperTest.php | 8 ++--- .../User/Repositories}/UserRepositoryTest.php | 8 ++--- .../User/Sanitizers}/UserSanitizerTest.php | 6 ++-- .../User/Validators}/UserValidatorTest.php | 10 +++---- .../Encryption/JWTTokenTest.php | 0 .../Encryption/SecurityTest.php | 0 .../Enums/Common/FlagsEnumTest.php | 0 .../Enums/Http/HttpCodesEnumTest.php | 0 .../Enums/Http/RoutesEnumTest.php | 0 .../Env/Adapters/DotEnvTest.php | 0 .../Env/EnvFactoryTest.php | 0 .../Env/EnvManagerTest.php | 0 .../Middleware/HealthMiddlewareTest.php | 0 .../Middleware/NotFoundMiddlewareTest.php | 0 .../ValidateTokenClaimsMiddlewareTest.php | 0 .../ValidateTokenPresenceMiddlewareTest.php | 0 .../ValidateTokenRevokedMiddlewareTest.php | 0 .../ValidateTokenStructureMiddlewareTest.php | 0 .../ValidateTokenUserMiddlewareTest.php | 0 .../Providers/CacheDataProviderTest.php | 0 .../Providers/ErrorHandlerProviderTest.php | 0 .../Providers/RouterProviderTest.php | 0 92 files changed, 249 insertions(+), 233 deletions(-) rename src/Domain/{Components => Infrastructure}/Constants/Cache.php (91%) rename src/Domain/{Components => Infrastructure}/Constants/Dates.php (94%) rename src/Domain/{Components => Infrastructure}/Container.php (91%) rename src/Domain/{Components => Infrastructure}/DataSource/AbstractRepository.php (91%) rename src/Domain/{Components/DataSource/AbstractInput.php => Infrastructure/DataSource/AbstractValueObject.php} (83%) rename src/Domain/{Components/DataSource/Auth => Infrastructure/DataSource/Auth/DTO}/AuthInput.php (85%) rename src/Domain/{Components/DataSource/Auth => Infrastructure/DataSource/Auth/Facades}/AuthFacade.php (88%) rename src/Domain/{Components/DataSource/Auth => Infrastructure/DataSource/Auth/Sanitizers}/AuthSanitizer.php (92%) rename src/Domain/{Components/DataSource/Auth => Infrastructure/DataSource/Auth/Validators}/AuthLoginValidator.php (67%) rename src/Domain/{Components/DataSource/Auth => Infrastructure/DataSource/Auth/Validators}/AuthTokenValidator.php (78%) rename src/Domain/{Components => Infrastructure}/DataSource/Interfaces/MapperInterface.php (78%) rename src/Domain/{Components => Infrastructure}/DataSource/Interfaces/SanitizerInterface.php (89%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/DTO}/User.php (92%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/DTO}/UserInput.php (93%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Facades}/UserFacade.php (91%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Mappers}/UserMapper.php (90%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Repositories}/UserRepository.php (87%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Repositories}/UserRepositoryInterface.php (87%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Sanitizers}/UserSanitizer.php (94%) rename src/Domain/{Components => Infrastructure}/DataSource/User/UserTypes.php (98%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Validators}/UserValidator.php (64%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Validators}/UserValidatorTrait.php (72%) rename src/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Validators}/UserValidatorUpdate.php (64%) rename src/Domain/{Components => Infrastructure}/DataSource/Validation/AbsInt.php (92%) rename src/Domain/{Components => Infrastructure}/DataSource/Validation/AbstractValidator.php (87%) rename src/Domain/{Components => Infrastructure}/DataSource/Validation/Result.php (96%) rename src/Domain/{Components => Infrastructure}/DataSource/Validation/ValidatorInterface.php (89%) rename src/Domain/{Components => Infrastructure}/Encryption/JWTToken.php (91%) rename src/Domain/{Components => Infrastructure}/Encryption/Security.php (95%) rename src/Domain/{Components => Infrastructure}/Encryption/TokenCache.php (91%) rename src/Domain/{Components => Infrastructure}/Encryption/TokenCacheInterface.php (83%) rename src/Domain/{Components => Infrastructure}/Encryption/TokenManager.php (92%) rename src/Domain/{Components => Infrastructure}/Encryption/TokenManagerInterface.php (89%) rename src/Domain/{Components => Infrastructure}/Enums/Common/FlagsEnum.php (82%) rename src/Domain/{Components => Infrastructure}/Enums/Common/JWTEnum.php (92%) rename src/Domain/{Components => Infrastructure}/Enums/Container/AuthDefinitionsEnum.php (90%) rename src/Domain/{Components => Infrastructure}/Enums/Container/CommonDefinitionsEnum.php (89%) rename src/Domain/{Components => Infrastructure}/Enums/Container/UserDefinitionsEnum.php (90%) rename src/Domain/{Components => Infrastructure}/Enums/EnumsInterface.php (89%) rename src/Domain/{Components => Infrastructure}/Enums/Http/HttpCodesEnum.php (97%) rename src/Domain/{Components => Infrastructure}/Enums/Http/RoutesEnum.php (97%) rename src/Domain/{Components => Infrastructure}/Enums/Input/AuthLoginInputEnum.php (92%) rename src/Domain/{Components => Infrastructure}/Enums/Input/AuthTokenInputEnum.php (89%) rename src/Domain/{Components => Infrastructure}/Enums/Input/UserInputInsertEnum.php (93%) rename src/Domain/{Components => Infrastructure}/Enums/Input/UserInputUpdateEnum.php (87%) rename src/Domain/{Components => Infrastructure}/Enums/Input/ValidatorEnumInterface.php (88%) rename src/Domain/{Components => Infrastructure}/Env/Adapters/AdapterInterface.php (82%) rename src/Domain/{Components => Infrastructure}/Env/Adapters/DotEnv.php (85%) rename src/Domain/{Components => Infrastructure}/Env/EnvFactory.php (82%) rename src/Domain/{Components => Infrastructure}/Env/EnvManager.php (96%) rename src/Domain/{Components => Infrastructure}/Env/EnvManagerTypes.php (90%) rename src/Domain/{Components => Infrastructure}/Exceptions/ExceptionTrait.php (89%) rename src/Domain/{Components => Infrastructure}/Exceptions/InvalidConfigurationArgumentException.php (87%) rename src/Domain/{Components => Infrastructure}/Exceptions/TokenValidationException.php (87%) rename src/Domain/{Components => Infrastructure}/Middleware/AbstractMiddleware.php (94%) rename src/Domain/{Components => Infrastructure}/Middleware/HealthMiddleware.php (87%) rename src/Domain/{Components => Infrastructure}/Middleware/NotFoundMiddleware.php (90%) rename src/Domain/{Components => Infrastructure}/Middleware/ValidateTokenClaimsMiddleware.php (87%) rename src/Domain/{Components => Infrastructure}/Middleware/ValidateTokenPresenceMiddleware.php (84%) rename src/Domain/{Components => Infrastructure}/Middleware/ValidateTokenRevokedMiddleware.php (82%) rename src/Domain/{Components => Infrastructure}/Middleware/ValidateTokenStructureMiddleware.php (83%) rename src/Domain/{Components => Infrastructure}/Middleware/ValidateTokenUserMiddleware.php (85%) rename src/Domain/{Components => Infrastructure}/Providers/ErrorHandlerProvider.php (94%) rename src/Domain/{Components => Infrastructure}/Providers/RouterProvider.php (95%) rename tests/Unit/Domain/{Components => Infrastructure}/ContainerTest.php (100%) rename tests/Unit/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/DTO}/UserInputTest.php (94%) rename tests/Unit/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/DTO}/UserTest.php (95%) rename tests/Unit/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Mappers}/UserMapperTest.php (96%) rename tests/Unit/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Repositories}/UserRepositoryTest.php (93%) rename tests/Unit/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Sanitizers}/UserSanitizerTest.php (94%) rename tests/Unit/Domain/{Components/DataSource/User => Infrastructure/DataSource/User/Validators}/UserValidatorTest.php (86%) rename tests/Unit/Domain/{Components => Infrastructure}/Encryption/JWTTokenTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Encryption/SecurityTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Enums/Common/FlagsEnumTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Enums/Http/HttpCodesEnumTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Enums/Http/RoutesEnumTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Env/Adapters/DotEnvTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Env/EnvFactoryTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Env/EnvManagerTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Middleware/HealthMiddlewareTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Middleware/NotFoundMiddlewareTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Middleware/ValidateTokenClaimsMiddlewareTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Middleware/ValidateTokenPresenceMiddlewareTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Middleware/ValidateTokenRevokedMiddlewareTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Middleware/ValidateTokenStructureMiddlewareTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Middleware/ValidateTokenUserMiddlewareTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Providers/CacheDataProviderTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Providers/ErrorHandlerProviderTest.php (100%) rename tests/Unit/Domain/{Components => Infrastructure}/Providers/RouterProviderTest.php (100%) diff --git a/src/Domain/Components/Constants/Cache.php b/src/Domain/Infrastructure/Constants/Cache.php similarity index 91% rename from src/Domain/Components/Constants/Cache.php rename to src/Domain/Infrastructure/Constants/Cache.php index be360a0..73f691c 100644 --- a/src/Domain/Components/Constants/Cache.php +++ b/src/Domain/Infrastructure/Constants/Cache.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Constants; +namespace Phalcon\Api\Domain\Infrastructure\Constants; -use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use function sha1; diff --git a/src/Domain/Components/Constants/Dates.php b/src/Domain/Infrastructure/Constants/Dates.php similarity index 94% rename from src/Domain/Components/Constants/Dates.php rename to src/Domain/Infrastructure/Constants/Dates.php index c3f1f71..b80248a 100644 --- a/src/Domain/Components/Constants/Dates.php +++ b/src/Domain/Infrastructure/Constants/Dates.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Constants; +namespace Phalcon\Api\Domain\Infrastructure\Constants; use DateTimeImmutable; use DateTimeZone; diff --git a/src/Domain/Components/Container.php b/src/Domain/Infrastructure/Container.php similarity index 91% rename from src/Domain/Components/Container.php rename to src/Domain/Infrastructure/Container.php index 34a6f94..6c290d7 100644 --- a/src/Domain/Components/Container.php +++ b/src/Domain/Infrastructure/Container.php @@ -11,22 +11,22 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components; +namespace Phalcon\Api\Domain\Infrastructure; -use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Enums\Container\AuthDefinitionsEnum; -use Phalcon\Api\Domain\Components\Enums\Container\CommonDefinitionsEnum; -use Phalcon\Api\Domain\Components\Enums\Container\UserDefinitionsEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; -use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenClaimsMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenPresenceMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenRevokedMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenStructureMiddleware; -use Phalcon\Api\Domain\Components\Middleware\ValidateTokenUserMiddleware; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\AuthDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\CommonDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\UserDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenClaimsMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenPresenceMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenRevokedMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenStructureMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenUserMiddleware; use Phalcon\Api\Responder\JsonResponder; use Phalcon\Cache\AdapterFactory; use Phalcon\Cache\Cache; diff --git a/src/Domain/Components/DataSource/AbstractRepository.php b/src/Domain/Infrastructure/DataSource/AbstractRepository.php similarity index 91% rename from src/Domain/Components/DataSource/AbstractRepository.php rename to src/Domain/Infrastructure/DataSource/AbstractRepository.php index a5125f4..4ac90dc 100644 --- a/src/Domain/Components/DataSource/AbstractRepository.php +++ b/src/Domain/Infrastructure/DataSource/AbstractRepository.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource; +namespace Phalcon\Api\Domain\Infrastructure\DataSource; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\DataMapper\Query\Delete; diff --git a/src/Domain/Components/DataSource/AbstractInput.php b/src/Domain/Infrastructure/DataSource/AbstractValueObject.php similarity index 83% rename from src/Domain/Components/DataSource/AbstractInput.php rename to src/Domain/Infrastructure/DataSource/AbstractValueObject.php index 16993b7..b3be385 100644 --- a/src/Domain/Components/DataSource/AbstractInput.php +++ b/src/Domain/Infrastructure/DataSource/AbstractValueObject.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource; +namespace Phalcon\Api\Domain\Infrastructure\DataSource; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; /** - * Base factory for input DTOs. + * Base factory for value objects such as input DTOs. * * Concrete DTOs must implement protected static function * @@ -27,7 +27,7 @@ * * @phpstan-import-type TInputSanitize from InputTypes */ -abstract class AbstractInput +abstract class AbstractValueObject { /** * Factory that accepts a SanitizerInterface and returns the concrete DTO. diff --git a/src/Domain/Components/DataSource/Auth/AuthInput.php b/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php similarity index 85% rename from src/Domain/Components/DataSource/Auth/AuthInput.php rename to src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php index 3fe35d6..6194bd8 100644 --- a/src/Domain/Components/DataSource/Auth/AuthInput.php +++ b/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php @@ -11,15 +11,15 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Auth; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\AbstractInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractValueObject; /** * @phpstan-import-type TAuthInput from InputTypes */ -final class AuthInput extends AbstractInput +final class AuthInput extends AbstractValueObject { /** * @param string|null $email diff --git a/src/Domain/Components/DataSource/Auth/AuthFacade.php b/src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php similarity index 88% rename from src/Domain/Components/DataSource/Auth/AuthFacade.php rename to src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php index 35007b1..c4f24a8 100644 --- a/src/Domain/Components/DataSource/Auth/AuthFacade.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php @@ -11,17 +11,18 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Auth; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Facades; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; -use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; -use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Encryption\TokenManagerInterface; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; /** * @phpstan-import-type TAuthLoginInput from InputTypes diff --git a/src/Domain/Components/DataSource/Auth/AuthSanitizer.php b/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php similarity index 92% rename from src/Domain/Components/DataSource/Auth/AuthSanitizer.php rename to src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php index 3fa50de..8fde303 100644 --- a/src/Domain/Components/DataSource/Auth/AuthSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Auth; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; use Phalcon\Filter\Filter; use Phalcon\Filter\FilterInterface; diff --git a/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php similarity index 67% rename from src/Domain/Components/DataSource/Auth/AuthLoginValidator.php rename to src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php index d6bf761..45489cf 100644 --- a/src/Domain/Components/DataSource/Auth/AuthLoginValidator.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php @@ -11,12 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Auth; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators; -use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Components\DataSource\Validation\Result; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Enums\Input\AuthLoginInputEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Input\AuthLoginInputEnum; final class AuthLoginValidator extends AbstractValidator { diff --git a/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php similarity index 78% rename from src/Domain/Components/DataSource/Auth/AuthTokenValidator.php rename to src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php index 4425a35..ecd60f4 100644 --- a/src/Domain/Components/DataSource/Auth/AuthTokenValidator.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php @@ -11,15 +11,16 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Auth; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; -use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Components\DataSource\Validation\Result; -use Phalcon\Api\Domain\Components\Encryption\TokenManagerInterface; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Enums\Input\AuthTokenInputEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Input\AuthTokenInputEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Filter\Validation\ValidationInterface; diff --git a/src/Domain/Components/DataSource/Interfaces/MapperInterface.php b/src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php similarity index 78% rename from src/Domain/Components/DataSource/Interfaces/MapperInterface.php rename to src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php index 9b1c7c7..4392806 100644 --- a/src/Domain/Components/DataSource/Interfaces/MapperInterface.php +++ b/src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Interfaces; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; /** * Contract for mapping between domain DTO/objects and persistence arrays. diff --git a/src/Domain/Components/DataSource/Interfaces/SanitizerInterface.php b/src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php similarity index 89% rename from src/Domain/Components/DataSource/Interfaces/SanitizerInterface.php rename to src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php index bf184b1..543430d 100644 --- a/src/Domain/Components/DataSource/Interfaces/SanitizerInterface.php +++ b/src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Interfaces; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces; use Phalcon\Api\Domain\ADR\InputTypes; diff --git a/src/Domain/Components/DataSource/User/User.php b/src/Domain/Infrastructure/DataSource/User/DTO/User.php similarity index 92% rename from src/Domain/Components/DataSource/User/User.php rename to src/Domain/Infrastructure/DataSource/User/DTO/User.php index dd58248..ef3cb39 100644 --- a/src/Domain/Components/DataSource/User/User.php +++ b/src/Domain/Infrastructure/DataSource/User/DTO/User.php @@ -11,7 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use function get_object_vars; use function trim; diff --git a/src/Domain/Components/DataSource/User/UserInput.php b/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php similarity index 93% rename from src/Domain/Components/DataSource/User/UserInput.php rename to src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php index de0c63a..cce2cca 100644 --- a/src/Domain/Components/DataSource/User/UserInput.php +++ b/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php @@ -11,18 +11,21 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\AbstractInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractValueObject; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use function get_object_vars; /** + * Value object for input + * * @phpstan-import-type TUserInput from InputTypes * @phpstan-import-type TUser from UserTypes */ -final class UserInput extends AbstractInput +final class UserInput extends AbstractValueObject { /** * @param int $id diff --git a/src/Domain/Components/DataSource/User/UserFacade.php b/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php similarity index 91% rename from src/Domain/Components/DataSource/User/UserFacade.php rename to src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php index b372a11..574cbc7 100644 --- a/src/Domain/Components/DataSource/User/UserFacade.php +++ b/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php @@ -11,17 +11,21 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Facades; use PDOException; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\DataSource\Interfaces\MapperInterface; -use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; -use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use function array_filter; diff --git a/src/Domain/Components/DataSource/User/UserMapper.php b/src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php similarity index 90% rename from src/Domain/Components/DataSource/User/UserMapper.php rename to src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php index b996be1..cc6b0c6 100644 --- a/src/Domain/Components/DataSource/User/UserMapper.php +++ b/src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php @@ -11,9 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers; -use Phalcon\Api\Domain\Components\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; /** * @phpstan-import-type TUser from UserTypes diff --git a/src/Domain/Components/DataSource/User/UserRepository.php b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php similarity index 87% rename from src/Domain/Components/DataSource/User/UserRepository.php rename to src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php index 11c5bf9..b57a260 100644 --- a/src/Domain/Components/DataSource/User/UserRepository.php +++ b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php @@ -11,10 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories; -use Phalcon\Api\Domain\Components\DataSource\AbstractRepository; -use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\DataMapper\Query\Insert; use Phalcon\DataMapper\Query\Select; diff --git a/src/Domain/Components/DataSource/User/UserRepositoryInterface.php b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php similarity index 87% rename from src/Domain/Components/DataSource/User/UserRepositoryInterface.php rename to src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php index 5afb71b..5c72fac 100644 --- a/src/Domain/Components/DataSource/User/UserRepositoryInterface.php +++ b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php @@ -11,7 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; /** * @phpstan-import-type TCriteria from UserTypes diff --git a/src/Domain/Components/DataSource/User/UserSanitizer.php b/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php similarity index 94% rename from src/Domain/Components/DataSource/User/UserSanitizer.php rename to src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php index 98c073d..762db1a 100644 --- a/src/Domain/Components/DataSource/User/UserSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; use Phalcon\Filter\Filter; use Phalcon\Filter\FilterInterface; diff --git a/src/Domain/Components/DataSource/User/UserTypes.php b/src/Domain/Infrastructure/DataSource/User/UserTypes.php similarity index 98% rename from src/Domain/Components/DataSource/User/UserTypes.php rename to src/Domain/Infrastructure/DataSource/User/UserTypes.php index 3ccb744..f875630 100644 --- a/src/Domain/Components/DataSource/User/UserTypes.php +++ b/src/Domain/Infrastructure/DataSource/User/UserTypes.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User; /** * @phpstan-type TLoginResponsePayload array{ diff --git a/src/Domain/Components/DataSource/User/UserValidator.php b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php similarity index 64% rename from src/Domain/Components/DataSource/User/UserValidator.php rename to src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php index 331b1b7..c34c29e 100644 --- a/src/Domain/Components/DataSource/User/UserValidator.php +++ b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; -use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Components\Enums\Input\UserInputInsertEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Infrastructure\Enums\Input\UserInputInsertEnum; final class UserValidator extends AbstractValidator { diff --git a/src/Domain/Components/DataSource/User/UserValidatorTrait.php b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php similarity index 72% rename from src/Domain/Components/DataSource/User/UserValidatorTrait.php rename to src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php index 1bae490..69fd2b9 100644 --- a/src/Domain/Components/DataSource/User/UserValidatorTrait.php +++ b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php @@ -11,12 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Components\DataSource\Validation\Result; -use Phalcon\Api\Domain\Components\Enums\Input\UserInputInsertEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; trait UserValidatorTrait { diff --git a/src/Domain/Components/DataSource/User/UserValidatorUpdate.php b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php similarity index 64% rename from src/Domain/Components/DataSource/User/UserValidatorUpdate.php rename to src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php index 2f1d0a0..a8acfaa 100644 --- a/src/Domain/Components/DataSource/User/UserValidatorUpdate.php +++ b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\User; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; -use Phalcon\Api\Domain\Components\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Components\Enums\Input\UserInputUpdateEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; +use Phalcon\Api\Domain\Infrastructure\Enums\Input\UserInputUpdateEnum; final class UserValidatorUpdate extends AbstractValidator { diff --git a/src/Domain/Components/DataSource/Validation/AbsInt.php b/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php similarity index 92% rename from src/Domain/Components/DataSource/Validation/AbsInt.php rename to src/Domain/Infrastructure/DataSource/Validation/AbsInt.php index 6575b70..3d79039 100644 --- a/src/Domain/Components/DataSource/Validation/AbsInt.php +++ b/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Validation; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; use Phalcon\Filter\Validation; use Phalcon\Filter\Validation\Validator\Numericality; @@ -42,7 +42,6 @@ public function validate(Validation $validation, string $field): bool /** @var string $value */ $value = $validation->getValue($field); - $value = abs((int)$value); if ($value <= 0) { $validation->appendMessage( diff --git a/src/Domain/Components/DataSource/Validation/AbstractValidator.php b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php similarity index 87% rename from src/Domain/Components/DataSource/Validation/AbstractValidator.php rename to src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php index bd94e0d..95cae33 100644 --- a/src/Domain/Components/DataSource/Validation/AbstractValidator.php +++ b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Validation; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthInput; -use Phalcon\Api\Domain\Components\Enums\Input\ValidatorEnumInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\Enums\Input\ValidatorEnumInterface; use Phalcon\Filter\Validation\ValidationInterface; use Phalcon\Filter\Validation\ValidatorInterface as PhalconValidator; diff --git a/src/Domain/Components/DataSource/Validation/Result.php b/src/Domain/Infrastructure/DataSource/Validation/Result.php similarity index 96% rename from src/Domain/Components/DataSource/Validation/Result.php rename to src/Domain/Infrastructure/DataSource/Validation/Result.php index 4a81296..064d357 100644 --- a/src/Domain/Components/DataSource/Validation/Result.php +++ b/src/Domain/Infrastructure/DataSource/Validation/Result.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Validation; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; use Phalcon\Api\Domain\ADR\InputTypes; diff --git a/src/Domain/Components/DataSource/Validation/ValidatorInterface.php b/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php similarity index 89% rename from src/Domain/Components/DataSource/Validation/ValidatorInterface.php rename to src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php index 72979d2..e79bacd 100644 --- a/src/Domain/Components/DataSource/Validation/ValidatorInterface.php +++ b/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\DataSource\Validation; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; /** * Validator contract. Accepts a DTO or input and returns a Result. diff --git a/src/Domain/Components/Encryption/JWTToken.php b/src/Domain/Infrastructure/Encryption/JWTToken.php similarity index 91% rename from src/Domain/Components/Encryption/JWTToken.php rename to src/Domain/Infrastructure/Encryption/JWTToken.php index dc7be76..a00026e 100644 --- a/src/Domain/Components/Encryption/JWTToken.php +++ b/src/Domain/Infrastructure/Encryption/JWTToken.php @@ -11,18 +11,18 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Encryption; +namespace Phalcon\Api\Domain\Infrastructure\Encryption; use DateTimeImmutable; use InvalidArgumentException; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; -use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Exceptions\TokenValidationException; use Phalcon\Encryption\Security\JWT\Builder; use Phalcon\Encryption\Security\JWT\Signer\Hmac; use Phalcon\Encryption\Security\JWT\Token\Parser; diff --git a/src/Domain/Components/Encryption/Security.php b/src/Domain/Infrastructure/Encryption/Security.php similarity index 95% rename from src/Domain/Components/Encryption/Security.php rename to src/Domain/Infrastructure/Encryption/Security.php index 78ae798..3499788 100644 --- a/src/Domain/Components/Encryption/Security.php +++ b/src/Domain/Infrastructure/Encryption/Security.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Encryption; +namespace Phalcon\Api\Domain\Infrastructure\Encryption; use function password_hash; use function password_verify; diff --git a/src/Domain/Components/Encryption/TokenCache.php b/src/Domain/Infrastructure/Encryption/TokenCache.php similarity index 91% rename from src/Domain/Components/Encryption/TokenCache.php rename to src/Domain/Infrastructure/Encryption/TokenCache.php index cef096e..c974469 100644 --- a/src/Domain/Components/Encryption/TokenCache.php +++ b/src/Domain/Infrastructure/Encryption/TokenCache.php @@ -11,20 +11,18 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Encryption; +namespace Phalcon\Api\Domain\Infrastructure\Encryption; use DateTimeImmutable; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Cache\Adapter\Redis; use Phalcon\Cache\Cache; use Psr\SimpleCache\InvalidArgumentException; -use function str_replace; - /** * Small component to issue/rotate/revoke tokens and * interact with cache. diff --git a/src/Domain/Components/Encryption/TokenCacheInterface.php b/src/Domain/Infrastructure/Encryption/TokenCacheInterface.php similarity index 83% rename from src/Domain/Components/Encryption/TokenCacheInterface.php rename to src/Domain/Infrastructure/Encryption/TokenCacheInterface.php index d73fffd..238d2a9 100644 --- a/src/Domain/Components/Encryption/TokenCacheInterface.php +++ b/src/Domain/Infrastructure/Encryption/TokenCacheInterface.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Encryption; +namespace Phalcon\Api\Domain\Infrastructure\Encryption; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Psr\SimpleCache\InvalidArgumentException; interface TokenCacheInterface diff --git a/src/Domain/Components/Encryption/TokenManager.php b/src/Domain/Infrastructure/Encryption/TokenManager.php similarity index 92% rename from src/Domain/Components/Encryption/TokenManager.php rename to src/Domain/Infrastructure/Encryption/TokenManager.php index 50ef6c0..af67d06 100644 --- a/src/Domain/Components/Encryption/TokenManager.php +++ b/src/Domain/Infrastructure/Encryption/TokenManager.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Encryption; +namespace Phalcon\Api\Domain\Infrastructure\Encryption; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Encryption\Security\JWT\Token\Token; use Throwable; diff --git a/src/Domain/Components/Encryption/TokenManagerInterface.php b/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php similarity index 89% rename from src/Domain/Components/Encryption/TokenManagerInterface.php rename to src/Domain/Infrastructure/Encryption/TokenManagerInterface.php index 589dead..48386fc 100644 --- a/src/Domain/Components/Encryption/TokenManagerInterface.php +++ b/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Encryption; +namespace Phalcon\Api\Domain\Infrastructure\Encryption; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; use Phalcon\Encryption\Security\JWT\Token\Token; /** diff --git a/src/Domain/Components/Enums/Common/FlagsEnum.php b/src/Domain/Infrastructure/Enums/Common/FlagsEnum.php similarity index 82% rename from src/Domain/Components/Enums/Common/FlagsEnum.php rename to src/Domain/Infrastructure/Enums/Common/FlagsEnum.php index 0a0be5b..78c5785 100644 --- a/src/Domain/Components/Enums/Common/FlagsEnum.php +++ b/src/Domain/Infrastructure/Enums/Common/FlagsEnum.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Common; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Common; -use Phalcon\Api\Domain\Components\Enums\EnumsInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\EnumsInterface; enum FlagsEnum: int implements EnumsInterface { diff --git a/src/Domain/Components/Enums/Common/JWTEnum.php b/src/Domain/Infrastructure/Enums/Common/JWTEnum.php similarity index 92% rename from src/Domain/Components/Enums/Common/JWTEnum.php rename to src/Domain/Infrastructure/Enums/Common/JWTEnum.php index c37ff35..41a497c 100644 --- a/src/Domain/Components/Enums/Common/JWTEnum.php +++ b/src/Domain/Infrastructure/Enums/Common/JWTEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Common; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Common; enum JWTEnum: string { diff --git a/src/Domain/Components/Enums/Container/AuthDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php similarity index 90% rename from src/Domain/Components/Enums/Container/AuthDefinitionsEnum.php rename to src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php index c87e8b9..12a0f0a 100644 --- a/src/Domain/Components/Enums/Container/AuthDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Container; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthFacade; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthLoginValidator; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthSanitizer; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthTokenValidator; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Facades\AuthFacade; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthLoginValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthTokenValidator; use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Domain\Services\Auth\LogoutPostService; use Phalcon\Api\Domain\Services\Auth\RefreshPostService; diff --git a/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php similarity index 89% rename from src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php rename to src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php index b022647..0f5547f 100644 --- a/src/Domain/Components/Enums/Container/CommonDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Container; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Encryption\TokenCache; -use Phalcon\Api\Domain\Components\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; use Phalcon\Events\Manager as EventsManager; use Phalcon\Mvc\Router; diff --git a/src/Domain/Components/Enums/Container/UserDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php similarity index 90% rename from src/Domain/Components/Enums/Container/UserDefinitionsEnum.php rename to src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php index 36c950f..38e3565 100644 --- a/src/Domain/Components/Enums/Container/UserDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php @@ -11,14 +11,14 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Container; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserFacade; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; -use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; -use Phalcon\Api\Domain\Components\DataSource\User\UserValidatorUpdate; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Facades\UserFacade; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidatorUpdate; use Phalcon\Api\Domain\Services\User\UserDeleteService; use Phalcon\Api\Domain\Services\User\UserGetService; use Phalcon\Api\Domain\Services\User\UserPostService; diff --git a/src/Domain/Components/Enums/EnumsInterface.php b/src/Domain/Infrastructure/Enums/EnumsInterface.php similarity index 89% rename from src/Domain/Components/Enums/EnumsInterface.php rename to src/Domain/Infrastructure/Enums/EnumsInterface.php index b763f88..34128e6 100644 --- a/src/Domain/Components/Enums/EnumsInterface.php +++ b/src/Domain/Infrastructure/Enums/EnumsInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums; +namespace Phalcon\Api\Domain\Infrastructure\Enums; use BackedEnum; diff --git a/src/Domain/Components/Enums/Http/HttpCodesEnum.php b/src/Domain/Infrastructure/Enums/Http/HttpCodesEnum.php similarity index 97% rename from src/Domain/Components/Enums/Http/HttpCodesEnum.php rename to src/Domain/Infrastructure/Enums/Http/HttpCodesEnum.php index cdb07e7..19f561c 100644 --- a/src/Domain/Components/Enums/Http/HttpCodesEnum.php +++ b/src/Domain/Infrastructure/Enums/Http/HttpCodesEnum.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Http; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Components\Enums\EnumsInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\EnumsInterface; /** * Enumeration for Http Codes. These are not only the regular HTTP codes diff --git a/src/Domain/Components/Enums/Http/RoutesEnum.php b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php similarity index 97% rename from src/Domain/Components/Enums/Http/RoutesEnum.php rename to src/Domain/Infrastructure/Enums/Http/RoutesEnum.php index f703473..3e4bf35 100644 --- a/src/Domain/Components/Enums/Http/RoutesEnum.php +++ b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Http; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use function str_replace; diff --git a/src/Domain/Components/Enums/Input/AuthLoginInputEnum.php b/src/Domain/Infrastructure/Enums/Input/AuthLoginInputEnum.php similarity index 92% rename from src/Domain/Components/Enums/Input/AuthLoginInputEnum.php rename to src/Domain/Infrastructure/Enums/Input/AuthLoginInputEnum.php index 560c39f..700e722 100644 --- a/src/Domain/Components/Enums/Input/AuthLoginInputEnum.php +++ b/src/Domain/Infrastructure/Enums/Input/AuthLoginInputEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; diff --git a/src/Domain/Components/Enums/Input/AuthTokenInputEnum.php b/src/Domain/Infrastructure/Enums/Input/AuthTokenInputEnum.php similarity index 89% rename from src/Domain/Components/Enums/Input/AuthTokenInputEnum.php rename to src/Domain/Infrastructure/Enums/Input/AuthTokenInputEnum.php index af4d9bf..4af780a 100644 --- a/src/Domain/Components/Enums/Input/AuthTokenInputEnum.php +++ b/src/Domain/Infrastructure/Enums/Input/AuthTokenInputEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; use Phalcon\Filter\Validation\Validator\PresenceOf; diff --git a/src/Domain/Components/Enums/Input/UserInputInsertEnum.php b/src/Domain/Infrastructure/Enums/Input/UserInputInsertEnum.php similarity index 93% rename from src/Domain/Components/Enums/Input/UserInputInsertEnum.php rename to src/Domain/Infrastructure/Enums/Input/UserInputInsertEnum.php index 69b35a3..438cbde 100644 --- a/src/Domain/Components/Enums/Input/UserInputInsertEnum.php +++ b/src/Domain/Infrastructure/Enums/Input/UserInputInsertEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; diff --git a/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php b/src/Domain/Infrastructure/Enums/Input/UserInputUpdateEnum.php similarity index 87% rename from src/Domain/Components/Enums/Input/UserInputUpdateEnum.php rename to src/Domain/Infrastructure/Enums/Input/UserInputUpdateEnum.php index 3d09aeb..e4449d6 100644 --- a/src/Domain/Components/Enums/Input/UserInputUpdateEnum.php +++ b/src/Domain/Infrastructure/Enums/Input/UserInputUpdateEnum.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; -use Phalcon\Api\Domain\Components\DataSource\Validation\AbsInt; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; diff --git a/src/Domain/Components/Enums/Input/ValidatorEnumInterface.php b/src/Domain/Infrastructure/Enums/Input/ValidatorEnumInterface.php similarity index 88% rename from src/Domain/Components/Enums/Input/ValidatorEnumInterface.php rename to src/Domain/Infrastructure/Enums/Input/ValidatorEnumInterface.php index 41633d5..9c0fdba 100644 --- a/src/Domain/Components/Enums/Input/ValidatorEnumInterface.php +++ b/src/Domain/Infrastructure/Enums/Input/ValidatorEnumInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; use UnitEnum; diff --git a/src/Domain/Components/Env/Adapters/AdapterInterface.php b/src/Domain/Infrastructure/Env/Adapters/AdapterInterface.php similarity index 82% rename from src/Domain/Components/Env/Adapters/AdapterInterface.php rename to src/Domain/Infrastructure/Env/Adapters/AdapterInterface.php index bab4b30..f9ed7ff 100644 --- a/src/Domain/Components/Env/Adapters/AdapterInterface.php +++ b/src/Domain/Infrastructure/Env/Adapters/AdapterInterface.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env\Adapters; +namespace Phalcon\Api\Domain\Infrastructure\Env\Adapters; -use Phalcon\Api\Domain\Components\Env\EnvManagerTypes; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManagerTypes; /** * @phpstan-import-type TDotEnvOptions from EnvManagerTypes diff --git a/src/Domain/Components/Env/Adapters/DotEnv.php b/src/Domain/Infrastructure/Env/Adapters/DotEnv.php similarity index 85% rename from src/Domain/Components/Env/Adapters/DotEnv.php rename to src/Domain/Infrastructure/Env/Adapters/DotEnv.php index 0133a82..c349bcc 100644 --- a/src/Domain/Components/Env/Adapters/DotEnv.php +++ b/src/Domain/Infrastructure/Env/Adapters/DotEnv.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env\Adapters; +namespace Phalcon\Api\Domain\Infrastructure\Env\Adapters; use Dotenv\Dotenv as ParentDotEnv; use Exception; -use Phalcon\Api\Domain\Components\Env\EnvManagerTypes; -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManagerTypes; +use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; /** * @phpstan-import-type TDotEnvOptions from EnvManagerTypes diff --git a/src/Domain/Components/Env/EnvFactory.php b/src/Domain/Infrastructure/Env/EnvFactory.php similarity index 82% rename from src/Domain/Components/Env/EnvFactory.php rename to src/Domain/Infrastructure/Env/EnvFactory.php index 4ce3ba1..f6044a6 100644 --- a/src/Domain/Components/Env/EnvFactory.php +++ b/src/Domain/Infrastructure/Env/EnvFactory.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env; +namespace Phalcon\Api\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Components\Env\Adapters\AdapterInterface; -use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Infrastructure\Env\Adapters\AdapterInterface; +use Phalcon\Api\Domain\Infrastructure\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; class EnvFactory { diff --git a/src/Domain/Components/Env/EnvManager.php b/src/Domain/Infrastructure/Env/EnvManager.php similarity index 96% rename from src/Domain/Components/Env/EnvManager.php rename to src/Domain/Infrastructure/Env/EnvManager.php index 94fd405..4a2d214 100644 --- a/src/Domain/Components/Env/EnvManager.php +++ b/src/Domain/Infrastructure/Env/EnvManager.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env; +namespace Phalcon\Api\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Components\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; use Phalcon\Support\Collection; use function array_merge; diff --git a/src/Domain/Components/Env/EnvManagerTypes.php b/src/Domain/Infrastructure/Env/EnvManagerTypes.php similarity index 90% rename from src/Domain/Components/Env/EnvManagerTypes.php rename to src/Domain/Infrastructure/Env/EnvManagerTypes.php index 3acf713..b7347e0 100644 --- a/src/Domain/Components/Env/EnvManagerTypes.php +++ b/src/Domain/Infrastructure/Env/EnvManagerTypes.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Env; +namespace Phalcon\Api\Domain\Infrastructure\Env; /** * @phpstan-type TDotEnvOptions array{ diff --git a/src/Domain/Components/Exceptions/ExceptionTrait.php b/src/Domain/Infrastructure/Exceptions/ExceptionTrait.php similarity index 89% rename from src/Domain/Components/Exceptions/ExceptionTrait.php rename to src/Domain/Infrastructure/Exceptions/ExceptionTrait.php index 1323f88..6a39f51 100644 --- a/src/Domain/Components/Exceptions/ExceptionTrait.php +++ b/src/Domain/Infrastructure/Exceptions/ExceptionTrait.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Exceptions; +namespace Phalcon\Api\Domain\Infrastructure\Exceptions; trait ExceptionTrait { diff --git a/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php b/src/Domain/Infrastructure/Exceptions/InvalidConfigurationArgumentException.php similarity index 87% rename from src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php rename to src/Domain/Infrastructure/Exceptions/InvalidConfigurationArgumentException.php index fe2f4a0..9ecf809 100644 --- a/src/Domain/Components/Exceptions/InvalidConfigurationArgumentException.php +++ b/src/Domain/Infrastructure/Exceptions/InvalidConfigurationArgumentException.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Exceptions; +namespace Phalcon\Api\Domain\Infrastructure\Exceptions; use InvalidArgumentException; diff --git a/src/Domain/Components/Exceptions/TokenValidationException.php b/src/Domain/Infrastructure/Exceptions/TokenValidationException.php similarity index 87% rename from src/Domain/Components/Exceptions/TokenValidationException.php rename to src/Domain/Infrastructure/Exceptions/TokenValidationException.php index 1acb086..08e5284 100644 --- a/src/Domain/Components/Exceptions/TokenValidationException.php +++ b/src/Domain/Infrastructure/Exceptions/TokenValidationException.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Exceptions; +namespace Phalcon\Api\Domain\Infrastructure\Exceptions; use InvalidArgumentException; diff --git a/src/Domain/Components/Middleware/AbstractMiddleware.php b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php similarity index 94% rename from src/Domain/Components/Middleware/AbstractMiddleware.php rename to src/Domain/Infrastructure/Middleware/AbstractMiddleware.php index 6dfcee8..998b3c2 100644 --- a/src/Domain/Components/Middleware/AbstractMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Responder\ResponderTypes; use Phalcon\Domain\Payload; diff --git a/src/Domain/Components/Middleware/HealthMiddleware.php b/src/Domain/Infrastructure/Middleware/HealthMiddleware.php similarity index 87% rename from src/Domain/Components/Middleware/HealthMiddleware.php rename to src/Domain/Infrastructure/Middleware/HealthMiddleware.php index 6e01eea..9093fdd 100644 --- a/src/Domain/Components/Middleware/HealthMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/HealthMiddleware.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Middleware/NotFoundMiddleware.php b/src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php similarity index 90% rename from src/Domain/Components/Middleware/NotFoundMiddleware.php rename to src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php index 47293c2..76a31d8 100644 --- a/src/Domain/Components/Middleware/NotFoundMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Events\Event; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php similarity index 87% rename from src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php index 4580e87..3dfb26e 100644 --- a/src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php similarity index 84% rename from src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php index dbe48ad..7c08858 100644 --- a/src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php similarity index 82% rename from src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php index 780d22c..cd93845 100644 --- a/src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Http\RequestInterface; use Phalcon\Mvc\Micro; use Phalcon\Support\Registry; diff --git a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php similarity index 83% rename from src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php index 57b8b9b..dc1535a 100644 --- a/src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Exceptions\TokenValidationException; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php similarity index 85% rename from src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php rename to src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php index 04503ee..3b3ff39 100644 --- a/src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Middleware; +namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Response\Exception; diff --git a/src/Domain/Components/Providers/ErrorHandlerProvider.php b/src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php similarity index 94% rename from src/Domain/Components/Providers/ErrorHandlerProvider.php rename to src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php index 7cf3b31..1dd1ffa 100644 --- a/src/Domain/Components/Providers/ErrorHandlerProvider.php +++ b/src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Providers; +namespace Phalcon\Api\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Logger\Logger; diff --git a/src/Domain/Components/Providers/RouterProvider.php b/src/Domain/Infrastructure/Providers/RouterProvider.php similarity index 95% rename from src/Domain/Components/Providers/RouterProvider.php rename to src/Domain/Infrastructure/Providers/RouterProvider.php index c17da85..2d184ae 100644 --- a/src/Domain/Components/Providers/RouterProvider.php +++ b/src/Domain/Infrastructure/Providers/RouterProvider.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Components\Providers; +namespace Phalcon\Api\Domain\Infrastructure\Providers; use Phalcon\Api\Action\ActionHandler; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Services/Auth/AbstractAuthService.php index ac877dd..8e4dd98 100644 --- a/src/Domain/Services/Auth/AbstractAuthService.php +++ b/src/Domain/Services/Auth/AbstractAuthService.php @@ -14,8 +14,8 @@ namespace Phalcon\Api\Domain\Services\Auth; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Components\DataSource\Auth\AuthFacade; -use Phalcon\Api\Domain\Components\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Facades\AuthFacade; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; abstract class AbstractAuthService implements DomainInterface { diff --git a/src/Domain/Services/User/AbstractUserService.php b/src/Domain/Services/User/AbstractUserService.php index c3368c4..347ef53 100644 --- a/src/Domain/Services/User/AbstractUserService.php +++ b/src/Domain/Services/User/AbstractUserService.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Services\User; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Components\DataSource\User\UserFacade; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Facades\UserFacade; abstract class AbstractUserService implements DomainInterface { diff --git a/src/Responder/JsonResponder.php b/src/Responder/JsonResponder.php index 36ea2ff..d0c8bbd 100644 --- a/src/Responder/JsonResponder.php +++ b/src/Responder/JsonResponder.php @@ -14,8 +14,8 @@ namespace Phalcon\Api\Responder; use Exception as BaseException; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Domain\Payload; use Phalcon\Http\ResponseInterface; diff --git a/tests/Unit/Domain/Components/ContainerTest.php b/tests/Unit/Domain/Infrastructure/ContainerTest.php similarity index 100% rename from tests/Unit/Domain/Components/ContainerTest.php rename to tests/Unit/Domain/Infrastructure/ContainerTest.php diff --git a/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php similarity index 94% rename from tests/Unit/Domain/Components/DataSource/User/UserInputTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php index 5ee9857..8e48f75 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserInputTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; use Faker\Factory as FakerFactory; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; use Phalcon\Api\Tests\AbstractUnitTestCase; use function json_encode; diff --git a/tests/Unit/Domain/Components/DataSource/User/UserTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php similarity index 95% rename from tests/Unit/Domain/Components/DataSource/User/UserTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php index 9d35178..2c9ec2c 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; -use Phalcon\Api\Domain\Components\DataSource\User\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Tests\AbstractUnitTestCase; use function rand; diff --git a/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php similarity index 96% rename from tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php index 6052b43..8361dbf 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserMapperTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; use Faker\Factory as FakerFactory; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; use Phalcon\Api\Tests\AbstractUnitTestCase; use function json_encode; diff --git a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php similarity index 93% rename from tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php index 6c377bc..e5ef672 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserRepositoryTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\User; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; diff --git a/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php similarity index 94% rename from tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php index 481651f..a6fc885 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserSanitizerTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Filter\Filter; diff --git a/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php similarity index 86% rename from tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php index 0019146..b0b8649 100644 --- a/tests/Unit/Domain/Components/DataSource/User/UserValidatorTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; use Faker\Factory as FakerFactory; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserInput; -use Phalcon\Api\Domain\Components\DataSource\User\UserSanitizer; -use Phalcon\Api\Domain\Components\DataSource\User\UserValidator; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Filter\Validation\ValidationInterface; diff --git a/tests/Unit/Domain/Components/Encryption/JWTTokenTest.php b/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php similarity index 100% rename from tests/Unit/Domain/Components/Encryption/JWTTokenTest.php rename to tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php diff --git a/tests/Unit/Domain/Components/Encryption/SecurityTest.php b/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php similarity index 100% rename from tests/Unit/Domain/Components/Encryption/SecurityTest.php rename to tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php diff --git a/tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php similarity index 100% rename from tests/Unit/Domain/Components/Enums/Common/FlagsEnumTest.php rename to tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php diff --git a/tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php similarity index 100% rename from tests/Unit/Domain/Components/Enums/Http/HttpCodesEnumTest.php rename to tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php diff --git a/tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php similarity index 100% rename from tests/Unit/Domain/Components/Enums/Http/RoutesEnumTest.php rename to tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php diff --git a/tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php similarity index 100% rename from tests/Unit/Domain/Components/Env/Adapters/DotEnvTest.php rename to tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php diff --git a/tests/Unit/Domain/Components/Env/EnvFactoryTest.php b/tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php similarity index 100% rename from tests/Unit/Domain/Components/Env/EnvFactoryTest.php rename to tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php diff --git a/tests/Unit/Domain/Components/Env/EnvManagerTest.php b/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php similarity index 100% rename from tests/Unit/Domain/Components/Env/EnvManagerTest.php rename to tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php diff --git a/tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php similarity index 100% rename from tests/Unit/Domain/Components/Middleware/HealthMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php diff --git a/tests/Unit/Domain/Components/Middleware/NotFoundMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php similarity index 100% rename from tests/Unit/Domain/Components/Middleware/NotFoundMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php similarity index 100% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenClaimsMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php similarity index 100% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenPresenceMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php similarity index 100% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenRevokedMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php similarity index 100% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenStructureMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php diff --git a/tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php similarity index 100% rename from tests/Unit/Domain/Components/Middleware/ValidateTokenUserMiddlewareTest.php rename to tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php diff --git a/tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php similarity index 100% rename from tests/Unit/Domain/Components/Providers/CacheDataProviderTest.php rename to tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php diff --git a/tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php similarity index 100% rename from tests/Unit/Domain/Components/Providers/ErrorHandlerProviderTest.php rename to tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php diff --git a/tests/Unit/Domain/Components/Providers/RouterProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php similarity index 100% rename from tests/Unit/Domain/Components/Providers/RouterProviderTest.php rename to tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php From 5ab3633209d15fb8d943d5ab5869dd43f46a4c03 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 11 Nov 2025 11:06:48 -0600 Subject: [PATCH 203/301] [#.x] - adjusted and added more tests --- tests/AbstractUnitTestCase.php | 8 +- tests/Unit/Action/ActionHandlerTest.php | 2 +- tests/Unit/Domain/ADR/InputTest.php | 70 +++++++++ tests/Unit/Domain/ADR/PayloadTest.php | 136 ++++++++++++++++++ .../Infrastructure/Constants/CacheTest.php | 76 ++++++++++ .../Infrastructure/Constants/DatesTest.php | 50 +++++++ .../Domain/Infrastructure/ContainerTest.php | 4 +- .../DataSource/Auth/DTO/AuthInputTest.php | 52 +++++++ .../Auth/Sanitizers/AuthSanitizerTest.php | 59 ++++++++ .../Validators/AuthLoginValidatorTest.php | 69 +++++++++ .../Validators/AuthTokenValidatorTest.php | 107 ++++++++++++++ .../DataSource/User/DTO/UserInputTest.php | 3 +- .../DataSource/User/DTO/UserTest.php | 2 +- .../User/Mappers/UserMapperTest.php | 2 +- .../User/Repositories/UserRepositoryTest.php | 2 +- .../User/Sanitizers/UserSanitizerTest.php | 2 +- .../User/Validators/UserValidatorTest.php | 2 +- .../Validators/UserValidatorUpdateTest.php | 80 +++++++++++ .../DataSource/Validation/AbsIntTest.php | 77 ++++++++++ .../DataSource/Validation/ResultTest.php | 78 ++++++++++ .../Encryption/JWTTokenTest.php | 12 +- .../Encryption/SecurityTest.php | 6 +- .../Encryption/TokenCacheTest.php | 101 +++++++++++++ .../Encryption/TokenManagerTest.php | 99 +++++++++++++ .../Enums/Common/FlagsEnumTest.php | 4 +- .../Enums/Http/HttpCodesEnumTest.php | 4 +- .../Enums/Http/RoutesEnumTest.php | 6 +- .../Env/Adapters/DotEnvTest.php | 10 +- .../Infrastructure/Env/EnvFactoryTest.php | 8 +- .../Infrastructure/Env/EnvManagerTest.php | 6 +- .../Middleware/HealthMiddlewareTest.php | 4 +- .../Middleware/NotFoundMiddlewareTest.php | 6 +- .../ValidateTokenClaimsMiddlewareTest.php | 6 +- .../ValidateTokenPresenceMiddlewareTest.php | 6 +- .../ValidateTokenRevokedMiddlewareTest.php | 10 +- .../ValidateTokenStructureMiddlewareTest.php | 8 +- .../ValidateTokenUserMiddlewareTest.php | 8 +- .../Providers/CacheDataProviderTest.php | 4 +- .../Providers/ErrorHandlerProviderTest.php | 8 +- .../Providers/RouterProviderTest.php | 6 +- .../Services/Auth/LoginPostServiceTest.php | 4 +- .../Services/Auth/LogoutPostServiceTest.php | 10 +- .../Services/Auth/RefreshPostServiceTest.php | 8 +- .../Services/User/UserServiceDeleteTest.php | 2 +- .../Services/User/UserServiceDispatchTest.php | 12 +- .../Services/User/UserServiceGetTest.php | 2 +- .../Services/User/UserServicePostTest.php | 6 +- .../Services/User/UserServicePutTest.php | 6 +- tests/Unit/Responder/JsonResponderTest.php | 4 +- 49 files changed, 1155 insertions(+), 102 deletions(-) create mode 100644 tests/Unit/Domain/ADR/InputTest.php create mode 100644 tests/Unit/Domain/ADR/PayloadTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Constants/CacheTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Constants/DatesTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Validation/AbsIntTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 23fbede..381d2ee 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -16,10 +16,10 @@ use DateTimeImmutable; use Faker\Factory; use PDO; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Encryption\Security; -use Phalcon\Api\Domain\Components\Enums\Common\JWTEnum; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Encryption\Security\JWT\Builder; diff --git a/tests/Unit/Action/ActionHandlerTest.php b/tests/Unit/Action/ActionHandlerTest.php index 7ba4ae9..c7463a6 100644 --- a/tests/Unit/Action/ActionHandlerTest.php +++ b/tests/Unit/Action/ActionHandlerTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Action; use Phalcon\Api\Action\ActionHandler; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Services\ServiceFixture; diff --git a/tests/Unit/Domain/ADR/InputTest.php b/tests/Unit/Domain/ADR/InputTest.php new file mode 100644 index 0000000..a6a6431 --- /dev/null +++ b/tests/Unit/Domain/ADR/InputTest.php @@ -0,0 +1,70 @@ + + * + * 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\ADR; + +use Faker\Factory; +use Phalcon\Api\Domain\ADR\Input; +use Phalcon\Http\Request; +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\TestCase; + +#[BackupGlobals(true)] +final class InputTest extends TestCase +{ + public function testInvoke(): void + { + $faker = Factory::create(); + $postData = [ + 'post1' => $faker->word(), + 'post2' => $faker->word(), + ]; + $getData = [ + 'get1' => $faker->word(), + 'get2' => $faker->word(), + ]; + $putData = [ + 'put1' => $faker->word(), + 'put2' => $faker->word(), + ]; + + $mockRequest = $this + ->getMockBuilder(Request::class) + ->onlyMethods( + [ + 'getQuery', + 'getPost', + 'getPut', + ] + ) + ->getMock() + ; + $mockRequest->method('getQuery')->willReturn($getData); + $mockRequest->method('getPost')->willReturn($postData); + $mockRequest->method('getPut')->willReturn($putData); + + $input = new Input(); + + $expected = [ + 'get1' => $getData['get1'], + 'get2' => $getData['get2'], + 'post1' => $postData['post1'], + 'post2' => $postData['post2'], + 'put1' => $putData['put1'], + 'put2' => $putData['put2'], + ]; + $actual = $input->__invoke($mockRequest); + $this->assertSame($expected, $actual); + } + +} diff --git a/tests/Unit/Domain/ADR/PayloadTest.php b/tests/Unit/Domain/ADR/PayloadTest.php new file mode 100644 index 0000000..4350b35 --- /dev/null +++ b/tests/Unit/Domain/ADR/PayloadTest.php @@ -0,0 +1,136 @@ + + * + * 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\ADR; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class PayloadTest extends AbstractUnitTestCase +{ + public function testCreatedContainsDataAndStatusCreated(): void + { + $data = ['id' => 1]; + $payload = Payload::created($data); + + $expected = Payload::class; + $actual = get_class($payload); + $this->assertSame($expected, $actual); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['data' => $data]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testDeletedContainsDataAndStatusDeleted(): void + { + $data = ['deleted' => true]; + $payload = Payload::deleted($data); + + $expected = DomainStatus::DELETED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['data' => $data]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testUpdatedContainsDataAndStatusUpdated(): void + { + $data = ['updated' => true]; + $payload = Payload::updated($data); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['data' => $data]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testSuccessContainsDataAndStatusSuccess(): void + { + $data = ['items' => [1, 2, 3]]; + $payload = Payload::success($data); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['data' => $data]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testErrorContainsErrorsAndStatusError(): void + { + $errors = [['something' => 'went wrong']]; + $payload = Payload::error($errors); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['errors' => $errors]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testInvalidContainsErrorsAndStatusInvalid(): void + { + $errors = [['field' => 'invalid']]; + $payload = Payload::invalid($errors); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['errors' => $errors]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testUnauthorizedContainsErrorsAndStatusUnauthorized(): void + { + $errors = [['reason' => 'no access']]; + $payload = Payload::unauthorized($errors); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['errors' => $errors]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } + + public function testNotFoundReturnsDefaultErrorAndStatusNotFound(): void + { + $payload = Payload::notFound(); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $expected = ['errors' => [['Record(s) not found']]]; + $actual = $payload->getResult(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php b/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php new file mode 100644 index 0000000..30d27b6 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php @@ -0,0 +1,76 @@ + + * + * 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\Infrastructure\Constants; + +use Phalcon\Api\Domain\Infrastructure\Constants\Cache; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function sha1; +use function uniqid; + +final class CacheTest extends AbstractUnitTestCase +{ + public function testConstants(): void + { + $expected = 86400; + $actual = Cache::CACHE_LIFETIME_DAY; + $this->assertSame($expected, $actual); + + $expected = 3600; + $actual = Cache::CACHE_LIFETIME_HOUR; + $this->assertSame($expected, $actual); + + $expected = 60; + $actual = Cache::CACHE_LIFETIME_MINUTE; + $this->assertSame($expected, $actual); + + $expected = 2592000; + $actual = Cache::CACHE_LIFETIME_MONTH; + $this->assertSame($expected, $actual); + + $expected = 14400; + $actual = Cache::CACHE_TOKEN_EXPIRY; + $this->assertSame($expected, $actual); + + $token = uniqid('tok-'); + $shaToken = sha1($token); + + $newUser = $this->getNewUserData(); + $newUser['usr_id'] = 1; + $domainUser = new User( + $newUser['usr_id'], + $newUser['usr_status_flag'], + $newUser['usr_email'], + $newUser['usr_password'], + $newUser['usr_name_prefix'], + $newUser['usr_name_first'], + $newUser['usr_name_middle'], + $newUser['usr_name_last'], + $newUser['usr_name_suffix'], + $newUser['usr_issuer'], + $newUser['usr_token_password'], + $newUser['usr_token_id'], + $newUser['usr_preferences'], + $newUser['usr_created_date'], + $newUser['usr_created_usr_id'], + $newUser['usr_updated_date'], + $newUser['usr_updated_usr_id'] + ); + + $expected = 'tk-' . $domainUser->id . '-' . $shaToken; + $actual = Cache::getCacheTokenKey($domainUser, $token); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php b/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php new file mode 100644 index 0000000..2909f28 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php @@ -0,0 +1,50 @@ + + * + * 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\Infrastructure\Constants; + +use DateTimeImmutable; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class DatesTest extends AbstractUnitTestCase +{ + public function testConstants(): void + { + $expected = 'Y-m-d'; + $actual = Dates::DATE_FORMAT; + $this->assertSame($expected, $actual); + + $expected = 'NOW()'; + $actual = Dates::DATE_NOW; + $this->assertSame($expected, $actual); + + $expected = 'Y-m-d H:i:s'; + $actual = Dates::DATE_TIME_FORMAT; + $this->assertSame($expected, $actual); + + $expected = 'Y-m-d\\TH:i:sp'; + $actual = Dates::DATE_TIME_UTC_FORMAT; + $this->assertSame($expected, $actual); + + $expected = 'UTC'; + $actual = Dates::DATE_TIME_ZONE; + $this->assertSame($expected, $actual); + + $now = new DateTimeImmutable(); + $expected = $now->format('Y-m-d'); + + $actual = Dates::toUTC(format: 'Y-m-d'); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/ContainerTest.php b/tests/Unit/Domain/Infrastructure/ContainerTest.php index c40a728..643afe4 100644 --- a/tests/Unit/Domain/Infrastructure/ContainerTest.php +++ b/tests/Unit/Domain/Infrastructure/ContainerTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\Filter; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php new file mode 100644 index 0000000..14fa5bc --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php @@ -0,0 +1,52 @@ + + * + * 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\Infrastructure\DataSource\Auth\DTO; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class AuthInputTest extends AbstractUnitTestCase +{ + public function testObject(): void + { + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + $faker = FakerFactory::create(); + + // Build an input with many fields present + $input = [ + 'email' => " Foo.Bar+tag@Example.COM ", + 'password' => $faker->password(), + 'token' => $faker->password(), + ]; + + $sanitized = $sanitizer->sanitize($input); + $authInput = AuthInput::new($sanitizer, $input); + + $expected = $sanitized['email']; + $actual = $authInput->email; + $this->assertSame($expected, $actual); + + $expected = $sanitized['password']; + $actual = $authInput->password; + $this->assertSame($expected, $actual); + + $expected = $sanitized['token']; + $actual = $authInput->token; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php new file mode 100644 index 0000000..3d0d693 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php @@ -0,0 +1,59 @@ + + * + * 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\Infrastructure\DataSource\Auth\Sanitizers; + +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class AuthSanitizerTest extends AbstractUnitTestCase +{ + public function testEmpty(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new AuthSanitizer($filter); + + $expected = [ + 'email' => null, + 'password' => null, + 'token' => null, + ]; + $actual = $sanitizer->sanitize([]); + $this->assertSame($expected, $actual); + } + + public function testObject(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new AuthSanitizer($filter); + + $userData = [ + 'email' => 'John.Doe (newsletter) +spam@example.COM', + 'password' => 'some ', + 'token' => 'some ', + ]; + + $expected = [ + 'email' => 'John.Doenewsletter+spam@example.COM', + 'password' => 'some ', + 'token' => 'some ', + ]; + $actual = $sanitizer->sanitize($userData); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php new file mode 100644 index 0000000..db79869 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php @@ -0,0 +1,69 @@ + + * + * 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\Infrastructure\DataSource\Auth\Validators; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthLoginValidator; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\ValidationInterface; + +final class AuthLoginValidatorTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $input = []; + $userInput = AuthInput::new($sanitizer, $input); + + $validator = new AuthLoginValidator($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + HttpCodesEnum::AppIncorrectCredentials->error(), + ]; + + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + $faker = FakerFactory::create(); + + $input = [ + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + ]; + $userInput = AuthInput::new($sanitizer, $input); + + $validator = new AuthLoginValidator($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php new file mode 100644 index 0000000..333d74d --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php @@ -0,0 +1,107 @@ + + * + * 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\Infrastructure\DataSource\Auth\Validators; + +use Exception; +use Faker\Factory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthTokenValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCacheInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\ValidationInterface; + +final class AuthTokenValidatorTest extends AbstractUnitTestCase +{ + public function testFailureTokenNotPresent(): void + { + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + /** @var TokenManager $tokenManager */ + $tokenManager = $this->container->get(Container::JWT_TOKEN_MANAGER); + /** @var UserRepository $repository */ + $repository = $this->container->get(Container::USER_REPOSITORY); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $input = []; + $userInput = AuthInput::new($sanitizer, $input); + + $validator = new AuthTokenValidator($tokenManager, $repository, $validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + HttpCodesEnum::AppTokenNotPresent->error(), + ]; + + $this->assertSame($expected, $actual); + } + + public function testFailureTokenNotValid(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + /** @var AuthSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + /** @var UserRepository $repository */ + $repository = $this->container->get(Container::USER_REPOSITORY); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $mockJWTToken = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject' + ] + ) + ->getMock() + ; + $mockJWTToken + ->method('getObject') + ->willThrowException(new Exception('error')) + ; + + $userData = $this->getNewUserData(); + $userData['usr_id'] = rand(1, 100); + $token = $this->getUserToken($userData); + + $tokenManager = new TokenManager($tokenCache, $env, $mockJWTToken); + + $input = [ + 'token' => $token, + ]; + $userInput = AuthInput::new($sanitizer, $input); + + $validator = new AuthTokenValidator($tokenManager, $repository, $validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + HttpCodesEnum::AppTokenNotValid->error(), + ]; + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php index 8e48f75..d210cb9 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\DTO; use Faker\Factory as FakerFactory; use Phalcon\Api\Domain\Infrastructure\Container; @@ -53,7 +53,6 @@ public function testToArray(): void $sanitized = $sanitizer->sanitize($input); $userInput = UserInput::new($sanitizer, $input); - // For each property use $expected and $actual then assertSame $expected = $sanitized['id']; $actual = $userInput->id; $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php index 2c9ec2c..198d47b 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\DTO; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Tests\AbstractUnitTestCase; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php index 8361dbf..4a03d93 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Mappers; use Faker\Factory as FakerFactory; use Phalcon\Api\Domain\Infrastructure\Constants\Dates; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php index e5ef672..07d0070 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Repositories; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php index a6fc885..60964a4 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Sanitizers; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php index b0b8649..155fdd0 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Validators; use Faker\Factory as FakerFactory; use Phalcon\Api\Domain\Infrastructure\Container; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php new file mode 100644 index 0000000..afab4ff --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php @@ -0,0 +1,80 @@ + + * + * 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\Infrastructure\DataSource\User\Validators; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidatorUpdate; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\ValidationInterface; + +final class UserValidatorUpdateTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + + $input = []; + $userInput = UserInput::new($sanitizer, $input); + + $validator = new UserValidatorUpdate($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + ['Field id is not a valid absolute integer and greater than 0'], + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], + ]; + + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(Container::USER_SANITIZER); + /** @var ValidationInterface $validation */ + $validation = $this->container->get(Container::VALIDATION); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 100), + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + ]; + + $userInput = UserInput::new($sanitizer, $input); + + $validator = new UserValidatorUpdate($validation); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Validation/AbsIntTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Validation/AbsIntTest.php new file mode 100644 index 0000000..d01141b --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Validation/AbsIntTest.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\Infrastructure\DataSource\Validation; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation; +use PHPUnit\Framework\Attributes\DataProvider; + +final class AbsIntTest extends AbstractUnitTestCase +{ + /** + * @return array + */ + public static function getExamples(): array + { + return [ + [ + 'amount' => 123, + 'expected' => 0, + ], + [ + 'amount' => 123.12, + 'expected' => 0, + ], + [ + 'amount' => '-12,000', + 'expected' => 1, + ], + [ + 'amount' => '-12,0@0', + 'expected' => 1, + ], + [ + 'amount' => '-12,0@@0', + 'expected' => 1, + ], + [ + 'amount' => '123abc', + 'expected' => 1, + ], + [ + 'amount' => '123.12e3', + 'expected' => 1, + ], + ]; + } + + #[DataProvider('getExamples')] + public function testValidator( + mixed $amount, + int $expected + ): void { + $validation = new Validation(); + $validation->add('amount', new AbsInt()); + + $messages = $validation->validate( + [ + 'amount' => $amount, + ] + ); + + $actual = $messages->count(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php new file mode 100644 index 0000000..63a60bb --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php @@ -0,0 +1,78 @@ + + * + * 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\Infrastructure\DataSource\Validation; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class ResultTest extends AbstractUnitTestCase +{ + public function testSuccessIsValidAndHasNoErrors(): void + { + $result = Result::success(); + + $actual = $result->isValid(); + $this->assertTrue($actual); + + $expected = []; + $actual = $result->getErrors(); + $this->assertSame($expected, $actual); + } + + public function testErrorIsInvalidAndReturnsErrors(): void + { + $errors = ['field' => ['must not be empty']]; + $result = Result::error($errors); + + $expected = $errors; + $actual = $result->getErrors(); + $this->assertSame($expected, $actual); + } + + public function testGetMetaReturnsDefaultWhenMissing(): void + { + $result = new Result([], ['present' => 123]); + + $expected = 123; + $actual = $result->getMeta('present'); + $this->assertSame($expected, $actual); + + $actual = $result->getMeta('uknown'); + $this->assertNull($actual); + + $expected = 'default'; + $actual = $result->getMeta('unknown', 'default'); + $this->assertSame($expected, $actual); + } + + public function testSetMetaAddsOrUpdatesMeta(): void + { + $result = new Result(); + + $actual = $result->getMeta('key'); + $this->assertNull($actual); + + $result->setMeta('key', 'value'); + + $expected = 'value'; + $actual = $result->getMeta('key'); + $this->assertSame($expected, $actual); + + $result->setMeta('key', 42); + + $expected = 42; + $actual = $result->getMeta('key'); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php b/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php index 5d93a4c..5236ae0 100644 --- a/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Encryption; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Exceptions\TokenValidationException; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Exceptions\TokenValidationException; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Encryption\Security\JWT\Token\Token; diff --git a/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php b/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php index be1d7df..d505046 100644 --- a/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Encryption; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; use Phalcon\Api\Tests\AbstractUnitTestCase; final class SecurityTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php b/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php new file mode 100644 index 0000000..411ab54 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php @@ -0,0 +1,101 @@ + + * + * 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\Infrastructure\Encryption; + +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Cache\Cache; + +use function rand; +use function uniqid; + +final class TokenCacheTest extends AbstractUnitTestCase +{ + public function testStoreAndInvalidateForUser(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var Cache $cache */ + $cache = $this->container->get(Container::CACHE); + /** @var TokenCache $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + + /** + * Empty cache + */ + $cache->getAdapter()->clear(); + + /** + * Create user data + */ + $newUser = $this->getNewUserData(); + $newUser['usr_id'] = rand(1, 100); + $domainUser = new User( + $newUser['usr_id'], + $newUser['usr_status_flag'], + $newUser['usr_email'], + $newUser['usr_password'], + $newUser['usr_name_prefix'], + $newUser['usr_name_first'], + $newUser['usr_name_middle'], + $newUser['usr_name_last'], + $newUser['usr_name_suffix'], + $newUser['usr_issuer'], + $newUser['usr_token_password'], + $newUser['usr_token_id'], + $newUser['usr_preferences'], + $newUser['usr_created_date'], + $newUser['usr_created_usr_id'], + $newUser['usr_updated_date'], + $newUser['usr_updated_usr_id'] + ); + + $token1 = uniqid('tok-'); + $token2 = uniqid('tok-'); + + $tokenKey1 = CacheConstants::getCacheTokenKey($domainUser, $token1); + $tokenKey2 = CacheConstants::getCacheTokenKey($domainUser, $token2); + + /** + * Store + */ + $actual = $tokenCache->storeTokenInCache($env, $domainUser, $token1); + $this->assertTrue($actual); + $actual = $tokenCache->storeTokenInCache($env, $domainUser, $token2); + $this->assertTrue($actual); + + /** + * Check the cache + */ + $actual = $cache->has($tokenKey1); + $this->assertTrue($actual); + $actual = $cache->has($tokenKey2); + $this->assertTrue($actual); + + /** + * Invalidate + */ + $actual = $tokenCache->invalidateForUser($env, $domainUser); + $this->assertTrue($actual); + + $actual = $cache->has($tokenKey1); + $this->assertFalse($actual); + $actual = $cache->has($tokenKey2); + $this->assertFalse($actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php b/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php new file mode 100644 index 0000000..868ea0d --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php @@ -0,0 +1,99 @@ + + * + * 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\Infrastructure\Encryption; + +use Exception; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCacheInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Encryption\Security\JWT\Token\Token; + +use function rand; + +final class TokenManagerTest extends AbstractUnitTestCase +{ + public function testGetObjectTokenEmpty(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + /** @var JWTToken $jwtToken */ + $jwtToken = $this->container->get(Container::JWT_TOKEN); + + $manager = new TokenManager($tokenCache, $env, $jwtToken); + + $expected = null; + $actual = $manager->getObject(''); + $this->assertSame($expected, $actual); + + $actual = $manager->getObject(null); + $this->assertSame($expected, $actual); + } + + public function testGetObjectWithException(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + $mockJWTToken = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() + ; + $mockJWTToken + ->method('getObject') + ->willThrowException(new Exception('error')) + ; + + $manager = new TokenManager($tokenCache, $env, $mockJWTToken); + + $expected = null; + $actual = $manager->getObject(''); + $this->assertSame($expected, $actual); + + $actual = $manager->getObject(null); + $this->assertSame($expected, $actual); + } + + public function testGetObjectSuccess(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(Container::ENV); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + /** @var JWTToken $jwtToken */ + $jwtToken = $this->container->get(Container::JWT_TOKEN); + $userData = $this->getNewUserData(); + $userData['usr_id'] = rand(1, 100); + + $token = $this->getUserToken($userData); + + $manager = new TokenManager($tokenCache, $env, $jwtToken); + + $expected = Token::class; + $actual = $manager->getObject($token); + $this->assertInstanceOf($expected, $actual); + + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php index 6c95c0f..613f813 100644 --- a/tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Common/FlagsEnumTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Enums\Common; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Enums\Common; -use Phalcon\Api\Domain\Components\Enums\Common\FlagsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; final class FlagsEnumTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php index 88f0e0b..b02f254 100644 --- a/tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Http/HttpCodesEnumTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Enums\Http; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php index 42592d4..df14ca9 100644 --- a/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Enums\Http; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php index 5d40f00..e4703a7 100644 --- a/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php +++ b/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Env\Adapters; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Env\Adapters; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; use Phalcon\Api\Tests\AbstractUnitTestCase; final class DotEnvTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php b/tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php index 90aa664..c869af9 100644 --- a/tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php +++ b/tests/Unit/Domain/Infrastructure/Env/EnvFactoryTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Env; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Components\Env\Adapters\DotEnv; -use Phalcon\Api\Domain\Components\Env\EnvFactory; -use Phalcon\Api\Domain\Components\Exceptions\InvalidConfigurationArgumentException; +use Phalcon\Api\Domain\Infrastructure\Env\Adapters\DotEnv; +use Phalcon\Api\Domain\Infrastructure\Env\EnvFactory; +use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; use Phalcon\Api\Tests\AbstractUnitTestCase; final class EnvFactoryTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php b/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php index 1e8f32c..3ff8f53 100644 --- a/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Env; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php index 153a256..e8c8f2b 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\HealthMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php index 146fac8..e258055 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Components\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Middleware\NotFoundMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Events\Event; use Phalcon\Mvc\Micro; diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php index 0489971..d4bcf8c 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php index bbc0517..e5d5111 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php index 5f2de88..c0c04a6 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Cache\Cache; diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php index f7f5f99..bd96962 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php index c3459e0..c412f1d 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Middleware; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; diff --git a/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php index a7b8887..3bbbc81 100644 --- a/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Cache\Cache; diff --git a/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php index a77e9ad..6992abe 100644 --- a/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Env\EnvManager; -use Phalcon\Api\Domain\Components\Providers\ErrorHandlerProvider; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Providers\ErrorHandlerProvider; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Logger\Logger; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php index 0429c1b..d0a993b 100644 --- a/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Components\Providers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Providers\RouterProvider; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Providers\RouterProvider; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; diff --git a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php index fe69a9a..3bc5aae 100644 --- a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php @@ -15,8 +15,8 @@ use Faker\Factory; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; diff --git a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php index cf2e648..326be2b 100644 --- a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php @@ -14,11 +14,11 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Domain\Services\Auth\LogoutPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; diff --git a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php b/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php index 406b74d..fdf5937 100644 --- a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php +++ b/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php @@ -14,10 +14,10 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\Encryption\JWTToken; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Domain\Services\Auth\RefreshPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; diff --git a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php b/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php index eb3fa4d..dde46b4 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Services\User\UserDeleteService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; diff --git a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php index 9741943..128da7b 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php @@ -14,12 +14,12 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; use DateTimeImmutable; -use Phalcon\Api\Domain\Components\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Components\Constants\Dates; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\Enums\Http\RoutesEnum; -use Phalcon\Api\Domain\Components\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Cache\Cache; diff --git a/tests/Unit/Domain/Services/User/UserServiceGetTest.php b/tests/Unit/Domain/Services/User/UserServiceGetTest.php index d062474..0bcbd48 100644 --- a/tests/Unit/Domain/Services/User/UserServiceGetTest.php +++ b/tests/Unit/Domain/Services/User/UserServiceGetTest.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Services\User; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; +use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Services\User\UserGetService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Services/User/UserServicePostTest.php index b5dd795..d3a92c6 100644 --- a/tests/Unit/Domain/Services/User/UserServicePostTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePostTest.php @@ -16,9 +16,9 @@ use DateTimeImmutable; use PayloadInterop\DomainStatus; use PDOException; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; use Phalcon\Api\Domain\Services\User\UserPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Services/User/UserServicePutTest.php index 3513747..799f9a3 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Services/User/UserServicePutTest.php @@ -16,9 +16,9 @@ use DateTimeImmutable; use PayloadInterop\DomainStatus; use PDOException; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\DataSource\User\UserMapper; -use Phalcon\Api\Domain\Components\DataSource\User\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; use Phalcon\Api\Domain\Services\User\UserPutService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; diff --git a/tests/Unit/Responder/JsonResponderTest.php b/tests/Unit/Responder/JsonResponderTest.php index a31b6b8..52815b9 100644 --- a/tests/Unit/Responder/JsonResponderTest.php +++ b/tests/Unit/Responder/JsonResponderTest.php @@ -14,8 +14,8 @@ namespace Phalcon\Api\Tests\Unit\Responder; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Domain\Payload; From 172ab904d91677654fab5d43b1bce35e0198101e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 11 Nov 2025 11:07:18 -0600 Subject: [PATCH 204/301] [#.x] - adjusting references --- public/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/index.php b/public/index.php index 9d75c6d..921e41d 100644 --- a/public/index.php +++ b/public/index.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use Phalcon\Api\Domain\Components\Container; -use Phalcon\Api\Domain\Components\Providers\ErrorHandlerProvider; -use Phalcon\Api\Domain\Components\Providers\RouterProvider; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Providers\ErrorHandlerProvider; +use Phalcon\Api\Domain\Infrastructure\Providers\RouterProvider; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Mvc\Micro; From c3d657195fa77f13c81dec6037452bdca8d85ded Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Tue, 11 Nov 2025 11:17:38 -0600 Subject: [PATCH 205/301] [#.x] - updated readme --- README.md | 64 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ac5552a..db2f735 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,41 @@ Contains a handler that translate HTTP requests into Domain calls. For example, ### `Domain` layer -#### `Components` +#### `ADR` -##### `Container` +- `Payload`: A uniform result object used across Domain → Responder. +- `Input`: Class collecting request input and used to pass it to the domain +- Interfaces for domain and `Input` -The application uses the `Phalcon\Di\Di` container with minimal components lazy loaded. Each non "core" component is also registered there (i.e. domain services, responder etc.) and all necessary dependencies are injected based on the service definitions. +#### `Infrastructure` -Additionally there are two `Providers` that are also registered in the DI container for further functionality. The `ErrorHandlerProvider` which caters for the starting up/shut down of the application and error logging, and the very important `RoutesProvider` which handles registering all the routes that the application serves. +##### `Constants` + +Classes with constants and helper methods used throughout the application + +##### `DataSource` + +**`Auth`** -##### `Payload` +Contains Data Transfer Objects (DTOs) to move data from input to domain and from database back to domain. A Facade is available for orchestration, sanitizer for input as well as validators. -A uniform result object used across Domain → Responder. +**`Interfaces`** + +Mapper and Sanitizer interfaces + +**`User`** + +Contains Data Transfer Objects (DTOs) to move data from input to domain and from database back to domain. A Facade is available for orchestration, a repository for database operations, sanitizer for input as well as validators. + +**`Validation`** + +Contains the `ValidatorInterface` for all validators, a `Result` object for returning back validation results/errors and the `AbsInt` validator to check the id for `Put` operations. + +##### `Encryption` + +Contains components for JWT handling and passwords. The `Security` component is a wrapper for the `password_*` PHP classes, which are used for password hashing and verification. + +The `TokenManager` offers methods to issue, refresh and revoke tokens. It works in conjunction with the `TokenCache` to store or invalidate tokens stored in Cache (Redis) ##### `Enums` @@ -89,19 +113,15 @@ Finally, the `RoutesEnum` also holds the middleware array, which defines their e The environment manager and adapters. It reads environment variables using [DotEnv][dotenv] as the main adapter but can be extended if necessary. -##### `Validation` - -Contains the `ValidatorInterface` for all validators and a `Result` object for returning back validation results/errors. +##### `Exceptions` -##### `Encryption` +Exception classes used in the application. -Contains components for JWT handling and passwords. The `Security` component is a wrapper for the `password_*` PHP classes, which are used for password hashing and verification. - -The `TokenManager` offers methods to issue, refresh and revoke tokens. It works in conjunction with the `TokenCache` to store or invalidate tokens stored in Cache (Redis) +##### `Container` -##### `DataSource`: +The application uses the `Phalcon\Di\Di` container with minimal components lazy loaded. Each non "core" component is also registered there (i.e. domain services, responder etc.) and all necessary dependencies are injected based on the service definitions. -The `User` / `Auth` folders contain repositories, sanitizers, mappers, validators and facades for each area. +Additionally there are two `Providers` that are also registered in the DI container for further functionality. The `ErrorHandlerProvider` which caters for the starting up/shut down of the application and error logging, and the very important `RoutesProvider` which handles registering all the routes that the application serves. #### `Services`: @@ -156,13 +176,13 @@ There are several middleware registered for this application and they are being The middleware execution order is defined in the `RoutesEnum`. The available middleware is: -- [NotFoundMiddleware.php](src/Domain/Components/Middleware/NotFoundMiddleware.php) -- [HealthMiddleware.php](src/Domain/Components/Middleware/HealthMiddleware.php) -- [ValidateTokenClaimsMiddleware.php](src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php) -- [ValidateTokenPresenceMiddleware.php](src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php) -- [ValidateTokenRevokedMiddleware.php](src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php) -- [ValidateTokenStructureMiddleware.php](src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php) -- [ValidateTokenUserMiddleware.php](src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php) +- [NotFoundMiddleware.php](src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php) +- [HealthMiddleware.php](src/Domain/Infrastructure/Middleware/HealthMiddleware.php) +- [ValidateTokenClaimsMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php) +- [ValidateTokenPresenceMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php) +- [ValidateTokenRevokedMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php) +- [ValidateTokenStructureMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php) +- [ValidateTokenUserMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php) **NotFoundMiddleware** From b92f934e821fc0e15244bac887543788efe1d519 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:31:01 -0600 Subject: [PATCH 206/301] [#.x] - changing class to final --- src/Domain/Infrastructure/DataSource/Validation/AbsInt.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php b/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php index 3d79039..2344291 100644 --- a/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php +++ b/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php @@ -16,7 +16,7 @@ use Phalcon\Filter\Validation; use Phalcon\Filter\Validation\Validator\Numericality; -class AbsInt extends Numericality +final class AbsInt extends Numericality { /** * @var string|null From 80fe750a9849d93780618f1b7e84e1946b5247b8 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:32:07 -0600 Subject: [PATCH 207/301] [#.x] - correcting payload references --- .../Middleware/AbstractMiddleware.php | 5 ++-- src/Responder/JsonResponder.php | 2 +- tests/Unit/Responder/JsonResponderTest.php | 27 +++---------------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php index 998b3c2..3fed350 100644 --- a/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php @@ -13,12 +13,11 @@ namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Responder\ResponderTypes; -use Phalcon\Domain\Payload; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\RequestInterface; use Phalcon\Http\Response\Exception as ResponseException; @@ -100,7 +99,7 @@ protected function halt( 'errors' => $errors, ]; - $payload = new Payload(DomainStatus::SUCCESS, $results); + $payload = Payload::success($results); $responder->__invoke($response, $payload); } diff --git a/src/Responder/JsonResponder.php b/src/Responder/JsonResponder.php index d0c8bbd..066cae1 100644 --- a/src/Responder/JsonResponder.php +++ b/src/Responder/JsonResponder.php @@ -14,9 +14,9 @@ namespace Phalcon\Api\Responder; use Exception as BaseException; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Api\Domain\Infrastructure\Constants\Dates; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Domain\Payload; use Phalcon\Http\ResponseInterface; use function sha1; diff --git a/tests/Unit/Responder/JsonResponderTest.php b/tests/Unit/Responder/JsonResponderTest.php index 52815b9..91d82f9 100644 --- a/tests/Unit/Responder/JsonResponderTest.php +++ b/tests/Unit/Responder/JsonResponderTest.php @@ -13,12 +13,11 @@ namespace Phalcon\Api\Tests\Unit\Responder; -use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Http\ResponseInterface; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -38,17 +37,7 @@ public function testFailure(): void $responder = $this->container->get(Container::RESPONDER_JSON); $errorContent = uniqid('error-'); - $payload = new Payload( - DomainStatus::UNAUTHORIZED, - [ - 'code' => HttpCodesEnum::Unauthorized->value, - 'message' => HttpCodesEnum::Unauthorized->text(), - 'data' => [], - 'errors' => [ - [1234 => $errorContent], - ], - ] - ); + $payload = Payload::unauthorized([[1234 => $errorContent]]); ob_start(); $outputResponse = $responder->__invoke($response, $payload); @@ -115,17 +104,7 @@ public function testSuccess(): void $responder = $this->container->getShared(Container::RESPONDER_JSON); $dataContent = uniqid('data-'); - $payload = new Payload( - DomainStatus::SUCCESS, - [ - 'code' => HttpCodesEnum::OK->value, - 'message' => HttpCodesEnum::OK->text(), - 'data' => [ - $dataContent, - ], - 'errors' => [], - ] - ); + $payload = Payload::success([$dataContent]); ob_start(); $outputResponse = $responder->__invoke($response, $payload); From 5f1f742f33b83df573d05a9fffc31a6cc4a0d3ee Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:32:43 -0600 Subject: [PATCH 208/301] [#.x] - switching sanitizers to use enums --- .../DataSource/AbstractSanitizer.php | 76 ++++++++++++++++ .../Auth/Sanitizers/AuthSanitizer.php | 67 +------------- .../User/Sanitizers/UserSanitizer.php | 87 +------------------ .../Enums/Sanitizers/AuthSanitizersEnum.php | 36 ++++++++ .../Sanitizers/SanitizersEnumInterface.php | 23 +++++ .../Enums/Sanitizers/UserSanitizersEnum.php | 63 ++++++++++++++ 6 files changed, 206 insertions(+), 146 deletions(-) create mode 100644 src/Domain/Infrastructure/DataSource/AbstractSanitizer.php create mode 100644 src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php create mode 100644 src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php create mode 100644 src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php diff --git a/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php b/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php new file mode 100644 index 0000000..bd61f7d --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php @@ -0,0 +1,76 @@ + + * + * 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\Infrastructure\DataSource; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\SanitizersEnumInterface; +use Phalcon\Filter\FilterInterface; + +/** + * @phpstan-import-type TInputSanitize from InputTypes + */ +abstract class AbstractSanitizer implements SanitizerInterface +{ + protected string $enum = ''; + + public function __construct( + private readonly FilterInterface $filter, + ) { + } + + /** + * @param TInputSanitize $input + * + * @return TInputSanitize + */ + public function sanitize(array $input): array + { + $enum = $this->enum; + /** @var SanitizersEnumInterface[] $fields */ + $fields = $enum::cases(); + + /** + * Set defaults + */ + $sanitized = []; + + /** + * Sanitize all fields. The fields can be `null` meaning they + * were not defined in the input array. If the value exists + * then it will be sanitized. + * + * If there is no sanitizer defined, the value will be left intact. + */ + /** @var SanitizersEnumInterface $field */ + foreach ($fields as $field) { + $value = $input[$field->name] ?? $field->default(); + + if (null !== $value) { + $sanitizer = $field->sanitizer(); + if (true !== empty($sanitizer)) { + $value = $this->filter->sanitize($value, $sanitizer); + } + } + + $sanitized[$field->name] = $value; + } + + /** + * Return sanitized array + */ + /** @var TInputSanitize $sanitized */ + return $sanitized; + } +} diff --git a/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php b/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php index 8fde303..8f8c6ac 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php @@ -13,69 +13,10 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers; -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; -use Phalcon\Filter\Filter; -use Phalcon\Filter\FilterInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\AuthSanitizersEnum; -/** - * @phpstan-import-type TAuthInput from InputTypes - */ -final class AuthSanitizer implements SanitizerInterface +final class AuthSanitizer extends AbstractSanitizer { - public function __construct( - private readonly FilterInterface $filter, - ) { - } - - /** - * Return a sanitized array of the input - * - * @param TAuthInput $input - * - * @return TAuthInput - */ - public function sanitize(array $input): array - { - /** @var array $fields */ - $fields = [ - 'email' => null, - 'password' => null, - 'token' => null, - ]; - - /** - * Sanitize all the fields. The fields can be `null` meaning they - * were not defined with the input or a value. If the value exists - * we will sanitize it - */ - $sanitized = []; - foreach ($fields as $name => $defaultValue) { - $value = $input[$name] ?? $defaultValue; - - if (null !== $value) { - $sanitizer = $this->getSanitizer($name); - if (true !== empty($sanitizer)) { - $value = $this->filter->sanitize($value, $sanitizer); - } - } - $sanitized[$name] = $value; - } - - /** @var TAuthInput $sanitized */ - return $sanitized; - } - - /** - * @param string $name - * - * @return string - */ - private function getSanitizer(string $name): string - { - return match ($name) { - 'email' => Filter::FILTER_EMAIL, - default => '', - }; - } + protected string $enum = AuthSanitizersEnum::class; } diff --git a/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php b/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php index 762db1a..f158eb4 100644 --- a/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php @@ -13,89 +13,10 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers; -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; -use Phalcon\Filter\Filter; -use Phalcon\Filter\FilterInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\UserSanitizersEnum; -/** - * @phpstan-import-type TUserInput from InputTypes - */ -final class UserSanitizer implements SanitizerInterface +final class UserSanitizer extends AbstractSanitizer { - public function __construct( - private readonly FilterInterface $filter, - ) { - } - - /** - * Return a sanitized array of the input - * - * @param TUserInput $input - * - * @return TUserInput - */ - public function sanitize(array $input): array - { - $fields = [ - 'id' => 0, - 'status' => 0, - 'email' => null, - 'password' => null, - 'namePrefix' => null, - 'nameFirst' => null, - 'nameLast' => null, - 'nameMiddle' => null, - 'nameSuffix' => null, - 'issuer' => null, - 'tokenPassword' => null, - 'tokenId' => null, - 'preferences' => null, - 'createdDate' => null, - 'createdUserId' => 0, - 'updatedDate' => null, - 'updatedUserId' => 0, - ]; - - /** - * Sanitize all the fields. The fields can be `null` meaning they - * were not defined with the input or a value. If the value exists - * we will sanitize it - */ - $sanitized = []; - foreach ($fields as $name => $defaultValue) { - $value = $input[$name] ?? $defaultValue; - - if (null !== $value) { - $sanitizer = $this->getSanitizer($name); - if (true !== empty($sanitizer)) { - $value = $this->filter->sanitize($value, $sanitizer); - } - } - $sanitized[$name] = $value; - } - - /** @var TUserInput $sanitized */ - return $sanitized; - } - - /** - * @param string $name - * - * @return string - */ - private function getSanitizer(string $name): string - { - return match ($name) { - 'id', - 'status', - 'createdUserId', - 'updatedUserId' => Filter::FILTER_ABSINT, - 'email' => Filter::FILTER_EMAIL, - 'password', - 'tokenId', - 'tokenPassword' => '', // Password will be distorted - default => Filter::FILTER_STRING, - }; - } + protected string $enum = UserSanitizersEnum::class; } diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php b/src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php new file mode 100644 index 0000000..2018e0f --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php @@ -0,0 +1,36 @@ + + * + * 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\Infrastructure\Enums\Sanitizers; + +use Phalcon\Filter\Filter; + +enum AuthSanitizersEnum implements SanitizersEnumInterface +{ + case email; + case password; + case token; + + public function default(): mixed + { + return null; + } + + public function sanitizer(): string + { + return match ($this) { + self::email => Filter::FILTER_EMAIL, + default => '' + }; + } +} diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php b/src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php new file mode 100644 index 0000000..0738cfd --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php @@ -0,0 +1,23 @@ + + * + * 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\Infrastructure\Enums\Sanitizers; + +use UnitEnum; + +interface SanitizersEnumInterface extends UnitEnum +{ + public function default(): mixed; + + public function sanitizer(): string; +} diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php b/src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php new file mode 100644 index 0000000..ff61d68 --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php @@ -0,0 +1,63 @@ + + * + * 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\Infrastructure\Enums\Sanitizers; + +use Phalcon\Filter\Filter; + +enum UserSanitizersEnum implements SanitizersEnumInterface +{ + case id; + case status; + case email; + case password; + case namePrefix; + case nameFirst; + case nameLast; + case nameMiddle; + case nameSuffix; + case issuer; + case tokenPassword; + case tokenId; + case preferences; + case createdDate; + case createdUserId; + case updatedDate; + case updatedUserId; + + public function default(): mixed + { + return match ($this) { + self::id, + self::status, + self::createdUserId, + self::updatedUserId => 0, + default => null + }; + } + + public function sanitizer(): string + { + return match ($this) { + self::id, + self::status, + self::createdUserId, + self::updatedUserId => Filter::FILTER_ABSINT, + self::email => Filter::FILTER_EMAIL, + self::password, + self::tokenId, + self::tokenPassword => '', // Password will be distorted + default => Filter::FILTER_STRING + }; + } +} From 4db5beea33070ced67a3d20e5212636dc9447a50 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:33:38 -0600 Subject: [PATCH 209/301] [#.x] - refactoring and correcting validator references --- .../DataSource/Auth/Validators/AuthLoginValidator.php | 4 ++-- .../DataSource/Auth/Validators/AuthTokenValidator.php | 4 ++-- .../DataSource/User/Validators/UserValidator.php | 4 ++-- .../DataSource/User/Validators/UserValidatorUpdate.php | 4 ++-- .../DataSource/Validation/AbstractValidator.php | 2 +- .../AuthLoginValidatorEnum.php} | 4 ++-- .../AuthTokenValidatorEnum.php} | 4 ++-- .../UserInputInsertEnum.php => Validators/UserInsertEnum.php} | 4 ++-- .../UserInputUpdateEnum.php => Validators/UserUpdateEnum.php} | 4 ++-- .../Enums/{Input => Validators}/ValidatorEnumInterface.php | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) rename src/Domain/Infrastructure/Enums/{Input/AuthLoginInputEnum.php => Validators/AuthLoginValidatorEnum.php} (84%) rename src/Domain/Infrastructure/Enums/{Input/AuthTokenInputEnum.php => Validators/AuthTokenValidatorEnum.php} (77%) rename src/Domain/Infrastructure/Enums/{Input/UserInputInsertEnum.php => Validators/UserInsertEnum.php} (85%) rename src/Domain/Infrastructure/Enums/{Input/UserInputUpdateEnum.php => Validators/UserUpdateEnum.php} (88%) rename src/Domain/Infrastructure/Enums/{Input => Validators}/ValidatorEnumInterface.php (87%) diff --git a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php index 45489cf..a987221 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php @@ -17,11 +17,11 @@ use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Infrastructure\Enums\Input\AuthLoginInputEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\AuthLoginValidatorEnum; final class AuthLoginValidator extends AbstractValidator { - protected string $fields = AuthLoginInputEnum::class; + protected string $fields = AuthLoginValidatorEnum::class; /** * Validate a AuthInput and return an array of errors. diff --git a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php index ecd60f4..6b2ed17 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php @@ -20,13 +20,13 @@ use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Infrastructure\Enums\Input\AuthTokenInputEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\AuthTokenValidatorEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Filter\Validation\ValidationInterface; final class AuthTokenValidator extends AbstractValidator { - protected string $fields = AuthTokenInputEnum::class; + protected string $fields = AuthTokenValidatorEnum::class; public function __construct( private TokenManagerInterface $tokenManager, diff --git a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php index c34c29e..729aee7 100644 --- a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php +++ b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php @@ -14,11 +14,11 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Infrastructure\Enums\Input\UserInputInsertEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\UserInsertEnum; final class UserValidator extends AbstractValidator { use UserValidatorTrait; - protected string $fields = UserInputInsertEnum::class; + protected string $fields = UserInsertEnum::class; } diff --git a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php index a8acfaa..291eea1 100644 --- a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php +++ b/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php @@ -14,11 +14,11 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Infrastructure\Enums\Input\UserInputUpdateEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\UserUpdateEnum; final class UserValidatorUpdate extends AbstractValidator { use UserValidatorTrait; - protected string $fields = UserInputUpdateEnum::class; + protected string $fields = UserUpdateEnum::class; } diff --git a/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php index 95cae33..2048065 100644 --- a/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php +++ b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; -use Phalcon\Api\Domain\Infrastructure\Enums\Input\ValidatorEnumInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Validators\ValidatorEnumInterface; use Phalcon\Filter\Validation\ValidationInterface; use Phalcon\Filter\Validation\ValidatorInterface as PhalconValidator; diff --git a/src/Domain/Infrastructure/Enums/Input/AuthLoginInputEnum.php b/src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php similarity index 84% rename from src/Domain/Infrastructure/Enums/Input/AuthLoginInputEnum.php rename to src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php index 700e722..cc536cf 100644 --- a/src/Domain/Infrastructure/Enums/Input/AuthLoginInputEnum.php +++ b/src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; -enum AuthLoginInputEnum implements ValidatorEnumInterface +enum AuthLoginValidatorEnum implements ValidatorEnumInterface { case email; case password; diff --git a/src/Domain/Infrastructure/Enums/Input/AuthTokenInputEnum.php b/src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php similarity index 77% rename from src/Domain/Infrastructure/Enums/Input/AuthTokenInputEnum.php rename to src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php index 4af780a..70b7df4 100644 --- a/src/Domain/Infrastructure/Enums/Input/AuthTokenInputEnum.php +++ b/src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; use Phalcon\Filter\Validation\Validator\PresenceOf; -enum AuthTokenInputEnum implements ValidatorEnumInterface +enum AuthTokenValidatorEnum implements ValidatorEnumInterface { case token; diff --git a/src/Domain/Infrastructure/Enums/Input/UserInputInsertEnum.php b/src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php similarity index 85% rename from src/Domain/Infrastructure/Enums/Input/UserInputInsertEnum.php rename to src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php index 438cbde..f9a11f7 100644 --- a/src/Domain/Infrastructure/Enums/Input/UserInputInsertEnum.php +++ b/src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; -enum UserInputInsertEnum implements ValidatorEnumInterface +enum UserInsertEnum implements ValidatorEnumInterface { case email; case password; diff --git a/src/Domain/Infrastructure/Enums/Input/UserInputUpdateEnum.php b/src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php similarity index 88% rename from src/Domain/Infrastructure/Enums/Input/UserInputUpdateEnum.php rename to src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php index e4449d6..31ee31f 100644 --- a/src/Domain/Infrastructure/Enums/Input/UserInputUpdateEnum.php +++ b/src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; -enum UserInputUpdateEnum implements ValidatorEnumInterface +enum UserUpdateEnum implements ValidatorEnumInterface { case id; case email; diff --git a/src/Domain/Infrastructure/Enums/Input/ValidatorEnumInterface.php b/src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php similarity index 87% rename from src/Domain/Infrastructure/Enums/Input/ValidatorEnumInterface.php rename to src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php index 9c0fdba..3821a33 100644 --- a/src/Domain/Infrastructure/Enums/Input/ValidatorEnumInterface.php +++ b/src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Input; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; use UnitEnum; From 4278501208dee6070ed9aae14851605cfb54bfa7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:33:50 -0600 Subject: [PATCH 210/301] [#.x] - fixing typo --- .../Infrastructure/Middleware/ValidateTokenUserMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php index 3b3ff39..4dc4ec1 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php @@ -64,7 +64,7 @@ public function call(Micro $application): bool /** * If we are here everything is fine and we need to keep the user - * as a "session" user in the transport + * as a "session" user in the registry */ $registry->set('user', $domainUser); From ec8adc4653b9277ddc7c843309b078f29b9e2389 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:34:07 -0600 Subject: [PATCH 211/301] [#.x] - reformatting --- .../Infrastructure/Enums/Http/RoutesEnum.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php index 3e4bf35..32ab547 100644 --- a/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php +++ b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php @@ -22,18 +22,19 @@ */ enum RoutesEnum: int { - /** - * Methods - */ - public const DELETE = 'delete'; /** * Events */ public const EVENT_BEFORE = 'before'; public const EVENT_FINISH = 'finish'; - public const GET = 'get'; - public const POST = 'post'; - public const PUT = 'put'; + + /** + * Methods + */ + public const DELETE = 'delete'; + public const GET = 'get'; + public const POST = 'post'; + public const PUT = 'put'; case authLoginPost = 11; case authLogoutPost = 12; From e6922e2fa3c0af41acb2812936f3e83e35f0fac4 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:34:24 -0600 Subject: [PATCH 212/301] [#.x] - fixing payload reference --- src/Responder/ResponderInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Responder/ResponderInterface.php b/src/Responder/ResponderInterface.php index 7b5342a..dfaceb2 100644 --- a/src/Responder/ResponderInterface.php +++ b/src/Responder/ResponderInterface.php @@ -13,7 +13,7 @@ namespace Phalcon\Api\Responder; -use Phalcon\Domain\Payload; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Http\ResponseInterface; interface ResponderInterface From 0bc5f3d166080263fdd1e4966fb5e81d2360bcd6 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:34:34 -0600 Subject: [PATCH 213/301] [#.x] - phpcs --- .../Enums/Container/CommonDefinitionsEnum.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php index 0f5547f..c3f426f 100644 --- a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php @@ -37,19 +37,19 @@ enum CommonDefinitionsEnum public function definition(): array { return match ($this) { - self::EventsManager => [ + self::EventsManager => [ 'className' => EventsManager::class, 'calls' => [ [ - 'method' => 'enablePriorities', + 'method' => 'enablePriorities', 'arguments' => [ [ - 'type' => 'parameter', + 'type' => 'parameter', 'value' => true, - ] - ] - ] - ] + ], + ], + ], + ], ], self::JWTToken => [ 'className' => JWTToken::class, From b8a7607eaffa914c25c9b7a84437d6ae9a4bb92b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:34:45 -0600 Subject: [PATCH 214/301] [#.x] - correcting test --- tests/Unit/Action/ActionHandlerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Action/ActionHandlerTest.php b/tests/Unit/Action/ActionHandlerTest.php index c7463a6..a572428 100644 --- a/tests/Unit/Action/ActionHandlerTest.php +++ b/tests/Unit/Action/ActionHandlerTest.php @@ -39,8 +39,8 @@ public function testInvoke(): void $responder = $this->container->getShared(Container::RESPONDER_JSON); $getData = [ - 'key' => uniqid('key-'), - 'data' => [ + 'key' => uniqid('key-'), + 'value' => [ uniqid('data-'), ], ]; From e0bae456b0338b7cec0261648d0e432de1dfbfa3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:35:06 -0600 Subject: [PATCH 215/301] [#.x] - minor changes for clarity --- .../User/Repositories/UserRepository.php | 22 ++++++++++--------- .../Repositories/UserRepositoryInterface.php | 10 ++++----- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php index b57a260..7052084 100644 --- a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php +++ b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php @@ -14,8 +14,8 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories; use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; use Phalcon\DataMapper\Pdo\Connection; @@ -41,7 +41,7 @@ class UserRepository extends AbstractRepository implements UserRepositoryInterfa public function __construct( Connection $connection, - private readonly UserMapper $mapper, + private readonly MapperInterface $mapper, ) { parent::__construct($connection); } @@ -110,16 +110,17 @@ public function findOneBy(array $criteria): ?User /** - * @param TUserDbRecordOptional $user + * + * @param TUserDbRecordOptional $columns * * @return int */ - public function insert(array $user): int + public function insert(array $columns): int { $insert = Insert::new($this->connection); $insert ->into($this->table) - ->columns($user) + ->columns($columns) ->perform() ; @@ -127,20 +128,21 @@ public function insert(array $user): int } /** - * @param TUserDbRecordOptional $user + * @param int $recordId + * @param TUserDbRecordOptional $columns * * @return int */ - public function update(int $userId, array $user): int + public function update(int $recordId, array $columns): int { $update = Update::new($this->connection); $update ->table($this->table) - ->columns($user) - ->where('usr_id = ', $userId) + ->columns($columns) + ->where($this->idField . ' = ', $recordId) ->perform() ; - return $userId; + return $recordId; } } diff --git a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php index 5c72fac..a8a1572 100644 --- a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php +++ b/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php @@ -58,17 +58,17 @@ public function findById(int $recordId): ?User; public function findOneBy(array $criteria): ?User; /** - * @param TUserDbRecordOptional $user + * @param TUserDbRecordOptional $columns * * @return int */ - public function insert(array $user): int; + public function insert(array $columns): int; /** - * @param int $userId - * @param TUserDbRecordOptional $user + * @param int $recordId + * @param TUserDbRecordOptional $columns * * @return int */ - public function update(int $userId, array $user): int; + public function update(int $recordId, array $columns): int; } From 8c45d18e91fbdbe769037b5769a474fab5fb42b7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:35:21 -0600 Subject: [PATCH 216/301] [#.x] - reworked payload for better results --- src/Domain/ADR/Payload.php | 85 +++++++++++++++++++++------ tests/Unit/Domain/ADR/PayloadTest.php | 31 ++++++---- 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/src/Domain/ADR/Payload.php b/src/Domain/ADR/Payload.php index 7f1d7b7..dc211af 100644 --- a/src/Domain/ADR/Payload.php +++ b/src/Domain/ADR/Payload.php @@ -14,19 +14,26 @@ namespace Phalcon\Api\Domain\ADR; use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Responder\ResponderTypes; use Phalcon\Domain\Payload as PhalconPayload; +use function array_key_exists; +use function var_dump; + /** * @phpstan-import-type TData from ResponderTypes * @phpstan-import-type TErrors from ResponderTypes + * @phpstan-import-type TResponsePayload from ResponderTypes + * @phpstan-type TPayloadDataInput TData|TResponsePayload + * @phpstan-type TPayloadErrorInput TErrors|TResponsePayload */ final class Payload extends PhalconPayload { /** - * @param string $status - * @param TData $data - * @param TErrors $errors + * @param string $status + * @param TPayloadDataInput $data + * @param TPayloadDataInput $errors */ private function __construct( string $status, @@ -34,18 +41,8 @@ private function __construct( array $errors = [] ) { $result = []; - - if (true !== empty($data)) { - $result = [ - 'data' => $data, - ]; - } - - if (true !== empty($errors)) { - $result = [ - 'errors' => $errors, - ]; - } + $result = $this->mergePart($result, $data, 'data'); + $result = $this->mergePart($result, $errors, 'errors'); parent::__construct($status, $result); } @@ -97,7 +94,13 @@ public static function notFound(): self { return new self( status: DomainStatus::NOT_FOUND, - errors: [['Record(s) not found']] + errors: [ + 'code' => HttpCodesEnum::NotFound->value, + 'message' => HttpCodesEnum::NotFound->text(), + 'data' => [], + 'errors' => [['Record(s) not found']], + ] + ); } @@ -118,7 +121,15 @@ public static function success(array $data): self */ public static function unauthorized(array $errors): self { - return new self(status: DomainStatus::UNAUTHORIZED, errors: $errors); + return new self( + status: DomainStatus::UNAUTHORIZED, + errors:[ + 'code' => HttpCodesEnum::Unauthorized->value, + 'message' => HttpCodesEnum::Unauthorized->text(), + 'data' => [], + 'errors' => $errors, + ] + ); } /** @@ -128,6 +139,44 @@ public static function unauthorized(array $errors): self */ public static function updated(array $data): self { - return new self(DomainStatus::UPDATED, $data); + return new self( + DomainStatus::UPDATED, + [ + 'data' => $data + ] + ); } + + /** + * Merge a part into the result. If the part already contains the + * $key, assume it's a preformatted payload and return it. Otherwise + * attach the part under the $key. + * + * @param TPayloadDataInput $existing + * @param TPayloadDataInput $element + * @param string $key + * + * @return TPayloadDataInput + */ + private function mergePart( + array $existing, + array $element, + string $key + ): array { + if (empty($element)) { + return $existing; + } + + if (array_key_exists($key, $element)) { + return $element; + } + + /** + * preserve any existing keys in $existing and add the new named key + */ + $existing[$key] = $element; + + return $existing; + } + } diff --git a/tests/Unit/Domain/ADR/PayloadTest.php b/tests/Unit/Domain/ADR/PayloadTest.php index 4350b35..4ec98c7 100644 --- a/tests/Unit/Domain/ADR/PayloadTest.php +++ b/tests/Unit/Domain/ADR/PayloadTest.php @@ -15,11 +15,12 @@ use PayloadInterop\DomainStatus; use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Tests\AbstractUnitTestCase; final class PayloadTest extends AbstractUnitTestCase { - public function testCreatedContainsDataAndStatusCreated(): void + public function testCreated(): void { $data = ['id' => 1]; $payload = Payload::created($data); @@ -37,7 +38,7 @@ public function testCreatedContainsDataAndStatusCreated(): void $this->assertSame($expected, $actual); } - public function testDeletedContainsDataAndStatusDeleted(): void + public function testDeleted(): void { $data = ['deleted' => true]; $payload = Payload::deleted($data); @@ -51,7 +52,7 @@ public function testDeletedContainsDataAndStatusDeleted(): void $this->assertSame($expected, $actual); } - public function testUpdatedContainsDataAndStatusUpdated(): void + public function testUpdated(): void { $data = ['updated' => true]; $payload = Payload::updated($data); @@ -65,7 +66,7 @@ public function testUpdatedContainsDataAndStatusUpdated(): void $this->assertSame($expected, $actual); } - public function testSuccessContainsDataAndStatusSuccess(): void + public function testSuccess(): void { $data = ['items' => [1, 2, 3]]; $payload = Payload::success($data); @@ -79,7 +80,7 @@ public function testSuccessContainsDataAndStatusSuccess(): void $this->assertSame($expected, $actual); } - public function testErrorContainsErrorsAndStatusError(): void + public function testError(): void { $errors = [['something' => 'went wrong']]; $payload = Payload::error($errors); @@ -93,7 +94,7 @@ public function testErrorContainsErrorsAndStatusError(): void $this->assertSame($expected, $actual); } - public function testInvalidContainsErrorsAndStatusInvalid(): void + public function testInvalid(): void { $errors = [['field' => 'invalid']]; $payload = Payload::invalid($errors); @@ -107,7 +108,7 @@ public function testInvalidContainsErrorsAndStatusInvalid(): void $this->assertSame($expected, $actual); } - public function testUnauthorizedContainsErrorsAndStatusUnauthorized(): void + public function testUnauthorized(): void { $errors = [['reason' => 'no access']]; $payload = Payload::unauthorized($errors); @@ -116,12 +117,17 @@ public function testUnauthorizedContainsErrorsAndStatusUnauthorized(): void $actual = $payload->getStatus(); $this->assertSame($expected, $actual); - $expected = ['errors' => $errors]; + $expected = [ + 'code' => HttpCodesEnum::Unauthorized->value, + 'message' => HttpCodesEnum::Unauthorized->text(), + 'data' => [], + 'errors' => $errors + ]; $actual = $payload->getResult(); $this->assertSame($expected, $actual); } - public function testNotFoundReturnsDefaultErrorAndStatusNotFound(): void + public function testNotFound(): void { $payload = Payload::notFound(); @@ -129,7 +135,12 @@ public function testNotFoundReturnsDefaultErrorAndStatusNotFound(): void $actual = $payload->getStatus(); $this->assertSame($expected, $actual); - $expected = ['errors' => [['Record(s) not found']]]; + $expected = [ + 'code' => HttpCodesEnum::NotFound->value, + 'message' => HttpCodesEnum::NotFound->text(), + 'data' => [], + 'errors' => [['Record(s) not found']] + ]; $actual = $payload->getResult(); $this->assertSame($expected, $actual); } From c8ec7f304f3626996524b9a5bb11fcc96ed32a4a Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Thu, 13 Nov 2025 08:35:44 -0600 Subject: [PATCH 217/301] [#.x] - phpcs --- src/Domain/ADR/Payload.php | 2 -- tests/Unit/Domain/ADR/InputTest.php | 1 - .../Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php | 1 - 3 files changed, 4 deletions(-) diff --git a/src/Domain/ADR/Payload.php b/src/Domain/ADR/Payload.php index dc211af..aad2cfb 100644 --- a/src/Domain/ADR/Payload.php +++ b/src/Domain/ADR/Payload.php @@ -100,7 +100,6 @@ public static function notFound(): self 'data' => [], 'errors' => [['Record(s) not found']], ] - ); } @@ -178,5 +177,4 @@ private function mergePart( return $existing; } - } diff --git a/tests/Unit/Domain/ADR/InputTest.php b/tests/Unit/Domain/ADR/InputTest.php index a6a6431..c82ecb3 100644 --- a/tests/Unit/Domain/ADR/InputTest.php +++ b/tests/Unit/Domain/ADR/InputTest.php @@ -66,5 +66,4 @@ public function testInvoke(): void $actual = $input->__invoke($mockRequest); $this->assertSame($expected, $actual); } - } diff --git a/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php b/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php index 868ea0d..a35a98c 100644 --- a/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php @@ -94,6 +94,5 @@ public function testGetObjectSuccess(): void $expected = Token::class; $actual = $manager->getObject($token); $this->assertInstanceOf($expected, $actual); - } } From cbe8911eff8e7f509a0238ae190b6286b939fd56 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 19 Nov 2025 08:27:48 -0600 Subject: [PATCH 218/301] [#.x] - docblocks and adding the latest video in readme --- README.md | 23 +++++++++++-------- src/Domain/ADR/Payload.php | 1 - .../DataSource/User/Facades/UserFacade.php | 3 ++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index db2f735..569eadb 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,19 @@ The main takeaways that we want to convey to developers are: - Components can be swapped out with others so the use of interfaces is essential - Static analysis tools (PHPStan) must not produce errors - Code coverage for tests must be at 100% - -Part 1 https://youtu.be/f3wP_M_NFKc -Part 2 https://youtu.be/VEZvUf_PdSY -Part 3 https://youtu.be/LP64Doh0t4g -Part 4 https://youtu.be/jCEZ2WMil8Q -Part 5 https://youtu.be/syU_3cIXFMM -Part 6 https://youtu.be/AgCbqW-leCM -Part 7 https://youtu.be/tGV4pSyVLdI -Part 8 https://youtu.be/GaJhNnw_1cE -Part 9 https://youtu.be/CWofDyTdToI + +### Videos on YouTube + +- Part 01 https://youtu.be/f3wP_M_NFKc +- Part 02 https://youtu.be/VEZvUf_PdSY +- Part 03 https://youtu.be/LP64Doh0t4g +- Part 04 https://youtu.be/jCEZ2WMil8Q +- Part 05 https://youtu.be/syU_3cIXFMM +- Part 06 https://youtu.be/AgCbqW-leCM +- Part 07 https://youtu.be/tGV4pSyVLdI +- Part 08 https://youtu.be/GaJhNnw_1cE +- Part 09 https://youtu.be/CWofDyTdToI +- Part 10 https://youtu.be/8YUrGAbafaA ## Directory Structure diff --git a/src/Domain/ADR/Payload.php b/src/Domain/ADR/Payload.php index aad2cfb..68c2c83 100644 --- a/src/Domain/ADR/Payload.php +++ b/src/Domain/ADR/Payload.php @@ -19,7 +19,6 @@ use Phalcon\Domain\Payload as PhalconPayload; use function array_key_exists; -use function var_dump; /** * @phpstan-import-type TData from ResponderTypes diff --git a/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php b/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php index 574cbc7..541a719 100644 --- a/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php +++ b/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php @@ -269,7 +269,8 @@ private function cleanupFields(array $row): array } /** - * @param string $message + * @param HttpCodesEnum $item + * @param string $message * * @return Payload */ From 4ce7b5743eb9a2fe641968316f69a831aea532ef Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 19:24:43 -0600 Subject: [PATCH 219/301] [#.x] - folder rename --- src/Domain/Infrastructure/Encryption/JWTToken.php | 2 +- .../Infrastructure/Encryption/JWTTokenTest.php | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Domain/Infrastructure/Encryption/JWTToken.php b/src/Domain/Infrastructure/Encryption/JWTToken.php index a00026e..2c242c7 100644 --- a/src/Domain/Infrastructure/Encryption/JWTToken.php +++ b/src/Domain/Infrastructure/Encryption/JWTToken.php @@ -18,7 +18,7 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; diff --git a/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php b/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php index 5236ae0..69130c2 100644 --- a/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/JWTTokenTest.php @@ -13,9 +13,8 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; use Phalcon\Api\Domain\Infrastructure\Exceptions\TokenValidationException; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -29,13 +28,13 @@ public function setUp(): void { parent::setUp(); - $this->jwtToken = $this->container->get(Container::JWT_TOKEN); + $this->jwtToken = $this->container->get(JWTToken::class); } public function testGetForUserReturnsTokenString(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $userData = $this->getUserData(); $domainUser = $userMapper->domain($userData); @@ -47,7 +46,7 @@ public function testGetForUserReturnsTokenString(): void public function testGetObjectReturnsPlainToken(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $userData = $this->getUserData(); $domainUser = $userMapper->domain($userData); @@ -77,7 +76,7 @@ public function testGetObjectThrowsOnInvalidTokenStructure(): void public function testGetUserReturnsUserArray(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $userData = $this->getUserData(); $domainUser = $userMapper->domain($userData); @@ -107,7 +106,7 @@ public function testGetUserReturnsUserArray(): void public function testValidateSuccess(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $userData = $this->getUserData(); $domainUser = $userMapper->domain($userData); From c1e7f6b5a59c3c202a3959b50516b8c48ee354b3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 19:25:55 -0600 Subject: [PATCH 220/301] [#.x] - refactoring fixtures; added new fixtures --- .../Application/Command/CommandFixture.php | 11 ++++---- .../Application/Handler/HandlerFixture.php | 26 +++++++++++++++++++ .../Service}/ServiceFixture.php | 2 +- tests/Fixtures/Domain/Components/Env/.env | 4 --- 4 files changed, 32 insertions(+), 11 deletions(-) rename src/Domain/Services/User/AbstractUserService.php => tests/Fixtures/Domain/Application/Command/CommandFixture.php (51%) create mode 100644 tests/Fixtures/Domain/Application/Handler/HandlerFixture.php rename tests/Fixtures/Domain/{Services => Application/Service}/ServiceFixture.php (88%) delete mode 100644 tests/Fixtures/Domain/Components/Env/.env diff --git a/src/Domain/Services/User/AbstractUserService.php b/tests/Fixtures/Domain/Application/Command/CommandFixture.php similarity index 51% rename from src/Domain/Services/User/AbstractUserService.php rename to tests/Fixtures/Domain/Application/Command/CommandFixture.php index 347ef53..d14f56b 100644 --- a/src/Domain/Services/User/AbstractUserService.php +++ b/tests/Fixtures/Domain/Application/Command/CommandFixture.php @@ -11,18 +11,17 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\User; +namespace Phalcon\Api\Tests\Fixtures\Domain\Application\Command; -use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Facades\UserFacade; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; -abstract class AbstractUserService implements DomainInterface +final readonly class CommandFixture implements CommandInterface { /** - * @param UserFacade $facade + * @param string|null $name */ public function __construct( - protected readonly UserFacade $facade, + public ?string $name, ) { } } diff --git a/tests/Fixtures/Domain/Application/Handler/HandlerFixture.php b/tests/Fixtures/Domain/Application/Handler/HandlerFixture.php new file mode 100644 index 0000000..1aa8443 --- /dev/null +++ b/tests/Fixtures/Domain/Application/Handler/HandlerFixture.php @@ -0,0 +1,26 @@ + + * + * 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\Fixtures\Domain\Application\Handler; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; + +final readonly class HandlerFixture implements HandlerInterface +{ + public function __invoke(CommandInterface $command): Payload + { + return Payload::success(['Name: ' . $command->name]); + } +} diff --git a/tests/Fixtures/Domain/Services/ServiceFixture.php b/tests/Fixtures/Domain/Application/Service/ServiceFixture.php similarity index 88% rename from tests/Fixtures/Domain/Services/ServiceFixture.php rename to tests/Fixtures/Domain/Application/Service/ServiceFixture.php index c47933b..879c8a7 100644 --- a/tests/Fixtures/Domain/Services/ServiceFixture.php +++ b/tests/Fixtures/Domain/Application/Service/ServiceFixture.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Fixtures\Domain\Services; +namespace Phalcon\Api\Tests\Fixtures\Domain\Application\Service; use Phalcon\Api\Domain\ADR\DomainInterface; use Phalcon\Api\Domain\ADR\Payload; diff --git a/tests/Fixtures/Domain/Components/Env/.env b/tests/Fixtures/Domain/Components/Env/.env deleted file mode 100644 index b707f0d..0000000 --- a/tests/Fixtures/Domain/Components/Env/.env +++ /dev/null @@ -1,4 +0,0 @@ -SAMPLE_STRING=sample_value -SAMPLE_INT=1 -SAMPLE_TRUE=true -SAMPLE_FALSE=false From d151172cf81890757ee4d0487e5a1d0df18bf00f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 19:26:52 -0600 Subject: [PATCH 221/301] [#.x] - referencing di services by class name --- .../Middleware/AbstractMiddleware.php | 3 +- .../ValidateTokenClaimsMiddleware.php | 12 ++++---- .../ValidateTokenPresenceMiddleware.php | 2 +- .../ValidateTokenRevokedMiddleware.php | 7 +++-- .../ValidateTokenStructureMiddleware.php | 25 ++++++++-------- .../ValidateTokenUserMiddleware.php | 16 +++++----- .../ValidateTokenClaimsMiddlewareTest.php | 29 ++++++++++--------- .../ValidateTokenPresenceMiddlewareTest.php | 6 ++-- .../ValidateTokenRevokedMiddlewareTest.php | 16 +++++----- .../ValidateTokenStructureMiddlewareTest.php | 10 +++---- .../ValidateTokenUserMiddlewareTest.php | 15 +++++----- 11 files changed, 72 insertions(+), 69 deletions(-) diff --git a/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php index 3fed350..22cdba4 100644 --- a/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php @@ -16,6 +16,7 @@ use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Responder\JsonResponder; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Responder\ResponderTypes; use Phalcon\Events\Exception as EventsException; @@ -88,7 +89,7 @@ protected function halt( /** @var ResponseInterface $response */ $response = $application->getSharedService(Container::RESPONSE); /** @var ResponderInterface $responder */ - $responder = $application->getService(Container::RESPONDER_JSON); + $responder = $application->getService(JsonResponder::class); $application->stop(); diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php index 3dfb26e..7e12351 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php @@ -13,9 +13,9 @@ namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Events\Exception as EventsException; @@ -37,10 +37,10 @@ final class ValidateTokenClaimsMiddleware extends AbstractMiddleware */ public function call(Micro $application): bool { - /** @var JWTToken $jwtToken */ - $jwtToken = $application->getSharedService(Container::JWT_TOKEN); /** @var Registry $registry */ - $registry = $application->getSharedService(Container::REGISTRY); + $registry = $application->getSharedService(Registry::class); + /** @var TokenManagerInterface $tokenManager */ + $tokenManager = $application->getSharedService(TokenManager::class); /** * Get the token object @@ -61,7 +61,7 @@ public function call(Micro $application): bool * user stored in the session */ /** @var array $errors */ - $errors = $jwtToken->validate($tokenObject, $sessionUser); + $errors = $tokenManager->validate($tokenObject, $sessionUser); if (true !== empty($errors)) { $this->halt( diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php index 7c08858..e4e361b 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php @@ -35,7 +35,7 @@ public function call(Micro $application): bool /** @var Request $request */ $request = $application->getSharedService(Container::REQUEST); /** @var EnvManager $env */ - $env = $application->getSharedService(Container::ENV); + $env = $application->getSharedService(EnvManager::class); if (true === $this->isEmptyBearerToken($request, $env)) { $this->halt( diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php index cd93845..2f0a40f 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php @@ -18,6 +18,7 @@ use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Cache\Cache; use Phalcon\Http\RequestInterface; use Phalcon\Mvc\Micro; use Phalcon\Support\Registry; @@ -35,11 +36,11 @@ public function call(Micro $application): bool /** @var RequestInterface $request */ $request = $application->getSharedService(Container::REQUEST); /** @var CacheInterface $cache */ - $cache = $application->getSharedService(Container::CACHE); + $cache = $application->getSharedService(Cache::class); /** @var EnvManager $env */ - $env = $application->getSharedService(Container::ENV); + $env = $application->getSharedService(EnvManager::class); /** @var Registry $registry */ - $registry = $application->getSharedService(Container::REGISTRY); + $registry = $application->getSharedService(Registry::class); /** @var User $domainUser */ $domainUser = $registry->get('user'); diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php index dc1535a..fdb9db6 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php @@ -14,10 +14,10 @@ namespace Phalcon\Api\Domain\Infrastructure\Middleware; use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; -use Phalcon\Api\Domain\Infrastructure\Exceptions\TokenValidationException; use Phalcon\Events\Exception as EventsException; use Phalcon\Http\Request; use Phalcon\Http\Response\Exception; @@ -38,22 +38,21 @@ public function call(Micro $application): bool /** @var Request $request */ $request = $application->getSharedService(Container::REQUEST); /** @var EnvManager $env */ - $env = $application->getSharedService(Container::ENV); - /** @var JWTToken $jwtToken */ - $jwtToken = $application->getSharedService(Container::JWT_TOKEN); + $env = $application->getSharedService(EnvManager::class); + /** @var TokenManagerInterface $tokenManager */ + $tokenManager = $application->getSharedService(TokenManager::class); - try { - $token = $jwtToken->getObject( - $this->getBearerTokenFromHeader($request, $env) - ); - } catch (TokenValidationException $ex) { + $hederToken = $this->getBearerTokenFromHeader($request, $env); + $tokenObject = $tokenManager->getObject($hederToken); + + if (null === $tokenObject) { $this->halt( $application, HttpCodesEnum::Unauthorized->value, HttpCodesEnum::Unauthorized->text(), [], [ - [$ex->getCode() => $ex->getMessage()], + [$tokenManager->getErrorMessage()], ] ); @@ -64,8 +63,8 @@ public function call(Micro $application): bool * If we are down here the token is an object and is valid */ /** @var Registry $registry */ - $registry = $application->getSharedService(Container::REGISTRY); - $registry->set('token', $token); + $registry = $application->getSharedService(Registry::class); + $registry->set('token', $tokenObject); return true; } diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php index 4dc4ec1..e0899dd 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php @@ -13,9 +13,9 @@ namespace Phalcon\Api\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; -use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Events\Exception as EventsException; @@ -36,19 +36,19 @@ final class ValidateTokenUserMiddleware extends AbstractMiddleware */ public function call(Micro $application): bool { - /** @var JWTToken $jwtToken */ - $jwtToken = $application->getSharedService(Container::JWT_TOKEN); /** @var UserRepository $repository */ - $repository = $application->getSharedService(Container::USER_REPOSITORY); + $repository = $application->getSharedService(UserRepository::class); /** @var Registry $registry */ - $registry = $application->getSharedService(Container::REGISTRY); + $registry = $application->getSharedService(Registry::class); + /** @var TokenManagerInterface $tokenManager */ + $tokenManager = $application->getSharedService(TokenManager::class); /** * Get the token object */ /** @var Token $tokenObject */ $tokenObject = $registry->get('token'); - $domainUser = $jwtToken->getUser($repository, $tokenObject); + $domainUser = $tokenManager->getUser($repository, $tokenObject); if (null === $domainUser) { $this->halt( diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php index d4bcf8c..5cad5e3 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddlewareTest.php @@ -13,8 +13,9 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenClaimsMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; @@ -69,25 +70,25 @@ public function testValidateTokenClaimsFailure( array $expectedErrors ): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); - [$micro, $middleware, $jwtToken] = $this->setupTest(); + [$micro, $middleware, $tokenManager] = $this->setupTest(); /** * Make the signature non valid */ $tokenUser = array_replace($user, $userData); $token = $this->getUserToken($tokenUser); - $tokenObject = $jwtToken->getObject($token); + $tokenObject = $tokenManager->getObject($token); $domainUser = $userMapper->domain($user); /** * Store the user in the registry */ /** @var Registry $registry */ - $registry = $this->container->get(Container::REGISTRY); + $registry = $this->container->get(Registry::class); $registry->set('user', $domainUser); $registry->set('token', $tokenObject); @@ -117,21 +118,21 @@ public function testValidateTokenClaimsFailure( public function testValidateTokenClaimsSuccess(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); $tokenUser = $userMapper->domain($user); - [$micro, $middleware, $jwtToken] = $this->setupTest(); + [$micro, $middleware, $tokenManager] = $this->setupTest(); $token = $this->getUserToken($user); - $tokenObject = $jwtToken->getObject($token); + $tokenObject = $tokenManager->getObject($token); /** * Store the user in the registry */ /** @var Registry $registry */ - $registry = $this->container->get(Container::REGISTRY); + $registry = $this->container->get(Registry::class); $registry->set('user', $tokenUser); $registry->set('token', $tokenObject); @@ -155,10 +156,10 @@ public function testValidateTokenClaimsSuccess(): void */ private function setupTest(): array { - $micro = new Micro($this->container); - $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_CLAIMS); - $jwtToken = $this->container->getShared(Container::JWT_TOKEN); + $micro = new Micro($this->container); + $middleware = $this->container->get(ValidateTokenClaimsMiddleware::class); + $tokenManager = $this->container->getShared(TokenManager::class); - return [$micro, $middleware, $jwtToken]; + return [$micro, $middleware, $tokenManager]; } } diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php index e5d5111..b81f66b 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddlewareTest.php @@ -13,8 +13,8 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenPresenceMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -25,7 +25,7 @@ final class ValidateTokenPresenceMiddlewareTest extends AbstractUnitTestCase public function testValidateTokenPresenceFailure(): void { $micro = new Micro($this->container); - $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_PRESENCE); + $middleware = $this->container->get(ValidateTokenPresenceMiddleware::class); /** * No token exception @@ -54,7 +54,7 @@ public function testValidateTokenPresenceFailure(): void public function testValidateTokenPresenceSuccess(): void { $micro = new Micro($this->container); - $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_PRESENCE); + $middleware = $this->container->get(ValidateTokenPresenceMiddleware::class); /** * No token return diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php index c0c04a6..4d8baee 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddlewareTest.php @@ -14,9 +14,9 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenRevokedMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Cache\Cache; @@ -30,7 +30,7 @@ final class ValidateTokenRevokedMiddlewareTest extends AbstractUnitTestCase public function testValidateTokenRevokedFailureInvalidToken(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); $tokenUser = $userMapper->domain($user); @@ -43,7 +43,7 @@ public function testValidateTokenRevokedFailureInvalidToken(): void * Store the user in the registry */ /** @var Registry $registry */ - $registry = $this->container->get(Container::REGISTRY); + $registry = $this->container->get(Registry::class); $registry->set('user', $tokenUser); // There is no entry in the cache for this token, so this should fail. @@ -73,7 +73,7 @@ public function testValidateTokenRevokedFailureInvalidToken(): void public function testValidateTokenRevokedSuccess(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); $tokenUser = $userMapper->domain($user); @@ -86,11 +86,11 @@ public function testValidateTokenRevokedSuccess(): void * Store the user in the registry */ /** @var Registry $registry */ - $registry = $this->container->get(Container::REGISTRY); + $registry = $this->container->get(Registry::class); $registry->set('user', $tokenUser); /** @var Cache $cache */ - $cache = $micro->getSharedService(Container::CACHE); + $cache = $micro->getSharedService(Cache::class); $sessionUser = $registry->get('user'); $cacheKey = CacheConstants::getCacheTokenKey($sessionUser, $token); $payload = [ @@ -119,7 +119,7 @@ public function testValidateTokenRevokedSuccess(): void private function setupTest(): array { $micro = new Micro($this->container); - $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_REVOKED); + $middleware = $this->container->get(ValidateTokenRevokedMiddleware::class); return [$micro, $middleware]; } diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php index bd96962..51d5038 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddlewareTest.php @@ -13,9 +13,9 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenStructureMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Mvc\Micro; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -84,13 +84,13 @@ public function testValidateTokenStructureFailureNoDots(): void public function testValidateTokenStructureSuccess(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $userData = $this->getNewUserData(); $domainUser = $userMapper->domain($userData); [$micro, $middleware] = $this->setupTest(); /** @var JWTToken $jwtToken */ - $jwtToken = $micro->getSharedService(Container::JWT_TOKEN); + $jwtToken = $micro->getSharedService(JWTToken::class); $token = $jwtToken->getForUser($domainUser); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); @@ -114,7 +114,7 @@ public function testValidateTokenStructureSuccess(): void private function setupTest(): array { $micro = new Micro($this->container); - $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE); + $middleware = $this->container->get(ValidateTokenStructureMiddleware::class); return [$micro, $middleware]; } diff --git a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php index c412f1d..799a083 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/ValidateTokenUserMiddlewareTest.php @@ -13,9 +13,10 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Middleware; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenUserMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Mvc\Micro; @@ -33,7 +34,7 @@ public function testValidateTokenUserFailureRecordNotFound(): void $token = $this->getUserToken($userData); $tokenObject = $jwtToken->getObject($token); /** @var Registry $registry */ - $registry = $this->container->get(Container::REGISTRY); + $registry = $this->container->get(Registry::class); $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); @@ -62,7 +63,7 @@ public function testValidateTokenUserFailureRecordNotFound(): void public function testValidateTokenUserSuccess(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $migration = new UsersMigration($this->getConnection()); $user = $this->getNewUser($migration); $domainUser = $userMapper->domain($user); @@ -72,7 +73,7 @@ public function testValidateTokenUserSuccess(): void $token = $jwtToken->getForUser($domainUser); $tokenObject = $jwtToken->getObject($token); /** @var Registry $registry */ - $registry = $this->container->get(Container::REGISTRY); + $registry = $this->container->get(Registry::class); $registry->set('token', $tokenObject); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); @@ -96,8 +97,8 @@ public function testValidateTokenUserSuccess(): void private function setupTest(): array { $micro = new Micro($this->container); - $middleware = $this->container->get(Container::MIDDLEWARE_VALIDATE_TOKEN_USER); - $jwtToken = $this->container->get(Container::JWT_TOKEN); + $middleware = $this->container->get(ValidateTokenUserMiddleware::class); + $jwtToken = $this->container->get(JWTToken::class); return [$micro, $middleware, $jwtToken]; } From c1685ed5d8484917d2d027e0f5fa0207c455dc6e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 19:31:35 -0600 Subject: [PATCH 222/301] [#.x] - referencing di services by class name; phpcs --- src/Domain/ADR/Payload.php | 10 +- .../User/Service}/UserPostService.php | 2 +- .../User/Service}/UserPutService.php | 2 +- .../DataSource/AbstractSanitizer.php | 4 +- .../DataSource/AbstractValueObject.php | 2 +- .../AuthSanitizer.php | 6 +- .../DataSource/Interfaces/MapperInterface.php | 49 --- .../Interfaces/SanitizerInterface.php | 31 -- .../DataSource/User/DTO/User.php | 36 +- .../DataSource/User/Facades/UserFacade.php | 351 ------------------ .../User/{Mappers => Mapper}/UserMapper.php | 10 +- .../UserRepository.php | 4 +- .../UserRepositoryInterface.php | 2 +- .../UserSanitizer.php | 6 +- .../UserValidator.php | 4 +- .../UserValidatorUpdate.php | 4 +- .../DataSource/Validation/Result.php | 2 +- .../Infrastructure/Enums/Http/RoutesEnum.php | 52 ++- .../AuthSanitizerEnum.php} | 4 +- .../UserSanitizerEnum.php} | 4 +- .../UserUpdateEnum.php | 19 +- .../Providers/ErrorHandlerProvider.php | 4 +- .../Providers/RouterProvider.php | 3 +- tests/AbstractUnitTestCase.php | 4 +- tests/Unit/Action/ActionHandlerTest.php | 5 +- tests/Unit/Domain/ADR/InputTest.php | 8 +- tests/Unit/Domain/ADR/PayloadTest.php | 100 ++--- .../Auth/Service}/LoginPostServiceTest.php | 21 +- .../Auth/Service}/LogoutPostServiceTest.php | 47 ++- .../Auth/Service}/RefreshPostServiceTest.php | 41 +- .../User/Service}/UserServiceDeleteTest.php | 11 +- .../User/Service}/UserServiceDispatchTest.php | 11 +- .../User/Service}/UserServiceGetTest.php | 13 +- .../User/Service}/UserServicePostTest.php | 76 ++-- .../User/Service}/UserServicePutTest.php | 120 +++--- .../Infrastructure/Constants/CacheTest.php | 8 +- .../Infrastructure/Constants/DatesTest.php | 2 +- .../DataSource/Auth/DTO/AuthInputTest.php | 5 +- .../AuthSanitizerTest.php | 5 +- .../DataSource/User/DTO/UserInputTest.php | 10 +- .../{Mappers => Mapper}/UserMapperTest.php | 63 ++-- .../UserRepositoryTest.php | 9 +- .../UserSanitizerTest.php | 4 +- .../Validator/UserValidatorUpdateTest.php | 88 +++++ .../Validators/UserValidatorUpdateTest.php | 80 ---- .../DataSource/Validation/ResultTest.php | 28 +- .../Encryption/SecurityTest.php | 3 +- .../Encryption/TokenCacheTest.php | 7 +- .../Enums/Http/RoutesEnumTest.php | 43 ++- .../Env/Adapters/DotEnvTest.php | 5 +- .../Infrastructure/Env/EnvManagerTest.php | 19 +- .../Middleware/HealthMiddlewareTest.php | 2 +- .../Middleware/NotFoundMiddlewareTest.php | 4 +- .../Providers/CacheDataProviderTest.php | 3 +- .../Providers/ErrorHandlerProviderTest.php | 8 +- tests/Unit/Responder/JsonResponderTest.php | 7 +- 56 files changed, 565 insertions(+), 906 deletions(-) rename src/Domain/{Services/User => Application/User/Service}/UserPostService.php (92%) rename src/Domain/{Services/User => Application/User/Service}/UserPutService.php (92%) rename src/Domain/Infrastructure/DataSource/Auth/{Sanitizers => Sanitizer}/AuthSanitizer.php (76%) delete mode 100644 src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php delete mode 100644 src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php delete mode 100644 src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php rename src/Domain/Infrastructure/DataSource/User/{Mappers => Mapper}/UserMapper.php (91%) rename src/Domain/Infrastructure/DataSource/User/{Repositories => Repository}/UserRepository.php (97%) rename src/Domain/Infrastructure/DataSource/User/{Repositories => Repository}/UserRepositoryInterface.php (99%) rename src/Domain/Infrastructure/DataSource/User/{Sanitizers => Sanitizer}/UserSanitizer.php (76%) rename src/Domain/Infrastructure/DataSource/User/{Validators => Validator}/UserValidator.php (87%) rename src/Domain/Infrastructure/DataSource/User/{Validators => Validator}/UserValidatorUpdate.php (87%) rename src/Domain/Infrastructure/Enums/{Sanitizers/AuthSanitizersEnum.php => Sanitizer/AuthSanitizerEnum.php} (82%) rename src/Domain/Infrastructure/Enums/{Sanitizers/UserSanitizersEnum.php => Sanitizer/UserSanitizerEnum.php} (91%) rename src/Domain/Infrastructure/Enums/{Validators => Validator}/UserUpdateEnum.php (69%) rename tests/Unit/Domain/{Services/Auth => Application/Auth/Service}/LoginPostServiceTest.php (87%) rename tests/Unit/Domain/{Services/Auth => Application/Auth/Service}/LogoutPostServiceTest.php (85%) rename tests/Unit/Domain/{Services/Auth => Application/Auth/Service}/RefreshPostServiceTest.php (85%) rename tests/Unit/Domain/{Services/User => Application/User/Service}/UserServiceDeleteTest.php (87%) rename tests/Unit/Domain/{Services/User => Application/User/Service}/UserServiceDispatchTest.php (88%) rename tests/Unit/Domain/{Services/User => Application/User/Service}/UserServiceGetTest.php (91%) rename tests/Unit/Domain/{Services/User => Application/User/Service}/UserServicePostTest.php (81%) rename tests/Unit/Domain/{Services/User => Application/User/Service}/UserServicePutTest.php (79%) rename tests/Unit/Domain/Infrastructure/DataSource/Auth/{Sanitizers => Sanitizer}/AuthSanitizerTest.php (89%) rename tests/Unit/Domain/Infrastructure/DataSource/User/{Mappers => Mapper}/UserMapperTest.php (82%) rename tests/Unit/Domain/Infrastructure/DataSource/User/{Repositories => Repository}/UserRepositoryTest.php (92%) rename tests/Unit/Domain/Infrastructure/DataSource/User/{Sanitizers => Sanitizer}/UserSanitizerTest.php (97%) create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdateTest.php delete mode 100644 tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php diff --git a/src/Domain/ADR/Payload.php b/src/Domain/ADR/Payload.php index 68c2c83..13d45be 100644 --- a/src/Domain/ADR/Payload.php +++ b/src/Domain/ADR/Payload.php @@ -30,8 +30,8 @@ final class Payload extends PhalconPayload { /** - * @param string $status - * @param TPayloadDataInput $data + * @param string $status + * @param TPayloadDataInput $data * @param TPayloadDataInput $errors */ private function __construct( @@ -121,7 +121,7 @@ public static function unauthorized(array $errors): self { return new self( status: DomainStatus::UNAUTHORIZED, - errors:[ + errors: [ 'code' => HttpCodesEnum::Unauthorized->value, 'message' => HttpCodesEnum::Unauthorized->text(), 'data' => [], @@ -140,7 +140,7 @@ public static function updated(array $data): self return new self( DomainStatus::UPDATED, [ - 'data' => $data + 'data' => $data, ] ); } @@ -152,7 +152,7 @@ public static function updated(array $data): self * * @param TPayloadDataInput $existing * @param TPayloadDataInput $element - * @param string $key + * @param string $key * * @return TPayloadDataInput */ diff --git a/src/Domain/Services/User/UserPostService.php b/src/Domain/Application/User/Service/UserPostService.php similarity index 92% rename from src/Domain/Services/User/UserPostService.php rename to src/Domain/Application/User/Service/UserPostService.php index 91928c0..0710eda 100644 --- a/src/Domain/Services/User/UserPostService.php +++ b/src/Domain/Application/User/Service/UserPostService.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\User; +namespace Phalcon\Api\Domain\Application\User\Service; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; diff --git a/src/Domain/Services/User/UserPutService.php b/src/Domain/Application/User/Service/UserPutService.php similarity index 92% rename from src/Domain/Services/User/UserPutService.php rename to src/Domain/Application/User/Service/UserPutService.php index cb74dd7..3ff3152 100644 --- a/src/Domain/Services/User/UserPutService.php +++ b/src/Domain/Application/User/Service/UserPutService.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\User; +namespace Phalcon\Api\Domain\Application\User\Service; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; diff --git a/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php b/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php index bd61f7d..18f2bb8 100644 --- a/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php @@ -14,8 +14,8 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; -use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\SanitizersEnumInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\SanitizersEnumInterface; use Phalcon\Filter\FilterInterface; /** diff --git a/src/Domain/Infrastructure/DataSource/AbstractValueObject.php b/src/Domain/Infrastructure/DataSource/AbstractValueObject.php index b3be385..f0f7e0d 100644 --- a/src/Domain/Infrastructure/DataSource/AbstractValueObject.php +++ b/src/Domain/Infrastructure/DataSource/AbstractValueObject.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerInterface; /** * Base factory for value objects such as input DTOs. diff --git a/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php b/src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizer.php similarity index 76% rename from src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php rename to src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizer.php index 8f8c6ac..c3a6da2 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizer.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer; use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; -use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\AuthSanitizersEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\AuthSanitizerEnum; final class AuthSanitizer extends AbstractSanitizer { - protected string $enum = AuthSanitizersEnum::class; + protected string $enum = AuthSanitizerEnum::class; } diff --git a/src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php b/src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php deleted file mode 100644 index 4392806..0000000 --- a/src/Domain/Infrastructure/DataSource/Interfaces/MapperInterface.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * 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\Infrastructure\DataSource\Interfaces; - -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; - -/** - * Contract for mapping between domain DTO/objects and persistence arrays. - * - * @phpstan-import-type TUser from UserTypes - * @phpstan-import-type TUserDbRecord from UserTypes - * @phpstan-import-type TUserDomainToDbRecord from UserTypes - */ -interface MapperInterface -{ - /** - * Map Domain User -> DB row (usr_*) - * - * @return TUserDomainToDbRecord - */ - public function db(UserInput $user): array; - - /** - * Map DB row (usr_*) -> Domain User - * - * @param TUserDbRecord|array{} $row - */ - public function domain(array $row): User; - - /** - * Map input row -> Domain User - * - * @param TUser $row - */ - public function input(array $row): User; -} diff --git a/src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php b/src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php deleted file mode 100644 index 543430d..0000000 --- a/src/Domain/Infrastructure/DataSource/Interfaces/SanitizerInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * 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\Infrastructure\DataSource\Interfaces; - -use Phalcon\Api\Domain\ADR\InputTypes; - -/** - * @phpstan-import-type TInputSanitize from InputTypes - */ -interface SanitizerInterface -{ - /** - * Return a sanitized array of the input - * - * @param TInputSanitize $input - * - * @return TInputSanitize - */ - public function sanitize(array $input): array; -} diff --git a/src/Domain/Infrastructure/DataSource/User/DTO/User.php b/src/Domain/Infrastructure/DataSource/User/DTO/User.php index ef3cb39..1c14fd3 100644 --- a/src/Domain/Infrastructure/DataSource/User/DTO/User.php +++ b/src/Domain/Infrastructure/DataSource/User/DTO/User.php @@ -21,26 +21,26 @@ /** * @phpstan-import-type TUser from UserTypes */ -final class User +final readonly class User { public function __construct( - public readonly int $id, - public readonly int $status, - public readonly string $email, - public readonly string $password, - public readonly ?string $namePrefix, - public readonly ?string $nameFirst, - public readonly ?string $nameMiddle, - public readonly ?string $nameLast, - public readonly ?string $nameSuffix, - public readonly ?string $issuer, - public readonly ?string $tokenPassword, - public readonly ?string $tokenId, - public readonly ?string $preferences, - public readonly ?string $createdDate, - public readonly ?int $createdUserId, - public readonly ?string $updatedDate, - public readonly ?int $updatedUserId, + public int $id, + public int $status, + public string $email, + public string $password, + public ?string $namePrefix, + public ?string $nameFirst, + public ?string $nameMiddle, + public ?string $nameLast, + public ?string $nameSuffix, + public ?string $issuer, + public ?string $tokenPassword, + public ?string $tokenId, + public ?string $preferences, + public ?string $createdDate, + public ?int $createdUserId, + public ?string $updatedDate, + public ?int $updatedUserId, ) { } diff --git a/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php b/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php deleted file mode 100644 index 541a719..0000000 --- a/src/Domain/Infrastructure/DataSource/User/Facades/UserFacade.php +++ /dev/null @@ -1,351 +0,0 @@ - - * - * 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\Infrastructure\DataSource\User\Facades; - -use PDOException; -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\ADR\Payload; -use Phalcon\Api\Domain\Infrastructure\Constants\Dates; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; -use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; -use Phalcon\Api\Domain\Infrastructure\Encryption\Security; -use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; - -use function array_filter; - -/** - * Orchestration for workflow - * - * - Sanitization - * - DTO creation - * - Validation - * - Pre-operation checks (when necessary) - * - Repository operation - * - * @phpstan-import-type TUserInput from InputTypes - * @phpstan-import-type TUserDomainToDbRecord from UserTypes - * @phpstan-import-type TUserDbRecordOptional from UserTypes - */ -final class UserFacade -{ - /** - * @param SanitizerInterface $sanitizer - * @param ValidatorInterface $validator - * @param MapperInterface $mapper - * @param UserRepositoryInterface $repository - * @param Security $security - */ - public function __construct( - private readonly SanitizerInterface $sanitizer, - private readonly ValidatorInterface $validator, - private readonly MapperInterface $mapper, - private readonly UserRepositoryInterface $repository, - private readonly Security $security, - ) { - } - - /** - * Delete a user. - * - * @param TUserInput $input - * - * @return Payload - */ - public function delete(array $input): Payload - { - $dto = UserInput::new($this->sanitizer, $input); - $userId = $dto->id; - - /** - * Success - */ - if ($userId > 0) { - $rowCount = $this->repository->deleteById($userId); - - if (0 !== $rowCount) { - return Payload::deleted( - [ - 'Record deleted successfully [#' . $userId . '].', - ], - ); - } - } - - /** - * 404 - */ - return Payload::notFound(); - } - - /** - * Get a user. - * - * @param TUserInput $input - * - * @return Payload - */ - public function get(array $input): Payload - { - $dto = UserInput::new($this->sanitizer, $input); - $userId = $dto->id; - - /** - * Success - */ - if ($userId > 0) { - $user = $this->repository->findById($userId); - - if (null !== $user) { - return Payload::success([$user->id => $user->toArray()]); - } - } - - /** - * 404 - */ - return Payload::notFound(); - } - - /** - * Create a user. - * - * @param TUserInput $input - * - * @return Payload - */ - public function insert(array $input): Payload - { - $dto = UserInput::new($this->sanitizer, $input); - - $validation = $this->validator->validate($dto); - if (!$validation->isValid()) { - return Payload::invalid($validation->getErrors()); - } - - /** - * Array for inserting - */ - $user = $this->mapper->db($dto); - - /** - * Pre-insert checks and manipulations - */ - $user = $this->preInsert($user); - - /** - * Insert the record - */ - try { - $userId = $this->repository->insert($user); - } catch (PDOException $ex) { - /** - * @todo send generic response and log the error - */ - return $this->getErrorPayload( - HttpCodesEnum::AppCannotCreateDatabaseRecord, - $ex->getMessage() - ); - } - - if ($userId < 1) { - return $this->getErrorPayload( - HttpCodesEnum::AppCannotCreateDatabaseRecord, - 'No id returned' - ); - } - - /** - * Get the user from the database - */ - /** @var User $domainUser */ - $domainUser = $this->repository->findById($userId); - - /** - * Return the user back - */ - return Payload::created([$domainUser->id => $domainUser->toArray()]); - } - - /** - * Create a user. - * - * @param TUserInput $input - * - * @return Payload - */ - public function update(array $input): Payload - { - $dto = UserInput::new($this->sanitizer, $input); - - $validation = $this->validator->validate($dto); - if (!$validation->isValid()) { - return Payload::invalid($validation->getErrors()); - } - - /** - * Check if the user exists, If not, return an error - */ - /** @var int $userId */ - $userId = $dto->id; - $domainUser = $this->repository->findById($userId); - - if (null === $domainUser) { - return Payload::notFound(); - } - - /** - * Array for updating - */ - $user = $this->mapper->db($dto); - - /** - * Pre-update checks and manipulations - */ - $user = $this->preUpdate($user); - - /** - * Update the record - */ - try { - $userId = $this->repository->update($userId, $user); - } catch (PDOException $ex) { - /** - * @todo send generic response and log the error - */ - return $this->getErrorPayload( - HttpCodesEnum::AppCannotUpdateDatabaseRecord, - $ex->getMessage() - ); - } - - if ($userId < 1) { - return $this->getErrorPayload( - HttpCodesEnum::AppCannotUpdateDatabaseRecord, - 'No id returned' - ); - } - - /** - * Get the user from the database - */ - /** @var User $domainUser */ - $domainUser = $this->repository->findById($userId); - - /** - * Return the user back - */ - return Payload::updated([$domainUser->id => $domainUser->toArray()]); - } - - /** - * @param TUserDbRecordOptional $row - * - * @return TUserDbRecordOptional - */ - private function cleanupFields(array $row): array - { - unset($row['usr_id']); - - return array_filter( - $row, - static fn($v) => $v !== null && $v !== '' - ); - } - - /** - * @param HttpCodesEnum $item - * @param string $message - * - * @return Payload - */ - private function getErrorPayload( - HttpCodesEnum $item, - string $message - ): Payload { - return Payload::error([[$item->text() . $message]]); - } - - /** - * @param TUserDomainToDbRecord $input - * - * @return TUserDbRecordOptional - */ - private function preInsert(array $input): array - { - $result = $this->processPassword($input); - $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); - - /** - * Set the created/updated dates if need be - */ - if (true === empty($result['usr_created_date'])) { - $result['usr_created_date'] = $now; - } - if (true === empty($result['usr_updated_date'])) { - $result['usr_updated_date'] = $now; - } - - /** @var TUserDbRecordOptional $result */ - return $this->cleanupFields($result); - } - - /** - * @param TUserDomainToDbRecord $input - * - * @return TUserDbRecordOptional - */ - private function preUpdate(array $input): array - { - $result = $this->processPassword($input); - $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); - - /** - * Set updated date to now if it has not been set - */ - if (true === empty($result['usr_updated_date'])) { - $result['usr_updated_date'] = $now; - } - - /** - * Remove createdDate and createdUserId - cannot be changed. This - * needs to be here because we don't want to touch those fields. - */ - unset($result['usr_created_date'], $result['usr_created_usr_id']); - - - return $this->cleanupFields($result); - } - - /** - * @param TUserDomainToDbRecord $input - * - * @return TUserDomainToDbRecord - */ - private function processPassword(array $input): array - { - if (null !== $input['usr_password']) { - $plain = $input['usr_password']; - $hashed = $this->security->hash($plain); - - $input['usr_password'] = $hashed; - } - - return $input; - } -} diff --git a/src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php b/src/Domain/Infrastructure/DataSource/User/Mapper/UserMapper.php similarity index 91% rename from src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php rename to src/Domain/Infrastructure/DataSource/User/Mapper/UserMapper.php index cc6b0c6..70866ff 100644 --- a/src/Domain/Infrastructure/DataSource/User/Mappers/UserMapper.php +++ b/src/Domain/Infrastructure/DataSource/User/Mapper/UserMapper.php @@ -11,11 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Application\User\Command\UserPutCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\MapperInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; /** @@ -30,8 +31,9 @@ final class UserMapper implements MapperInterface * * @return TUserDomainToDbRecord */ - public function db(UserInput $user): array + public function db(CommandInterface $user): array { + /** @var UserPutCommand $user */ return [ 'usr_id' => $user->id, 'usr_status_flag' => $user->status, diff --git a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php b/src/Domain/Infrastructure/DataSource/User/Repository/UserRepository.php similarity index 97% rename from src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php rename to src/Domain/Infrastructure/DataSource/User/Repository/UserRepository.php index 7052084..ae62a64 100644 --- a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepository.php +++ b/src/Domain/Infrastructure/DataSource/User/Repository/UserRepository.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository; use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractRepository; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\MapperInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; diff --git a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php b/src/Domain/Infrastructure/DataSource/User/Repository/UserRepositoryInterface.php similarity index 99% rename from src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php rename to src/Domain/Infrastructure/DataSource/User/Repository/UserRepositoryInterface.php index a8a1572..15ef7ba 100644 --- a/src/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryInterface.php +++ b/src/Domain/Infrastructure/DataSource/User/Repository/UserRepositoryInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; diff --git a/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php b/src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizer.php similarity index 76% rename from src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php rename to src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizer.php index f158eb4..db9c45b 100644 --- a/src/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizer.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer; use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; -use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers\UserSanitizersEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\UserSanitizerEnum; final class UserSanitizer extends AbstractSanitizer { - protected string $enum = UserSanitizersEnum::class; + protected string $enum = UserSanitizerEnum::class; } diff --git a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidator.php similarity index 87% rename from src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php rename to src/Domain/Infrastructure/DataSource/User/Validator/UserValidator.php index 729aee7..a22c578 100644 --- a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidator.php +++ b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidator.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Infrastructure\Enums\Validators\UserInsertEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\UserInsertEnum; final class UserValidator extends AbstractValidator { diff --git a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdate.php similarity index 87% rename from src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php rename to src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdate.php index 291eea1..4bded48 100644 --- a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdate.php +++ b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdate.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Infrastructure\Enums\Validators\UserUpdateEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\UserUpdateEnum; final class UserValidatorUpdate extends AbstractValidator { diff --git a/src/Domain/Infrastructure/DataSource/Validation/Result.php b/src/Domain/Infrastructure/DataSource/Validation/Result.php index 064d357..f0131cf 100644 --- a/src/Domain/Infrastructure/DataSource/Validation/Result.php +++ b/src/Domain/Infrastructure/DataSource/Validation/Result.php @@ -26,7 +26,7 @@ final class Result * @param TResultMeta $meta */ public function __construct( - private array $errors = [], + private readonly array $errors = [], private array $meta = [], ) { } diff --git a/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php index 32ab547..42c805c 100644 --- a/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php +++ b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php @@ -13,7 +13,20 @@ namespace Phalcon\Api\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLogoutPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthRefreshPostService; +use Phalcon\Api\Domain\Application\User\Service\UserDeleteService; +use Phalcon\Api\Domain\Application\User\Service\UserGetService; +use Phalcon\Api\Domain\Application\User\Service\UserPostService; +use Phalcon\Api\Domain\Application\User\Service\UserPutService; +use Phalcon\Api\Domain\Infrastructure\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenClaimsMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenPresenceMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenRevokedMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenStructureMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenUserMiddleware; use function str_replace; @@ -22,16 +35,15 @@ */ enum RoutesEnum: int { + /** + * Methods + */ + public const DELETE = 'delete'; /** * Events */ public const EVENT_BEFORE = 'before'; public const EVENT_FINISH = 'finish'; - - /** - * Methods - */ - public const DELETE = 'delete'; public const GET = 'get'; public const POST = 'post'; public const PUT = 'put'; @@ -75,13 +87,13 @@ public function method(): string public static function middleware(): array { return [ - Container::MIDDLEWARE_NOT_FOUND => self::EVENT_BEFORE, - Container::MIDDLEWARE_HEALTH => self::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_PRESENCE => self::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE => self::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_USER => self::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_CLAIMS => self::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_REVOKED => self::EVENT_BEFORE, + NotFoundMiddleware::class => self::EVENT_BEFORE, + HealthMiddleware::class => self::EVENT_BEFORE, + ValidateTokenPresenceMiddleware::class => self::EVENT_BEFORE, + ValidateTokenStructureMiddleware::class => self::EVENT_BEFORE, + ValidateTokenUserMiddleware::class => self::EVENT_BEFORE, + ValidateTokenClaimsMiddleware::class => self::EVENT_BEFORE, + ValidateTokenRevokedMiddleware::class => self::EVENT_BEFORE, ]; } @@ -106,13 +118,13 @@ public function prefix(): string public function service(): string { return match ($this) { - self::authLoginPost => Container::AUTH_LOGIN_POST_SERVICE, - self::authLogoutPost => Container::AUTH_LOGOUT_POST_SERVICE, - self::authRefreshPost => Container::AUTH_REFRESH_POST_SERVICE, - self::userDelete => Container::USER_DELETE_SERVICE, - self::userGet => Container::USER_GET_SERVICE, - self::userPost => Container::USER_POST_SERVICE, - self::userPut => Container::USER_PUT_SERVICE, + self::authLoginPost => AuthLoginPostService::class, + self::authLogoutPost => AuthLogoutPostService::class, + self::authRefreshPost => AuthRefreshPostService::class, + self::userDelete => UserDeleteService::class, + self::userGet => UserGetService::class, + self::userPost => UserPostService::class, + self::userPut => UserPutService::class, }; } diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php b/src/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnum.php similarity index 82% rename from src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php rename to src/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnum.php index 2018e0f..5baf791 100644 --- a/src/Domain/Infrastructure/Enums/Sanitizers/AuthSanitizersEnum.php +++ b/src/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnum.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer; use Phalcon\Filter\Filter; -enum AuthSanitizersEnum implements SanitizersEnumInterface +enum AuthSanitizerEnum implements SanitizersEnumInterface { case email; case password; diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php b/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php similarity index 91% rename from src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php rename to src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php index ff61d68..ee3eea5 100644 --- a/src/Domain/Infrastructure/Enums/Sanitizers/UserSanitizersEnum.php +++ b/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer; use Phalcon\Filter\Filter; -enum UserSanitizersEnum implements SanitizersEnumInterface +enum UserSanitizerEnum implements SanitizersEnumInterface { case id; case status; diff --git a/src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php b/src/Domain/Infrastructure/Enums/Validator/UserUpdateEnum.php similarity index 69% rename from src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php rename to src/Domain/Infrastructure/Enums/Validator/UserUpdateEnum.php index 31ee31f..2ce9983 100644 --- a/src/Domain/Infrastructure/Enums/Validators/UserUpdateEnum.php +++ b/src/Domain/Infrastructure/Enums/Validator/UserUpdateEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; use Phalcon\Filter\Validation\Validator\Email; @@ -26,18 +26,25 @@ enum UserUpdateEnum implements ValidatorEnumInterface case tokenPassword; case tokenId; + public function allowEmpty(): bool + { + return match ($this) { + self::id => false, + default => true, + }; + } + public function validators(): array { return match ($this) { self::id => [ PresenceOf::class, - AbsInt::class + AbsInt::class, ], - self::email => [ - PresenceOf::class, - Email::class + self::email => [ + Email::class, ], - default => [PresenceOf::class], + default => [], }; } } diff --git a/src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php b/src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php index 1dd1ffa..80f42f8 100644 --- a/src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php +++ b/src/Domain/Infrastructure/Providers/ErrorHandlerProvider.php @@ -32,9 +32,9 @@ class ErrorHandlerProvider implements ServiceProviderInterface public function register(DiInterface $container): void { /** @var EnvManager $env */ - $env = $container->getShared(Container::ENV); + $env = $container->getShared(EnvManager::class); /** @var Logger $logger */ - $logger = $container->getShared(Container::LOGGER); + $logger = $container->getShared(Logger::class); /** @var int $time */ $time = $container->getShared(Container::TIME); diff --git a/src/Domain/Infrastructure/Providers/RouterProvider.php b/src/Domain/Infrastructure/Providers/RouterProvider.php index 2d184ae..deede91 100644 --- a/src/Domain/Infrastructure/Providers/RouterProvider.php +++ b/src/Domain/Infrastructure/Providers/RouterProvider.php @@ -17,6 +17,7 @@ use Phalcon\Api\Domain\ADR\DomainInterface; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; +use Phalcon\Api\Responder\JsonResponder; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; @@ -82,7 +83,7 @@ private function attachRoutes(Micro $application): void /** @var ResponseInterface $response */ $response = $application->getService(Container::RESPONSE); /** @var ResponderInterface $responder */ - $responder = $application->getService(Container::RESPONDER_JSON); + $responder = $application->getService(JsonResponder::class); $routes = RoutesEnum::cases(); foreach ($routes as $route) { diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 381d2ee..4755dbc 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -89,7 +89,7 @@ public function getConnection(): Connection { if (null === $this->connection) { /** @var Connection $connection */ - $connection = $this->container->getShared(Container::CONNECTION); + $connection = $this->container->getShared(Connection::class); $this->connection = $connection; } @@ -146,7 +146,7 @@ public function getNewUserData(array $fields = []): array $faker = Factory::create(); $password = $fields['usr_password'] ?? $this->getStrongPassword(); /** @var Security $security */ - $security = $this->container->getShared(Container::SECURITY); + $security = $this->container->getShared(Security::class); $password = $security->hash($password); diff --git a/tests/Unit/Action/ActionHandlerTest.php b/tests/Unit/Action/ActionHandlerTest.php index a572428..fdeb3d5 100644 --- a/tests/Unit/Action/ActionHandlerTest.php +++ b/tests/Unit/Action/ActionHandlerTest.php @@ -15,9 +15,10 @@ use Phalcon\Api\Action\ActionHandler; use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Responder\JsonResponder; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Api\Tests\Fixtures\Domain\Services\ServiceFixture; +use Phalcon\Api\Tests\Fixtures\Domain\Application\Service\ServiceFixture; use Phalcon\Http\RequestInterface; use Phalcon\Http\ResponseInterface; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -36,7 +37,7 @@ public function testInvoke(): void /** @var ResponseInterface $response */ $response = $this->container->getShared(Container::RESPONSE); /** @var ResponderInterface $responder */ - $responder = $this->container->getShared(Container::RESPONDER_JSON); + $responder = $this->container->getShared(JsonResponder::class); $getData = [ 'key' => uniqid('key-'), diff --git a/tests/Unit/Domain/ADR/InputTest.php b/tests/Unit/Domain/ADR/InputTest.php index c82ecb3..8aa411f 100644 --- a/tests/Unit/Domain/ADR/InputTest.php +++ b/tests/Unit/Domain/ADR/InputTest.php @@ -24,16 +24,16 @@ final class InputTest extends TestCase { public function testInvoke(): void { - $faker = Factory::create(); + $faker = Factory::create(); $postData = [ 'post1' => $faker->word(), 'post2' => $faker->word(), ]; - $getData = [ + $getData = [ 'get1' => $faker->word(), 'get2' => $faker->word(), ]; - $putData = [ + $putData = [ 'put1' => $faker->word(), 'put2' => $faker->word(), ]; @@ -63,7 +63,7 @@ public function testInvoke(): void 'put1' => $putData['put1'], 'put2' => $putData['put2'], ]; - $actual = $input->__invoke($mockRequest); + $actual = $input->__invoke($mockRequest); $this->assertSame($expected, $actual); } } diff --git a/tests/Unit/Domain/ADR/PayloadTest.php b/tests/Unit/Domain/ADR/PayloadTest.php index 4ec98c7..f5e744a 100644 --- a/tests/Unit/Domain/ADR/PayloadTest.php +++ b/tests/Unit/Domain/ADR/PayloadTest.php @@ -22,126 +22,126 @@ final class PayloadTest extends AbstractUnitTestCase { public function testCreated(): void { - $data = ['id' => 1]; + $data = ['id' => 1]; $payload = Payload::created($data); $expected = Payload::class; - $actual = get_class($payload); + $actual = get_class($payload); $this->assertSame($expected, $actual); $expected = DomainStatus::CREATED; - $actual = $payload->getStatus(); + $actual = $payload->getStatus(); $this->assertSame($expected, $actual); $expected = ['data' => $data]; - $actual = $payload->getResult(); + $actual = $payload->getResult(); $this->assertSame($expected, $actual); } public function testDeleted(): void { - $data = ['deleted' => true]; + $data = ['deleted' => true]; $payload = Payload::deleted($data); $expected = DomainStatus::DELETED; - $actual = $payload->getStatus(); + $actual = $payload->getStatus(); $this->assertSame($expected, $actual); $expected = ['data' => $data]; - $actual = $payload->getResult(); + $actual = $payload->getResult(); $this->assertSame($expected, $actual); } - public function testUpdated(): void + public function testError(): void { - $data = ['updated' => true]; - $payload = Payload::updated($data); + $errors = [['something' => 'went wrong']]; + $payload = Payload::error($errors); - $expected = DomainStatus::UPDATED; - $actual = $payload->getStatus(); + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); $this->assertSame($expected, $actual); - $expected = ['data' => $data]; - $actual = $payload->getResult(); + $expected = ['errors' => $errors]; + $actual = $payload->getResult(); $this->assertSame($expected, $actual); } - public function testSuccess(): void + public function testInvalid(): void { - $data = ['items' => [1, 2, 3]]; - $payload = Payload::success($data); + $errors = [['field' => 'invalid']]; + $payload = Payload::invalid($errors); - $expected = DomainStatus::SUCCESS; - $actual = $payload->getStatus(); + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); $this->assertSame($expected, $actual); - $expected = ['data' => $data]; - $actual = $payload->getResult(); + $expected = ['errors' => $errors]; + $actual = $payload->getResult(); $this->assertSame($expected, $actual); } - public function testError(): void + public function testNotFound(): void { - $errors = [['something' => 'went wrong']]; - $payload = Payload::error($errors); + $payload = Payload::notFound(); - $expected = DomainStatus::ERROR; - $actual = $payload->getStatus(); + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); $this->assertSame($expected, $actual); - $expected = ['errors' => $errors]; - $actual = $payload->getResult(); + $expected = [ + 'code' => HttpCodesEnum::NotFound->value, + 'message' => HttpCodesEnum::NotFound->text(), + 'data' => [], + 'errors' => [['Record(s) not found']], + ]; + $actual = $payload->getResult(); $this->assertSame($expected, $actual); } - public function testInvalid(): void + public function testSuccess(): void { - $errors = [['field' => 'invalid']]; - $payload = Payload::invalid($errors); + $data = ['items' => [1, 2, 3]]; + $payload = Payload::success($data); - $expected = DomainStatus::INVALID; - $actual = $payload->getStatus(); + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); $this->assertSame($expected, $actual); - $expected = ['errors' => $errors]; - $actual = $payload->getResult(); + $expected = ['data' => $data]; + $actual = $payload->getResult(); $this->assertSame($expected, $actual); } public function testUnauthorized(): void { - $errors = [['reason' => 'no access']]; + $errors = [['reason' => 'no access']]; $payload = Payload::unauthorized($errors); $expected = DomainStatus::UNAUTHORIZED; - $actual = $payload->getStatus(); + $actual = $payload->getStatus(); $this->assertSame($expected, $actual); $expected = [ 'code' => HttpCodesEnum::Unauthorized->value, 'message' => HttpCodesEnum::Unauthorized->text(), 'data' => [], - 'errors' => $errors + 'errors' => $errors, ]; - $actual = $payload->getResult(); + $actual = $payload->getResult(); $this->assertSame($expected, $actual); } - public function testNotFound(): void + public function testUpdated(): void { - $payload = Payload::notFound(); + $data = ['updated' => true]; + $payload = Payload::updated($data); - $expected = DomainStatus::NOT_FOUND; - $actual = $payload->getStatus(); + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); $this->assertSame($expected, $actual); - $expected = [ - 'code' => HttpCodesEnum::NotFound->value, - 'message' => HttpCodesEnum::NotFound->text(), - 'data' => [], - 'errors' => [['Record(s) not found']] - ]; - $actual = $payload->getResult(); + $expected = ['data' => $data]; + $actual = $payload->getResult(); $this->assertSame($expected, $actual); } } diff --git a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php b/tests/Unit/Domain/Application/Auth/Service/LoginPostServiceTest.php similarity index 87% rename from tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php rename to tests/Unit/Domain/Application/Auth/Service/LoginPostServiceTest.php index 3bc5aae..9ea47ae 100644 --- a/tests/Unit/Domain/Services/Auth/LoginPostServiceTest.php +++ b/tests/Unit/Domain/Application/Auth/Service/LoginPostServiceTest.php @@ -11,13 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; +namespace Phalcon\Api\Tests\Unit\Domain\Application\Auth\Service; use Faker\Factory; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Services\Auth\LoginPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -27,8 +26,8 @@ final class LoginPostServiceTest extends AbstractUnitTestCase { public function testServiceEmptyCredentials(): void { - /** @var LoginPostService $service */ - $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + /** @var AuthLoginPostService $service */ + $service = $this->container->get(AuthLoginPostService::class); $payload = $service->__invoke([]); @@ -46,8 +45,8 @@ public function testServiceEmptyCredentials(): void public function testServiceWithCredentials(): void { - /** @var LoginPostService $service */ - $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + /** @var AuthLoginPostService $service */ + $service = $this->container->get(AuthLoginPostService::class); $migration = new UsersMigration($this->getConnection()); /** @@ -113,8 +112,8 @@ public function testServiceWithCredentials(): void public function testServiceWrongCredentials(): void { $faker = Factory::create(); - /** @var LoginPostService $service */ - $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + /** @var AuthLoginPostService $service */ + $service = $this->container->get(AuthLoginPostService::class); /** * Issue a wrong password @@ -140,8 +139,8 @@ public function testServiceWrongCredentials(): void public function testServiceWrongCredentialsForUser(): void { - /** @var LoginPostService $service */ - $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + /** @var AuthLoginPostService $service */ + $service = $this->container->get(AuthLoginPostService::class); $migration = new UsersMigration($this->getConnection()); $password = 'password'; diff --git a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php b/tests/Unit/Domain/Application/Auth/Service/LogoutPostServiceTest.php similarity index 85% rename from tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php rename to tests/Unit/Domain/Application/Auth/Service/LogoutPostServiceTest.php index 326be2b..afddf99 100644 --- a/tests/Unit/Domain/Services/Auth/LogoutPostServiceTest.php +++ b/tests/Unit/Domain/Application/Auth/Service/LogoutPostServiceTest.php @@ -11,16 +11,15 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; +namespace Phalcon\Api\Tests\Unit\Domain\Application\Auth\Service; use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLogoutPostService; use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Services\Auth\LoginPostService; -use Phalcon\Api\Domain\Services\Auth\LogoutPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Cache\Cache; @@ -33,8 +32,8 @@ final class LogoutPostServiceTest extends AbstractUnitTestCase { public function testServiceEmptyToken(): void { - /** @var LogoutPostService $service */ - $service = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + /** @var AuthLogoutPostService $service */ + $service = $this->container->get(AuthLogoutPostService::class); $payload = $service->__invoke([]); @@ -53,7 +52,7 @@ public function testServiceEmptyToken(): void public function testServiceInvalidToken(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $user = $this->getNewUserData(); $user['usr_id'] = 1; $domainUser = $userMapper->domain($user); @@ -108,10 +107,10 @@ public function testServiceInvalidToken(): void /** * Replace the service with the mocked one */ - $this->container->set(Container::JWT_TOKEN, $mockJWT); + $this->container->set(JWTToken::class, $mockJWT); - /** @var LogoutPostService $service */ - $service = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + /** @var AuthLogoutPostService $service */ + $service = $this->container->get(AuthLogoutPostService::class); $payload = $service->__invoke(['token' => '1234']); @@ -171,9 +170,9 @@ public function testServiceNotRefreshToken(): void /** * Replace the service with the mocked one */ - $this->container->set(Container::JWT_TOKEN, $mockJWT); - /** @var LogoutPostService $service */ - $logoutService = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + $this->container->set(JWTToken::class, $mockJWT); + /** @var AuthLogoutPostService $service */ + $logoutService = $this->container->get(AuthLogoutPostService::class); /** * Logout now @@ -197,13 +196,13 @@ public function testServiceNotRefreshToken(): void public function testServiceSuccess(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); - /** @var LogoutPostService $service */ - $logoutService = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + $userMapper = $this->container->get(UserMapper::class); + /** @var AuthLogoutPostService $service */ + $logoutService = $this->container->get(AuthLogoutPostService::class); /** @var Cache $cache */ - $cache = $this->container->getShared(Container::CACHE); - /** @var LoginPostService $service */ - $service = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); + $cache = $this->container->getShared(Cache::class); + /** @var AuthLoginPostService $service */ + $service = $this->container->get(AuthLoginPostService::class); $migration = new UsersMigration($this->getConnection()); /** @@ -283,7 +282,7 @@ public function testServiceSuccess(): void public function testServiceWrongUser(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $domainUser = $userMapper->domain([]); /** @@ -331,10 +330,10 @@ public function testServiceWrongUser(): void /** * Replace the service with the mocked one */ - $this->container->set(Container::JWT_TOKEN, $mockJWT); + $this->container->set(JWTToken::class, $mockJWT); - /** @var LogoutPostService $service */ - $logoutService = $this->container->get(Container::AUTH_LOGOUT_POST_SERVICE); + /** @var AuthLogoutPostService $service */ + $logoutService = $this->container->get(AuthLogoutPostService::class); /** * Logout now diff --git a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php b/tests/Unit/Domain/Application/Auth/Service/RefreshPostServiceTest.php similarity index 85% rename from tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php rename to tests/Unit/Domain/Application/Auth/Service/RefreshPostServiceTest.php index fdf5937..6c13192 100644 --- a/tests/Unit/Domain/Services/Auth/RefreshPostServiceTest.php +++ b/tests/Unit/Domain/Application/Auth/Service/RefreshPostServiceTest.php @@ -11,15 +11,14 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\Auth; +namespace Phalcon\Api\Tests\Unit\Domain\Application\Auth\Service; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthRefreshPostService; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Services\Auth\LoginPostService; -use Phalcon\Api\Domain\Services\Auth\RefreshPostService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Encryption\Security\JWT\Token\Item; @@ -31,8 +30,8 @@ final class RefreshPostServiceTest extends AbstractUnitTestCase { public function testServiceEmptyToken(): void { - /** @var RefreshPostService $service */ - $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + /** @var AuthRefreshPostService $service */ + $service = $this->container->get(AuthRefreshPostService::class); $payload = $service->__invoke([]); @@ -51,7 +50,7 @@ public function testServiceEmptyToken(): void public function testServiceInvalidToken(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $user = $this->getNewUserData(); $user['usr_id'] = 1; $domainUser = $userMapper->domain($user); @@ -106,10 +105,10 @@ public function testServiceInvalidToken(): void /** * Replace the service with the mocked one */ - $this->container->set(Container::JWT_TOKEN, $mockJWT); + $this->container->set(JWTToken::class, $mockJWT); - /** @var RefreshPostService $service */ - $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + /** @var AuthRefreshPostService $service */ + $service = $this->container->get(AuthRefreshPostService::class); $payload = $service->__invoke(['token' => '1234']); @@ -169,10 +168,10 @@ public function testServiceNotRefreshToken(): void /** * Replace the service with the mocked one */ - $this->container->set(Container::JWT_TOKEN, $mockJWT); + $this->container->set(JWTToken::class, $mockJWT); - /** @var RefreshPostService $service */ - $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + /** @var AuthRefreshPostService $service */ + $service = $this->container->get(AuthRefreshPostService::class); $payload = $service->__invoke(['token' => '1234']); @@ -190,10 +189,10 @@ public function testServiceNotRefreshToken(): void public function testServiceWithCredentials(): void { - /** @var LoginPostService $service */ - $loginService = $this->container->get(Container::AUTH_LOGIN_POST_SERVICE); - /** @var RefreshPostService $service */ - $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + /** @var AuthLoginPostService $service */ + $loginService = $this->container->get(AuthLoginPostService::class); + /** @var AuthRefreshPostService $service */ + $service = $this->container->get(AuthRefreshPostService::class); $migration = new UsersMigration($this->getConnection()); /** @@ -284,10 +283,10 @@ public function testServiceWrongUser(): void /** * Replace the service with the mocked one */ - $this->container->set(Container::JWT_TOKEN, $mockJWT); + $this->container->set(JWTToken::class, $mockJWT); - /** @var RefreshPostService $service */ - $service = $this->container->get(Container::AUTH_REFRESH_POST_SERVICE); + /** @var AuthRefreshPostService $service */ + $service = $this->container->get(AuthRefreshPostService::class); $payload = $service->__invoke(['token' => '1234']); diff --git a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php b/tests/Unit/Domain/Application/User/Service/UserServiceDeleteTest.php similarity index 87% rename from tests/Unit/Domain/Services/User/UserServiceDeleteTest.php rename to tests/Unit/Domain/Application/User/Service/UserServiceDeleteTest.php index dde46b4..fdbe759 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDeleteTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServiceDeleteTest.php @@ -11,22 +11,19 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\User; +namespace Phalcon\Api\Tests\Unit\Domain\Application\User\Service; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Services\User\UserDeleteService; +use Phalcon\Api\Domain\Application\User\Service\UserDeleteService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; -use PHPUnit\Framework\Attributes\BackupGlobals; -#[BackupGlobals(true)] final class UserServiceDeleteTest extends AbstractUnitTestCase { public function testServiceWithUserId(): void { /** @var UserDeleteService $service */ - $service = $this->container->get(Container::USER_DELETE_SERVICE); + $service = $this->container->get(UserDeleteService::class); /** * We need to ask for a user to be deleted with an ID that does not @@ -84,7 +81,7 @@ public function testServiceWithUserId(): void public function testServiceZeroUserId(): void { /** @var UserDeleteService $service */ - $service = $this->container->get(Container::USER_DELETE_SERVICE); + $service = $this->container->get(UserDeleteService::class); $payload = $service->__invoke( [ diff --git a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php b/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php similarity index 88% rename from tests/Unit/Domain/Services/User/UserServiceDispatchTest.php rename to tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php index 128da7b..78798f3 100644 --- a/tests/Unit/Domain/Services/User/UserServiceDispatchTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php @@ -11,13 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\User; +namespace Phalcon\Api\Tests\Unit\Domain\Application\User\Service; use DateTimeImmutable; use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; use Phalcon\Api\Domain\Infrastructure\Constants\Dates; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -31,11 +30,11 @@ final class UserServiceDispatchTest extends AbstractUnitTestCase public function testDispatchGet(): void { /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); /** @var Cache $cache */ - $cache = $this->container->getShared(Container::CACHE); + $cache = $this->container->getShared(Cache::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); diff --git a/tests/Unit/Domain/Services/User/UserServiceGetTest.php b/tests/Unit/Domain/Application/User/Service/UserServiceGetTest.php similarity index 91% rename from tests/Unit/Domain/Services/User/UserServiceGetTest.php rename to tests/Unit/Domain/Application/User/Service/UserServiceGetTest.php index 0bcbd48..95a5cde 100644 --- a/tests/Unit/Domain/Services/User/UserServiceGetTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServiceGetTest.php @@ -11,22 +11,19 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\User; +namespace Phalcon\Api\Tests\Unit\Domain\Application\User\Service; use PayloadInterop\DomainStatus; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Services\User\UserGetService; +use Phalcon\Api\Domain\Application\User\Service\UserGetService; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; -use PHPUnit\Framework\Attributes\BackupGlobals; -#[BackupGlobals(true)] final class UserServiceGetTest extends AbstractUnitTestCase { public function testServiceEmptyUserId(): void { /** @var UserGetService $service */ - $service = $this->container->get(Container::USER_GET_SERVICE); + $service = $this->container->get(UserGetService::class); $payload = $service->__invoke([]); @@ -47,7 +44,7 @@ public function testServiceEmptyUserId(): void public function testServiceWithUserId(): void { /** @var UserGetService $service */ - $service = $this->container->get(Container::USER_GET_SERVICE); + $service = $this->container->get(UserGetService::class); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); @@ -142,7 +139,7 @@ public function testServiceWithUserId(): void public function testServiceWrongUserId(): void { /** @var UserGetService $service */ - $service = $this->container->get(Container::USER_GET_SERVICE); + $service = $this->container->get(UserGetService::class); $payload = $service->__invoke( [ diff --git a/tests/Unit/Domain/Services/User/UserServicePostTest.php b/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php similarity index 81% rename from tests/Unit/Domain/Services/User/UserServicePostTest.php rename to tests/Unit/Domain/Application/User/Service/UserServicePostTest.php index d3a92c6..2bfa6bc 100644 --- a/tests/Unit/Domain/Services/User/UserServicePostTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php @@ -11,21 +11,19 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\User; +namespace Phalcon\Api\Tests\Unit\Domain\Application\User\Service; use DateTimeImmutable; use PayloadInterop\DomainStatus; use PDOException; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; -use Phalcon\Api\Domain\Services\User\UserPostService; +use Phalcon\Api\Domain\Application\User\Service\UserPostService; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; use Phalcon\Api\Tests\AbstractUnitTestCase; -use PHPUnit\Framework\Attributes\BackupGlobals; +use Phalcon\Support\Registry; use function htmlspecialchars; -#[BackupGlobals(true)] final class UserServicePostTest extends AbstractUnitTestCase { public function testServiceFailureNoIdReturned(): void @@ -42,12 +40,14 @@ public function testServiceFailureNoIdReturned(): void ; $userRepository->method('insert')->willReturn(0); - $this->container->setShared(Container::USER_REPOSITORY, $userRepository); + $this->container->setShared(UserRepository::class, $userRepository); /** @var UserPostService $service */ - $service = $this->container->get(Container::USER_POST_SERVICE); + $service = $this->container->get(UserPostService::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -56,6 +56,12 @@ public function testServiceFailureNoIdReturned(): void * $userData is a db record. We need a domain object here */ $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + $domainData = $domainUser->toArray(); $payload = $service->__invoke($domainData); @@ -91,12 +97,14 @@ public function testServiceFailurePdoError(): void ->willThrowException(new PDOException('abcde')) ; - $this->container->setShared(Container::USER_REPOSITORY, $userRepository); + $this->container->setShared(UserRepository::class, $userRepository); /** @var UserPostService $service */ - $service = $this->container->get(Container::USER_POST_SERVICE); + $service = $this->container->get(UserPostService::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -105,6 +113,12 @@ public function testServiceFailurePdoError(): void * $userData is a db record. We need a domain object here */ $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + $domainData = $domainUser->toArray(); $payload = $service->__invoke($domainData); @@ -126,9 +140,9 @@ public function testServiceFailurePdoError(): void public function testServiceFailureValidation(): void { /** @var UserPostService $service */ - $service = $this->container->get(Container::USER_POST_SERVICE); + $service = $this->container->get(UserPostService::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); $userData = $this->getNewUserData(); @@ -172,18 +186,24 @@ public function testServiceFailureValidation(): void public function testServiceSuccess(): void { /** @var UserPostService $service */ - $service = $this->container->get(Container::USER_POST_SERVICE); + $service = $this->container->get(UserPostService::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); - $userData = $this->getNewUserData(); - $userData['usr_created_usr_id'] = 4; - $userData['usr_updated_usr_id'] = 5; + $userData = $this->getNewUserData(); /** * $userData is a db record. We need a domain object here */ $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + $domainData = $domainUser->toArray(); $payload = $service->__invoke($domainData); @@ -253,7 +273,7 @@ public function testServiceSuccess(): void $actual = $data['createdDate']; $this->assertSame($expected, $actual); - $expected = 4; + $expected = $domainUser->id; $actual = $data['createdUserId']; $this->assertSame($expected, $actual); @@ -261,7 +281,7 @@ public function testServiceSuccess(): void $actual = $data['updatedDate']; $this->assertSame($expected, $actual); - $expected = 5; + $expected = $domainUser->id; $actual = $data['updatedUserId']; $this->assertSame($expected, $actual); } @@ -271,9 +291,11 @@ public function testServiceSuccessEmptyDates(): void $now = new DateTimeImmutable(); $today = $now->format('Y-m-d'); /** @var UserPostService $service */ - $service = $this->container->get(Container::USER_POST_SERVICE); + $service = $this->container->get(UserPostService::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); $userData = $this->getNewUserData(); unset( @@ -285,6 +307,12 @@ public function testServiceSuccessEmptyDates(): void * $userData is a db record. We need a domain object here */ $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + $domainData = $domainUser->toArray(); $payload = $service->__invoke($domainData); diff --git a/tests/Unit/Domain/Services/User/UserServicePutTest.php b/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php similarity index 79% rename from tests/Unit/Domain/Services/User/UserServicePutTest.php rename to tests/Unit/Domain/Application/User/Service/UserServicePutTest.php index 799f9a3..aea8afb 100644 --- a/tests/Unit/Domain/Services/User/UserServicePutTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php @@ -11,27 +11,27 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Services\User; +namespace Phalcon\Api\Tests\Unit\Domain\Application\User\Service; use DateTimeImmutable; use PayloadInterop\DomainStatus; use PDOException; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; -use Phalcon\Api\Domain\Services\User\UserPutService; +use Phalcon\Api\Domain\Application\User\Service\UserPutService; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; -use PHPUnit\Framework\Attributes\BackupGlobals; +use Phalcon\Support\Registry; -#[BackupGlobals(true)] final class UserServicePutTest extends AbstractUnitTestCase { public function testServiceFailureNoIdReturned(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -51,10 +51,10 @@ public function testServiceFailureNoIdReturned(): void $userRepository->method('update')->willReturn(0); $userRepository->method('findById')->willReturn($findByUser); - $this->container->setShared(Container::USER_REPOSITORY, $userRepository); + $this->container->setShared(UserRepository::class, $userRepository); /** @var UserPutService $service */ - $service = $this->container->get(Container::USER_PUT_SERVICE); + $service = $this->container->get(UserPutService::class); /** * Update user @@ -63,6 +63,12 @@ public function testServiceFailureNoIdReturned(): void $userData['usr_id'] = 1; $updateUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $updateUser); + $updateUser = $updateUser->toArray(); $payload = $service->__invoke($updateUser); @@ -84,8 +90,10 @@ public function testServiceFailureNoIdReturned(): void public function testServiceFailurePdoError(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -108,10 +116,10 @@ public function testServiceFailurePdoError(): void ->willThrowException(new PDOException('abcde')) ; - $this->container->setShared(Container::USER_REPOSITORY, $userRepository); + $this->container->setShared(UserRepository::class, $userRepository); /** @var UserPutService $service */ - $service = $this->container->get(Container::USER_PUT_SERVICE); + $service = $this->container->get(UserPutService::class); $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -120,6 +128,12 @@ public function testServiceFailurePdoError(): void * $userData is a db record. We need a domain object here */ $updateUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $updateUser); + $updateUser = $updateUser->toArray(); $payload = $service->__invoke($updateUser); @@ -141,13 +155,13 @@ public function testServiceFailurePdoError(): void public function testServiceFailureRecordNotFound(): void { /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + $userData = $this->getNewUserData(); $userData['usr_id'] = 1; - $findByUser = $userMapper->domain($userData); - $userRepository = $this ->getMockBuilder(UserRepository::class) ->disableOriginalConstructor() @@ -160,10 +174,10 @@ public function testServiceFailureRecordNotFound(): void ; $userRepository->method('findById')->willReturn(null); - $this->container->setShared(Container::USER_REPOSITORY, $userRepository); + $this->container->setShared(UserRepository::class, $userRepository); /** @var UserPutService $service */ - $service = $this->container->get(Container::USER_PUT_SERVICE); + $service = $this->container->get(UserPutService::class); /** * Update user @@ -172,6 +186,12 @@ public function testServiceFailureRecordNotFound(): void $userData['usr_id'] = 1; $updateUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $updateUser); + $updateUser = $updateUser->toArray(); $payload = $service->__invoke($updateUser); @@ -193,24 +213,26 @@ public function testServiceFailureRecordNotFound(): void public function testServiceFailureValidation(): void { /** @var UserPutService $service */ - $service = $this->container->get(Container::USER_PUT_SERVICE); + $service = $this->container->get(UserPutService::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); - $userData = $this->getNewUserData(); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + $userData = $this->getNewUserData(); - unset( - $userData['usr_id'], - $userData['usr_email'], - $userData['usr_password'], - $userData['usr_issuer'], - $userData['usr_token_password'], - $userData['usr_token_id'] - ); + $userData['usr_email'] = 'wrong-email'; + unset($userData['usr_id']); /** * $userData is a db record. We need a domain object here */ $updateUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $updateUser); + $updateUser = $updateUser->toArray(); $payload = $service->__invoke($updateUser); @@ -226,12 +248,7 @@ public function testServiceFailureValidation(): void $expected = [ ['Field id is not a valid absolute integer and greater than 0'], - ['Field email is required'], ['Field email must be an email address'], - ['Field password is required'], - ['Field issuer is required'], - ['Field tokenPassword is required'], - ['Field tokenId is required'], ]; $actual = $errors; $this->assertSame($expected, $actual); @@ -240,9 +257,11 @@ public function testServiceFailureValidation(): void public function testServiceSuccess(): void { /** @var UserPutService $service */ - $service = $this->container->get(Container::USER_PUT_SERVICE); + $service = $this->container->get(UserPutService::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); @@ -255,12 +274,17 @@ public function testServiceSuccess(): void $userData['usr_password'] = $this->getStrongPassword(); $userData['usr_created_usr_id'] = 4; - $userData['usr_updated_usr_id'] = 5; /** * $userData is a db record. We need a domain object here */ $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + $domainData = $domainUser->toArray(); $payload = $service->__invoke($domainData); @@ -343,7 +367,7 @@ public function testServiceSuccess(): void $actual = $data['updatedDate']; $this->assertSame($expected, $actual); - $expected = 5; + $expected = $domainUser->id; $actual = $data['updatedUserId']; $this->assertSame($expected, $actual); } @@ -353,9 +377,11 @@ public function testServiceSuccessEmptyDates(): void $now = new DateTimeImmutable(); $today = $now->format('Y-m-d'); /** @var UserPutService $service */ - $service = $this->container->get(Container::USER_PUT_SERVICE); + $service = $this->container->get(UserPutService::class); /** @var UserMapper $userMapper */ - $userMapper = $this->container->get(Container::USER_MAPPER); + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); $migration = new UsersMigration($this->getConnection()); $dbUser = $this->getNewUser($migration); @@ -376,6 +402,12 @@ public function testServiceSuccessEmptyDates(): void * $userData is a db record. We need a domain object here */ $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + $domainData = $domainUser->toArray(); $payload = $service->__invoke($domainData); @@ -458,7 +490,7 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['updatedDate']; $this->assertStringContainsString($today, $actual); - $expected = $dbUser['usr_updated_usr_id']; + $expected = $domainUser->id; $actual = $data['updatedUserId']; $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php b/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php index 30d27b6..e08a0bd 100644 --- a/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php +++ b/tests/Unit/Domain/Infrastructure/Constants/CacheTest.php @@ -44,12 +44,12 @@ public function testConstants(): void $actual = Cache::CACHE_TOKEN_EXPIRY; $this->assertSame($expected, $actual); - $token = uniqid('tok-'); + $token = uniqid('tok-'); $shaToken = sha1($token); - $newUser = $this->getNewUserData(); + $newUser = $this->getNewUserData(); $newUser['usr_id'] = 1; - $domainUser = new User( + $domainUser = new User( $newUser['usr_id'], $newUser['usr_status_flag'], $newUser['usr_email'], @@ -70,7 +70,7 @@ public function testConstants(): void ); $expected = 'tk-' . $domainUser->id . '-' . $shaToken; - $actual = Cache::getCacheTokenKey($domainUser, $token); + $actual = Cache::getCacheTokenKey($domainUser, $token); $this->assertSame($expected, $actual); } } diff --git a/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php b/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php index 2909f28..b4c88e1 100644 --- a/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php +++ b/tests/Unit/Domain/Infrastructure/Constants/DatesTest.php @@ -41,7 +41,7 @@ public function testConstants(): void $actual = Dates::DATE_TIME_ZONE; $this->assertSame($expected, $actual); - $now = new DateTimeImmutable(); + $now = new DateTimeImmutable(); $expected = $now->format('Y-m-d'); $actual = Dates::toUTC(format: 'Y-m-d'); diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php index 14fa5bc..26ed3da 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/DTO/AuthInputTest.php @@ -14,9 +14,8 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\DTO; use Faker\Factory as FakerFactory; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizer; use Phalcon\Api\Tests\AbstractUnitTestCase; final class AuthInputTest extends AbstractUnitTestCase @@ -24,7 +23,7 @@ final class AuthInputTest extends AbstractUnitTestCase public function testObject(): void { /** @var AuthSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + $sanitizer = $this->container->get(AuthSanitizer::class); $faker = FakerFactory::create(); // Build an input with many fields present diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerTest.php similarity index 89% rename from tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerTest.php index 3d0d693..9859430 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizers/AuthSanitizerTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerTest.php @@ -11,11 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\Sanitizers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\Sanitizer; use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizer; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Filter\Filter; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php index d210cb9..98f8d33 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/DTO/UserInputTest.php @@ -14,9 +14,8 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\DTO; use Faker\Factory as FakerFactory; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; use Phalcon\Api\Tests\AbstractUnitTestCase; use function json_encode; @@ -26,10 +25,9 @@ final class UserInputTest extends AbstractUnitTestCase public function testToArray(): void { /** @var UserSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::USER_SANITIZER); + $sanitizer = $this->container->get(UserSanitizer::class); $faker = FakerFactory::create(); - // Build an input with many fields present $input = [ 'id' => $faker->numberBetween(1, 1000), 'status' => $faker->numberBetween(0, 9), @@ -44,9 +42,9 @@ public function testToArray(): void 'tokenPassword' => $faker->password(), 'tokenId' => $faker->uuid(), 'preferences' => json_encode(['k' => $faker->word()]), - 'createdDate' => $faker->date('Y-m-d'), + 'createdDate' => $faker->date(), 'createdUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast - 'updatedDate' => $faker->date('Y-m-d'), + 'updatedDate' => $faker->date(), 'updatedUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast ]; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Mapper/UserMapperTest.php similarity index 82% rename from tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Mapper/UserMapperTest.php index 4a03d93..6e0040a 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Mappers/UserMapperTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Mapper/UserMapperTest.php @@ -11,12 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Mappers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Mapper; use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; use Phalcon\Api\Domain\Infrastructure\Constants\Dates; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Tests\AbstractUnitTestCase; use function json_encode; @@ -25,7 +25,9 @@ final class UserMapperTest extends AbstractUnitTestCase { public function testDb(): void { - $faker = FakerFactory::create(); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + $faker = FakerFactory::create(); $preferences = [ 'theme' => $faker->randomElement(['dark', 'light']), @@ -36,27 +38,30 @@ public function testDb(): void $createdDate = $faker->date(Dates::DATE_TIME_FORMAT); $updatedDate = $faker->date(Dates::DATE_TIME_FORMAT); - $user = new UserInput( - $faker->numberBetween(1, 1000), - $faker->numberBetween(0, 9), - $faker->safeEmail(), - $faker->password(), - $faker->optional()->title(), - $faker->firstName(), - $faker->optional()->word(), - $faker->lastName(), - $faker->optional()->suffix(), - $faker->company(), - $faker->optional()->password(), - $faker->uuid(), - $preferencesJson, - $createdDate, - $faker->numberBetween(1, 1000), - $updatedDate, - $faker->numberBetween(1, 1000) - ); - - $mapper = new UserMapper(); + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + 'namePrefix' => $faker->optional()->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->optional()->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => $faker->optional()->suffix(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->optional()->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => $preferencesJson, + 'createdDate' => $createdDate, + 'createdUserId' => $faker->numberBetween(1, 1000), + 'updatedDate' => $updatedDate, + 'updatedUserId' => $faker->numberBetween(1, 1000), + ]; + + $user = $factory->update($input); + + /** @var UserMapper $mapper */ + $mapper = $this->container->get(UserMapper::class); $row = $mapper->db($user); $expected = [ @@ -86,7 +91,7 @@ public function testDb(): void public function testDomain(): void { $faker = FakerFactory::create(); - $mapper = new UserMapper(); + $mapper = $this->container->get(UserMapper::class); // Empty row: defaults should be applied $emptyUser = $mapper->domain([]); @@ -162,7 +167,7 @@ public function testDomain(): void public function testInput(): void { $faker = FakerFactory::create(); - $mapper = new UserMapper(); + $mapper = $this->container->get(UserMapper::class); $preferences = ['k' => $faker->word()]; $preferencesJson = json_encode($preferences); @@ -181,9 +186,9 @@ public function testInput(): void 'tokenPassword' => $faker->optional()->password(), 'tokenId' => $faker->uuid(), 'preferences' => $preferencesJson, - 'createdDate' => $faker->date('Y-m-d'), + 'createdDate' => $faker->date(), 'createdUserId' => $faker->numberBetween(1, 1000), - 'updatedDate' => $faker->date('Y-m-d'), + 'updatedDate' => $faker->date(), 'updatedUserId' => $faker->numberBetween(1, 1000), ]; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Repository/UserRepositoryTest.php similarity index 92% rename from tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Repository/UserRepositoryTest.php index 07d0070..aaf8a25 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Repositories/UserRepositoryTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Repository/UserRepositoryTest.php @@ -11,11 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Repositories; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Repository; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; @@ -24,7 +23,7 @@ final class UserRepositoryTest extends AbstractUnitTestCase public function testFindByEmail(): void { /** @var UserRepository $repository */ - $repository = $this->container->get(Container::USER_REPOSITORY); + $repository = $this->container->get(UserRepository::class); $migration = new UsersMigration($this->getConnection()); @@ -42,7 +41,7 @@ public function testFindByEmail(): void public function testFindById(): void { /** @var UserRepository $repository */ - $repository = $this->container->get(Container::USER_REPOSITORY); + $repository = $this->container->get(UserRepository::class); $migration = new UsersMigration($this->getConnection()); diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php similarity index 97% rename from tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php index 60964a4..9e63b12 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizers/UserSanitizerTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Sanitizers; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Sanitizer; use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Filter\Filter; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdateTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdateTest.php new file mode 100644 index 0000000..4cbe259 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdateTest.php @@ -0,0 +1,88 @@ + + * + * 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\Infrastructure\DataSource\User\Validator; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserValidatorUpdate; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class UserValidatorUpdateTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var UserSanitizer $sanitizer */ + $sanitizer = $this->container->get(UserSanitizer::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + /** @var UserValidatorUpdate $validator */ + $validator = $this->container->get(UserValidatorUpdate::class); + + $input = []; + $userInput = $factory->update($input); + + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + ['Field id is not a valid absolute integer and greater than 0'], + ]; + + $this->assertSame($expected, $actual); + + $input = [ + 'id' => 1, + 'email' => 'not-email', + ]; + $userInput = $factory->update($input); + + /** @var UserValidatorUpdate $validator */ + $validator = $this->container->get(UserValidatorUpdate::class); + + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = [ + ['Field email must be an email address'], + ]; + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + /** @var UserValidatorUpdate $validator */ + $validator = $this->container->get(UserValidatorUpdate::class); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 100), + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + ]; + + $userInput = $factory->update($input); + + $result = $validator->validate($userInput); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php deleted file mode 100644 index afab4ff..0000000 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorUpdateTest.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * 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\Infrastructure\DataSource\User\Validators; - -use Faker\Factory as FakerFactory; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidatorUpdate; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Validation\ValidationInterface; - -final class UserValidatorUpdateTest extends AbstractUnitTestCase -{ - public function testError(): void - { - /** @var UserSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::USER_SANITIZER); - /** @var ValidationInterface $validation */ - $validation = $this->container->get(Container::VALIDATION); - - $input = []; - $userInput = UserInput::new($sanitizer, $input); - - $validator = new UserValidatorUpdate($validation); - $result = $validator->validate($userInput); - $actual = $result->getErrors(); - - $expected = [ - ['Field id is not a valid absolute integer and greater than 0'], - ['Field email is required'], - ['Field email must be an email address'], - ['Field password is required'], - ['Field issuer is required'], - ['Field tokenPassword is required'], - ['Field tokenId is required'], - ]; - - $this->assertSame($expected, $actual); - } - - public function testSuccess(): void - { - /** @var UserSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::USER_SANITIZER); - /** @var ValidationInterface $validation */ - $validation = $this->container->get(Container::VALIDATION); - $faker = FakerFactory::create(); - - $input = [ - 'id' => $faker->numberBetween(1, 100), - 'email' => $faker->safeEmail(), - 'password' => $faker->password(), - 'issuer' => $faker->company(), - 'tokenPassword' => $faker->password(), - 'tokenId' => $faker->uuid(), - ]; - - $userInput = UserInput::new($sanitizer, $input); - - $validator = new UserValidatorUpdate($validation); - $result = $validator->validate($userInput); - $actual = $result->getErrors(); - - $expected = []; - $this->assertSame($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php index 63a60bb..4bf619f 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/Validation/ResultTest.php @@ -18,18 +18,6 @@ final class ResultTest extends AbstractUnitTestCase { - public function testSuccessIsValidAndHasNoErrors(): void - { - $result = Result::success(); - - $actual = $result->isValid(); - $this->assertTrue($actual); - - $expected = []; - $actual = $result->getErrors(); - $this->assertSame($expected, $actual); - } - public function testErrorIsInvalidAndReturnsErrors(): void { $errors = ['field' => ['must not be empty']]; @@ -48,7 +36,7 @@ public function testGetMetaReturnsDefaultWhenMissing(): void $actual = $result->getMeta('present'); $this->assertSame($expected, $actual); - $actual = $result->getMeta('uknown'); + $actual = $result->getMeta('uknown'); $this->assertNull($actual); $expected = 'default'; @@ -60,7 +48,7 @@ public function testSetMetaAddsOrUpdatesMeta(): void { $result = new Result(); - $actual = $result->getMeta('key'); + $actual = $result->getMeta('key'); $this->assertNull($actual); $result->setMeta('key', 'value'); @@ -75,4 +63,16 @@ public function testSetMetaAddsOrUpdatesMeta(): void $actual = $result->getMeta('key'); $this->assertSame($expected, $actual); } + + public function testSuccessIsValidAndHasNoErrors(): void + { + $result = Result::success(); + + $actual = $result->isValid(); + $this->assertTrue($actual); + + $expected = []; + $actual = $result->getErrors(); + $this->assertSame($expected, $actual); + } } diff --git a/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php b/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php index d505046..5913ff9 100644 --- a/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/SecurityTest.php @@ -13,7 +13,6 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Encryption\Security; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -22,7 +21,7 @@ final class SecurityTest extends AbstractUnitTestCase public function testSecurity(): void { /** @var Security $security */ - $security = $this->container->get(Container::SECURITY); + $security = $this->container->get(Security::class); $password = $this->getStrongPassword(); $hashed = $security->hash($password); diff --git a/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php b/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php index 411ab54..ac87357 100644 --- a/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/TokenCacheTest.php @@ -14,7 +14,6 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; @@ -29,11 +28,11 @@ final class TokenCacheTest extends AbstractUnitTestCase public function testStoreAndInvalidateForUser(): void { /** @var EnvManager $env */ - $env = $this->container->get(Container::ENV); + $env = $this->container->get(EnvManager::class); /** @var Cache $cache */ - $cache = $this->container->get(Container::CACHE); + $cache = $this->container->get(Cache::class); /** @var TokenCache $tokenCache */ - $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + $tokenCache = $this->container->get(TokenCache::class); /** * Empty cache diff --git a/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php index df14ca9..84d1788 100644 --- a/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php @@ -13,8 +13,21 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Enums\Http; -use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLogoutPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthRefreshPostService; +use Phalcon\Api\Domain\Application\User\Service\UserDeleteService; +use Phalcon\Api\Domain\Application\User\Service\UserGetService; +use Phalcon\Api\Domain\Application\User\Service\UserPostService; +use Phalcon\Api\Domain\Application\User\Service\UserPutService; use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Infrastructure\Middleware\HealthMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\NotFoundMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenClaimsMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenPresenceMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenRevokedMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenStructureMiddleware; +use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenUserMiddleware; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -29,7 +42,7 @@ public static function getExamples(): array '/login', '/auth/login', RoutesEnum::POST, - Container::AUTH_LOGIN_POST_SERVICE, + AuthLoginPostService::class, ], [ RoutesEnum::authLogoutPost, @@ -37,7 +50,7 @@ public static function getExamples(): array '/logout', '/auth/logout', RoutesEnum::POST, - Container::AUTH_LOGOUT_POST_SERVICE, + AuthLogoutPostService::class, ], [ RoutesEnum::authRefreshPost, @@ -45,7 +58,7 @@ public static function getExamples(): array '/refresh', '/auth/refresh', RoutesEnum::POST, - Container::AUTH_REFRESH_POST_SERVICE, + AuthRefreshPostService::class, ], [ RoutesEnum::userDelete, @@ -53,7 +66,7 @@ public static function getExamples(): array '', '/user', RoutesEnum::DELETE, - Container::USER_DELETE_SERVICE, + UserDeleteService::class, ], [ RoutesEnum::userGet, @@ -61,7 +74,7 @@ public static function getExamples(): array '', '/user', RoutesEnum::GET, - Container::USER_GET_SERVICE, + UserGetService::class, ], [ RoutesEnum::userPost, @@ -69,7 +82,7 @@ public static function getExamples(): array '', '/user', RoutesEnum::POST, - Container::USER_POST_SERVICE, + UserPostService::class, ], [ RoutesEnum::userPut, @@ -77,7 +90,7 @@ public static function getExamples(): array '', '/user', RoutesEnum::PUT, - Container::USER_PUT_SERVICE, + UserPutService::class, ], ]; } @@ -122,13 +135,13 @@ public function testCheckItems( public function testMiddleware(): void { $expected = [ - Container::MIDDLEWARE_NOT_FOUND => RoutesEnum::EVENT_BEFORE, - Container::MIDDLEWARE_HEALTH => RoutesEnum::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_PRESENCE => RoutesEnum::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE => RoutesEnum::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_USER => RoutesEnum::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_CLAIMS => RoutesEnum::EVENT_BEFORE, - Container::MIDDLEWARE_VALIDATE_TOKEN_REVOKED => RoutesEnum::EVENT_BEFORE, + NotFoundMiddleware::class => RoutesEnum::EVENT_BEFORE, + HealthMiddleware::class => RoutesEnum::EVENT_BEFORE, + ValidateTokenPresenceMiddleware::class => RoutesEnum::EVENT_BEFORE, + ValidateTokenStructureMiddleware::class => RoutesEnum::EVENT_BEFORE, + ValidateTokenUserMiddleware::class => RoutesEnum::EVENT_BEFORE, + ValidateTokenClaimsMiddleware::class => RoutesEnum::EVENT_BEFORE, + ValidateTokenRevokedMiddleware::class => RoutesEnum::EVENT_BEFORE, ]; $actual = RoutesEnum::middleware(); $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php b/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php index e4703a7..7377527 100644 --- a/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php +++ b/tests/Unit/Domain/Infrastructure/Env/Adapters/DotEnvTest.php @@ -13,7 +13,6 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Env\Adapters; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Env\Adapters\DotEnv; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Domain\Infrastructure\Exceptions\InvalidConfigurationArgumentException; @@ -28,10 +27,10 @@ public function setUp(): void parent::setUp(); /** @var EnvManager $env */ - $env = $this->container->get(Container::ENV); + $env = $this->container->get(EnvManager::class); $this->envFile = $env->appPath() - . '/tests/Fixtures/Domain/Components/Env/'; + . '/tests/Fixtures/Domain/Infrastructure/Env/'; } public function testLoadExceptionForEmptyFilePath(): void diff --git a/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php b/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php index 3ff8f53..f5484f5 100644 --- a/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php +++ b/tests/Unit/Domain/Infrastructure/Env/EnvManagerTest.php @@ -13,7 +13,6 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Env; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -24,7 +23,7 @@ final class EnvManagerTest extends AbstractUnitTestCase public function testAppEnvReturnsDefault(): void { /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $expected = 'development'; $actual = $env->appEnv(); @@ -36,7 +35,7 @@ public function testAppEnvReturnsValue(): void $_ENV = ['APP_ENV' => 'production']; /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $expected = 'production'; $actual = $env->appEnv(); @@ -46,7 +45,7 @@ public function testAppEnvReturnsValue(): void public function testAppLogLevelReturnsDefault(): void { /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $expected = 1; $actual = $env->appLogLevel(); @@ -58,7 +57,7 @@ public function testAppLogLevelReturnsValue(): void $_ENV = ['APP_LOG_LEVEL' => 5]; /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $expected = 5; $actual = $env->appLogLevel(); @@ -68,7 +67,7 @@ public function testAppLogLevelReturnsValue(): void public function testAppPathReturnsRoot(): void { /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $expected = dirname(__DIR__, 5); $actual = $env->appPath(); @@ -78,7 +77,7 @@ public function testAppPathReturnsRoot(): void public function testAppTimezoneReturnsDefault(): void { /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $expected = 'UTC'; $actual = $env->appTimezone(); @@ -90,7 +89,7 @@ public function testAppTimezoneReturnsValue(): void $_ENV = ['APP_TIMEZONE' => 'America/Los_Angeles']; /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $expected = 'America/Los_Angeles'; $actual = $env->appTimezone(); @@ -100,12 +99,12 @@ public function testAppTimezoneReturnsValue(): void public function testGetFromDotEnvLoad(): void { /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $_ENV = [ 'APP_ENV_ADAPTER' => 'dotenv', 'APP_ENV_FILE_PATH' => $env->appPath() - . '/tests/Fixtures/Domain/Components/Env/', + . '/tests/Fixtures/Domain/Infrastructure/Env/', ]; $env->load(); diff --git a/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php index e8c8f2b..36d1dad 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/HealthMiddlewareTest.php @@ -27,7 +27,7 @@ final class HealthMiddlewareTest extends AbstractUnitTestCase public function testCall(): void { $application = new Micro($this->container); - $middleware = new HealthMiddleware(); + $middleware = $this->container->get(HealthMiddleware::class); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ diff --git a/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php b/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php index e258055..8d0ce4c 100644 --- a/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php +++ b/tests/Unit/Domain/Infrastructure/Middleware/NotFoundMiddlewareTest.php @@ -29,7 +29,7 @@ final class NotFoundMiddlewareTest extends AbstractUnitTestCase public function testBeforeNotFound(): void { $application = new Micro($this->container); - $middleware = new NotFoundMiddleware(); + $middleware = $this->container->get(NotFoundMiddleware::class); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ @@ -57,7 +57,7 @@ public function testBeforeNotFound(): void public function testCall(): void { $application = new Micro($this->container); - $middleware = new NotFoundMiddleware(); + $middleware = $this->container->get(NotFoundMiddleware::class); $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); $_SERVER = [ diff --git a/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php index 3bbbc81..851a4c2 100644 --- a/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/CacheDataProviderTest.php @@ -13,7 +13,6 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Providers; -use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Cache\Cache; @@ -22,7 +21,7 @@ final class CacheDataProviderTest extends AbstractUnitTestCase public function testCheckRegistration(): void { $expected = Cache::class; - $actual = $this->container->get(Container::CACHE); + $actual = $this->container->get(Cache::class); $this->assertInstanceOf($expected, $actual); } } diff --git a/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php index 6992abe..4937535 100644 --- a/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/ErrorHandlerProviderTest.php @@ -44,7 +44,7 @@ function () use ($now) { ); /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $provider = new ErrorHandlerProvider(); $provider->register($this->container); @@ -76,7 +76,7 @@ function () use ($now) { ); /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); $provider = new ErrorHandlerProvider(); $provider->register($this->container); @@ -111,9 +111,9 @@ function () use ($now) { $_ENV['APP_LOG_LEVEL'] = 2; /** @var Logger $logger */ - $logger = $this->container->getShared(Container::LOGGER); + $logger = $this->container->getShared(Logger::class); /** @var EnvManager $env */ - $env = $this->container->getShared(Container::ENV); + $env = $this->container->getShared(EnvManager::class); /** @var string $logName */ $logName = $env->get('LOG_FILENAME', 'rest-api'); diff --git a/tests/Unit/Responder/JsonResponderTest.php b/tests/Unit/Responder/JsonResponderTest.php index 91d82f9..d3b84fe 100644 --- a/tests/Unit/Responder/JsonResponderTest.php +++ b/tests/Unit/Responder/JsonResponderTest.php @@ -13,11 +13,12 @@ namespace Phalcon\Api\Tests\Unit\Responder; +use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Responder\JsonResponder; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Http\ResponseInterface; use PHPUnit\Framework\Attributes\BackupGlobals; @@ -34,7 +35,7 @@ public function testFailure(): void /** @var ResponseInterface $response */ $response = $this->container->getShared(Container::RESPONSE); /** @var ResponderInterface $responder */ - $responder = $this->container->get(Container::RESPONDER_JSON); + $responder = $this->container->get(JsonResponder::class); $errorContent = uniqid('error-'); $payload = Payload::unauthorized([[1234 => $errorContent]]); @@ -101,7 +102,7 @@ public function testSuccess(): void /** @var ResponseInterface $response */ $response = $this->container->getShared(Container::RESPONSE); /** @var ResponderInterface $responder */ - $responder = $this->container->getShared(Container::RESPONDER_JSON); + $responder = $this->container->getShared(JsonResponder::class); $dataContent = uniqid('data-'); $payload = Payload::success([$dataContent]); From fac4d8d7c5ad24cd6ef1af6b113a3eef067b5d65 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:04:26 -0600 Subject: [PATCH 223/301] [#.x] - added allowEmpty for validators --- .../AuthLoginValidatorEnum.php | 9 +++++++-- .../AuthTokenValidatorEnum.php | 7 ++++++- .../{Validators => Validator}/UserInsertEnum.php | 13 +++++++++---- .../ValidatorEnumInterface.php | 7 ++++++- 4 files changed, 28 insertions(+), 8 deletions(-) rename src/Domain/Infrastructure/Enums/{Validators => Validator}/AuthLoginValidatorEnum.php (80%) rename src/Domain/Infrastructure/Enums/{Validators => Validator}/AuthTokenValidatorEnum.php (78%) rename src/Domain/Infrastructure/Enums/{Validators => Validator}/UserInsertEnum.php (72%) rename src/Domain/Infrastructure/Enums/{Validators => Validator}/ValidatorEnumInterface.php (75%) diff --git a/src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php b/src/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnum.php similarity index 80% rename from src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php rename to src/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnum.php index cc536cf..3043b4c 100644 --- a/src/Domain/Infrastructure/Enums/Validators/AuthLoginValidatorEnum.php +++ b/src/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validator; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; @@ -21,12 +21,17 @@ enum AuthLoginValidatorEnum implements ValidatorEnumInterface case email; case password; + public function allowEmpty(): bool + { + return false; + } + public function validators(): array { return match ($this) { self::email => [ PresenceOf::class, - Email::class + Email::class, ], self::password => [PresenceOf::class], }; diff --git a/src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php b/src/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnum.php similarity index 78% rename from src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php rename to src/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnum.php index 70b7df4..0ba003b 100644 --- a/src/Domain/Infrastructure/Enums/Validators/AuthTokenValidatorEnum.php +++ b/src/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validator; use Phalcon\Filter\Validation\Validator\PresenceOf; @@ -19,6 +19,11 @@ enum AuthTokenValidatorEnum implements ValidatorEnumInterface { case token; + public function allowEmpty(): bool + { + return false; + } + public function validators(): array { return [PresenceOf::class]; diff --git a/src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php b/src/Domain/Infrastructure/Enums/Validator/UserInsertEnum.php similarity index 72% rename from src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php rename to src/Domain/Infrastructure/Enums/Validator/UserInsertEnum.php index f9a11f7..f91b54c 100644 --- a/src/Domain/Infrastructure/Enums/Validators/UserInsertEnum.php +++ b/src/Domain/Infrastructure/Enums/Validator/UserInsertEnum.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validator; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; @@ -24,14 +24,19 @@ enum UserInsertEnum implements ValidatorEnumInterface case tokenPassword; case tokenId; + public function allowEmpty(): bool + { + return false; + } + public function validators(): array { return match ($this) { - self::email => [ + self::email => [ PresenceOf::class, - Email::class + Email::class, ], - default => [PresenceOf::class], + default => [PresenceOf::class], }; } } diff --git a/src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php b/src/Domain/Infrastructure/Enums/Validator/ValidatorEnumInterface.php similarity index 75% rename from src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php rename to src/Domain/Infrastructure/Enums/Validator/ValidatorEnumInterface.php index 3821a33..50ccb95 100644 --- a/src/Domain/Infrastructure/Enums/Validators/ValidatorEnumInterface.php +++ b/src/Domain/Infrastructure/Enums/Validator/ValidatorEnumInterface.php @@ -11,12 +11,17 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Validators; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Validator; use UnitEnum; interface ValidatorEnumInterface extends UnitEnum { + /** + * @return bool + */ + public function allowEmpty(): bool; + /** * @return array */ From dd2affd01ac0abfeef14f5dedd33529c7833e7d8 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:04:58 -0600 Subject: [PATCH 224/301] [#.x] - added getErrorMessage to the manager --- .../Encryption/TokenManager.php | 21 +++++++++++++++++-- .../Encryption/TokenManagerInterface.php | 9 +++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Domain/Infrastructure/Encryption/TokenManager.php b/src/Domain/Infrastructure/Encryption/TokenManager.php index af67d06..abe5d33 100644 --- a/src/Domain/Infrastructure/Encryption/TokenManager.php +++ b/src/Domain/Infrastructure/Encryption/TokenManager.php @@ -15,7 +15,7 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Encryption\Security\JWT\Token\Token; use Throwable; @@ -29,6 +29,13 @@ */ final class TokenManager implements TokenManagerInterface { + private string $errorMessage = ''; + + /** + * @param TokenCacheInterface $tokenCache + * @param EnvManager $env + * @param JWTToken $jwtToken + */ public function __construct( private readonly TokenCacheInterface $tokenCache, private readonly EnvManager $env, @@ -36,6 +43,14 @@ public function __construct( ) { } + /** + * @return string + */ + public function getErrorMessage(): string + { + return $this->errorMessage; + } + /** * Parse a token and return either null or a Token object * @@ -50,8 +65,10 @@ public function getObject(?string $token): ?Token } try { + $this->errorMessage = ''; return $this->jwtToken->getObject($token); - } catch (Throwable) { + } catch (Throwable $ex) { + $this->errorMessage = $ex->getMessage(); return null; } } diff --git a/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php b/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php index 48386fc..ea3d78e 100644 --- a/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php +++ b/src/Domain/Infrastructure/Encryption/TokenManagerInterface.php @@ -15,7 +15,7 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; use Phalcon\Encryption\Security\JWT\Token\Token; /** @@ -28,6 +28,13 @@ */ interface TokenManagerInterface { + /** + * Returns the last error message + * + * @return string + */ + public function getErrorMessage(): string; + /** * Parse a token and return either null or a Token object * From a4881e60f83f81aefa2412bdd56f83eef8da736c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:07:20 -0600 Subject: [PATCH 225/301] [#.x] - refactoring folders --- .../User => Application/User/Service}/UserDeleteService.php | 2 +- .../User => Application/User/Service}/UserGetService.php | 2 +- .../Enums/{Sanitizers => Sanitizer}/SanitizersEnumInterface.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/Domain/{Services/User => Application/User/Service}/UserDeleteService.php (92%) rename src/Domain/{Services/User => Application/User/Service}/UserGetService.php (92%) rename src/Domain/Infrastructure/Enums/{Sanitizers => Sanitizer}/SanitizersEnumInterface.php (86%) diff --git a/src/Domain/Services/User/UserDeleteService.php b/src/Domain/Application/User/Service/UserDeleteService.php similarity index 92% rename from src/Domain/Services/User/UserDeleteService.php rename to src/Domain/Application/User/Service/UserDeleteService.php index 4ed07d0..9148f9e 100644 --- a/src/Domain/Services/User/UserDeleteService.php +++ b/src/Domain/Application/User/Service/UserDeleteService.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\User; +namespace Phalcon\Api\Domain\Application\User\Service; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; diff --git a/src/Domain/Services/User/UserGetService.php b/src/Domain/Application/User/Service/UserGetService.php similarity index 92% rename from src/Domain/Services/User/UserGetService.php rename to src/Domain/Application/User/Service/UserGetService.php index 433ea29..08395f2 100644 --- a/src/Domain/Services/User/UserGetService.php +++ b/src/Domain/Application/User/Service/UserGetService.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\User; +namespace Phalcon\Api\Domain\Application\User\Service; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; diff --git a/src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php b/src/Domain/Infrastructure/Enums/Sanitizer/SanitizersEnumInterface.php similarity index 86% rename from src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php rename to src/Domain/Infrastructure/Enums/Sanitizer/SanitizersEnumInterface.php index 0738cfd..bdc84f8 100644 --- a/src/Domain/Infrastructure/Enums/Sanitizers/SanitizersEnumInterface.php +++ b/src/Domain/Infrastructure/Enums/Sanitizer/SanitizersEnumInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizers; +namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer; use UnitEnum; From f50e2f2d11b89b930f7db9954a96f43782f62e7d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:08:32 -0600 Subject: [PATCH 226/301] [#.x] - new command bus and handler locator and tests --- .../Infrastructure/CommandBus/CommandBus.php | 39 +++++++++++++ .../CommandBus/CommandBusInterface.php | 21 +++++++ .../CommandBus/CommandInterface.php | 18 ++++++ .../CommandBus/ContainerHandlerLocator.php | 55 +++++++++++++++++++ .../CommandBus/HandlerInterface.php | 21 +++++++ .../CommandBus/HandlerLocatorInterface.php | 24 ++++++++ .../Exceptions/HandlerRuntimeException.php | 21 +++++++ .../CommandBus/CommandBusTest.php | 51 +++++++++++++++++ .../ContainerHandlerLocatorTest.php | 43 +++++++++++++++ 9 files changed, 293 insertions(+) create mode 100644 src/Domain/Infrastructure/CommandBus/CommandBus.php create mode 100644 src/Domain/Infrastructure/CommandBus/CommandBusInterface.php create mode 100644 src/Domain/Infrastructure/CommandBus/CommandInterface.php create mode 100644 src/Domain/Infrastructure/CommandBus/ContainerHandlerLocator.php create mode 100644 src/Domain/Infrastructure/CommandBus/HandlerInterface.php create mode 100644 src/Domain/Infrastructure/CommandBus/HandlerLocatorInterface.php create mode 100644 src/Domain/Infrastructure/Exceptions/HandlerRuntimeException.php create mode 100644 tests/Unit/Domain/Infrastructure/CommandBus/CommandBusTest.php create mode 100644 tests/Unit/Domain/Infrastructure/CommandBus/ContainerHandlerLocatorTest.php diff --git a/src/Domain/Infrastructure/CommandBus/CommandBus.php b/src/Domain/Infrastructure/CommandBus/CommandBus.php new file mode 100644 index 0000000..895e790 --- /dev/null +++ b/src/Domain/Infrastructure/CommandBus/CommandBus.php @@ -0,0 +1,39 @@ + + * + * 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\Infrastructure\CommandBus; + +use Phalcon\Api\Domain\ADR\Payload; + +final readonly class CommandBus implements CommandBusInterface +{ + /** + * @param HandlerLocatorInterface $locator + */ + public function __construct( + private HandlerLocatorInterface $locator, + ) { + } + + /** + * @param CommandInterface $command + * + * @return Payload + */ + public function dispatch(CommandInterface $command): Payload + { + $handler = $this->locator->resolve($command); + + return $handler($command); + } +} diff --git a/src/Domain/Infrastructure/CommandBus/CommandBusInterface.php b/src/Domain/Infrastructure/CommandBus/CommandBusInterface.php new file mode 100644 index 0000000..1e43b37 --- /dev/null +++ b/src/Domain/Infrastructure/CommandBus/CommandBusInterface.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\Infrastructure\CommandBus; + +use Phalcon\Api\Domain\ADR\Payload; + +interface CommandBusInterface +{ + public function dispatch(CommandInterface $command): Payload; +} diff --git a/src/Domain/Infrastructure/CommandBus/CommandInterface.php b/src/Domain/Infrastructure/CommandBus/CommandInterface.php new file mode 100644 index 0000000..c0c4fad --- /dev/null +++ b/src/Domain/Infrastructure/CommandBus/CommandInterface.php @@ -0,0 +1,18 @@ + + * + * 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\Infrastructure\CommandBus; + +interface CommandInterface +{ +} diff --git a/src/Domain/Infrastructure/CommandBus/ContainerHandlerLocator.php b/src/Domain/Infrastructure/CommandBus/ContainerHandlerLocator.php new file mode 100644 index 0000000..adf46ee --- /dev/null +++ b/src/Domain/Infrastructure/CommandBus/ContainerHandlerLocator.php @@ -0,0 +1,55 @@ + + * + * 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\Infrastructure\CommandBus; + +use Phalcon\Api\Domain\Infrastructure\Exceptions\HandlerRuntimeException; +use Phalcon\Di\DiInterface; + +final readonly class ContainerHandlerLocator implements HandlerLocatorInterface +{ + /** + * @param DiInterface $container + */ + public function __construct( + private DiInterface $container, + ) { + } + + /** + * @param CommandInterface $command + * + * @return HandlerInterface + */ + public function resolve(CommandInterface $command): HandlerInterface + { + $commandClass = get_class($command); + + $handlerClass = str_replace( + 'Command', + 'Handler', + $commandClass + ); + + if (true !== $this->container->has($handlerClass)) { + throw HandlerRuntimeException::new( + "No handler configured for $commandClass" + ); + } + + /** @var HandlerInterface $handler */ + $handler = $this->container->get($handlerClass); + + return $handler; + } +} diff --git a/src/Domain/Infrastructure/CommandBus/HandlerInterface.php b/src/Domain/Infrastructure/CommandBus/HandlerInterface.php new file mode 100644 index 0000000..41e5a98 --- /dev/null +++ b/src/Domain/Infrastructure/CommandBus/HandlerInterface.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\Infrastructure\CommandBus; + +use Phalcon\Api\Domain\ADR\Payload; + +interface HandlerInterface +{ + public function __invoke(CommandInterface $command): Payload; +} diff --git a/src/Domain/Infrastructure/CommandBus/HandlerLocatorInterface.php b/src/Domain/Infrastructure/CommandBus/HandlerLocatorInterface.php new file mode 100644 index 0000000..a5bc31e --- /dev/null +++ b/src/Domain/Infrastructure/CommandBus/HandlerLocatorInterface.php @@ -0,0 +1,24 @@ + + * + * 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\Infrastructure\CommandBus; + +interface HandlerLocatorInterface +{ + /** + * @param CommandInterface $command + * + * @return HandlerInterface + */ + public function resolve(CommandInterface $command): HandlerInterface; +} diff --git a/src/Domain/Infrastructure/Exceptions/HandlerRuntimeException.php b/src/Domain/Infrastructure/Exceptions/HandlerRuntimeException.php new file mode 100644 index 0000000..40bd652 --- /dev/null +++ b/src/Domain/Infrastructure/Exceptions/HandlerRuntimeException.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\Infrastructure\Exceptions; + +use InvalidArgumentException; + +class HandlerRuntimeException extends InvalidArgumentException +{ + use ExceptionTrait; +} diff --git a/tests/Unit/Domain/Infrastructure/CommandBus/CommandBusTest.php b/tests/Unit/Domain/Infrastructure/CommandBus/CommandBusTest.php new file mode 100644 index 0000000..bf9ee80 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/CommandBus/CommandBusTest.php @@ -0,0 +1,51 @@ + + * + * 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\Infrastructure\CommandBus; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Application\Command\CommandFixture; +use Phalcon\Api\Tests\Fixtures\Domain\Application\Handler\HandlerFixture; + +use function uniqid; + +final class CommandBusTest extends AbstractUnitTestCase +{ + public function testDispatch(): void + { + $name = uniqid('name-'); + $command = new CommandFixture($name); + /** + * Register services + */ + $this->container->set(HandlerFixture::class, HandlerFixture::class); + + $bus = $this->container->get(CommandBus::class); + + $result = $bus->dispatch($command); + + $expected = Payload::class; + $actual = $result; + $this->assertInstanceOf($expected, $actual); + + $expected = [ + 'data' => [ + "Name: $name", + ], + ]; + $actual = $result->getResult(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/CommandBus/ContainerHandlerLocatorTest.php b/tests/Unit/Domain/Infrastructure/CommandBus/ContainerHandlerLocatorTest.php new file mode 100644 index 0000000..89fd346 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/CommandBus/ContainerHandlerLocatorTest.php @@ -0,0 +1,43 @@ + + * + * 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\Infrastructure\CommandBus; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\ContainerHandlerLocator; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Application\Command\CommandFixture; +use Phalcon\Api\Tests\Fixtures\Domain\Application\Handler\HandlerFixture; + +use function uniqid; + +final class ContainerHandlerLocatorTest extends AbstractUnitTestCase +{ + public function testDispatch(): void + { + $name = uniqid('name-'); + $command = new CommandFixture($name); + /** + * Register services + */ + $this->container->set(HandlerFixture::class, HandlerFixture::class); + + /** @var ContainerHandlerLocator $locator */ + $locator = $this->container->get(ContainerHandlerLocator::class); + + $result = $locator->resolve($command); + + $expected = HandlerFixture::class; + $actual = $result; + $this->assertInstanceOf($expected, $actual); + } +} From 07e831360a607dbe14caa94e47284139879d6d56 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:09:02 -0600 Subject: [PATCH 227/301] [#.x] - refactored folders --- .../DataSource/Interface/MapperInterface.php | 49 +++++++++++++++++++ .../Interface/SanitizerInterface.php | 31 ++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Interface/MapperInterface.php create mode 100644 src/Domain/Infrastructure/DataSource/Interface/SanitizerInterface.php diff --git a/src/Domain/Infrastructure/DataSource/Interface/MapperInterface.php b/src/Domain/Infrastructure/DataSource/Interface/MapperInterface.php new file mode 100644 index 0000000..edf829a --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Interface/MapperInterface.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\Infrastructure\DataSource\Interface; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +/** + * Contract for mapping between domain DTO/objects and persistence arrays. + * + * @phpstan-import-type TUser from UserTypes + * @phpstan-import-type TUserDbRecord from UserTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + */ +interface MapperInterface +{ + /** + * Map Domain User -> DB row (usr_*) + * + * @return TUserDomainToDbRecord + */ + public function db(CommandInterface $user): array; + + /** + * Map DB row (usr_*) -> Domain User + * + * @param TUserDbRecord|array{} $row + */ + public function domain(array $row): User; + + /** + * Map input row -> Domain User + * + * @param TUser $row + */ + public function input(array $row): User; +} diff --git a/src/Domain/Infrastructure/DataSource/Interface/SanitizerInterface.php b/src/Domain/Infrastructure/DataSource/Interface/SanitizerInterface.php new file mode 100644 index 0000000..6441fff --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Interface/SanitizerInterface.php @@ -0,0 +1,31 @@ + + * + * 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\Infrastructure\DataSource\Interface; + +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TInputSanitize from InputTypes + */ +interface SanitizerInterface +{ + /** + * Return a sanitized array of the input + * + * @param TInputSanitize $input + * + * @return TInputSanitize + */ + public function sanitize(array $input): array; +} From b9722bcc4e6831e674b9a07061569b906f63b5ae Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:10:21 -0600 Subject: [PATCH 228/301] [#.x] - refactored validators to use commands --- .../AuthLoginValidator.php | 12 ++-- .../AuthTokenValidator.php | 22 +++--- .../UserValidatorTrait.php | 14 ++-- .../Validation/AbstractValidator.php | 20 +++--- .../Validation/ValidatorInterface.php | 6 +- .../Auth/Validator/AuthLoginValidatorTest.php | 64 +++++++++++++++++ .../AuthTokenValidatorTest.php | 62 ++++++++--------- .../Validators/AuthLoginValidatorTest.php | 69 ------------------- .../UserValidatorTest.php | 39 +++++------ 9 files changed, 153 insertions(+), 155 deletions(-) rename src/Domain/Infrastructure/DataSource/Auth/{Validators => Validator}/AuthLoginValidator.php (76%) rename src/Domain/Infrastructure/DataSource/Auth/{Validators => Validator}/AuthTokenValidator.php (78%) rename src/Domain/Infrastructure/DataSource/User/{Validators => Validator}/UserValidatorTrait.php (68%) create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorTest.php rename tests/Unit/Domain/Infrastructure/DataSource/Auth/{Validators => Validator}/AuthTokenValidatorTest.php (51%) delete mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php rename tests/Unit/Domain/Infrastructure/DataSource/User/{Validators => Validator}/UserValidatorTest.php (52%) diff --git a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidator.php similarity index 76% rename from src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php rename to src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidator.php index a987221..275775b 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidator.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidator.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Infrastructure\Enums\Validators\AuthLoginValidatorEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\AuthLoginValidatorEnum; final class AuthLoginValidator extends AbstractValidator { @@ -27,13 +27,13 @@ final class AuthLoginValidator extends AbstractValidator * Validate a AuthInput and return an array of errors. * Empty array means valid. * - * @param AuthInput $input + * @param CommandInterface $command * * @return Result */ - public function validate(mixed $input): Result + public function validate(CommandInterface $command): Result { - $errors = $this->runValidations($input); + $errors = $this->runValidations($command); if (true !== empty($errors)) { return Result::error( [HttpCodesEnum::AppIncorrectCredentials->error()] diff --git a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidator.php similarity index 78% rename from src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php rename to src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidator.php index 6b2ed17..a8508f7 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidator.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidator.php @@ -11,16 +11,18 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; +use Phalcon\Api\Domain\Application\Auth\Command\AuthLogoutPostCommand; +use Phalcon\Api\Domain\Application\Auth\Command\AuthRefreshPostCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Infrastructure\Enums\Validators\AuthTokenValidatorEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\AuthTokenValidatorEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Filter\Validation\ValidationInterface; @@ -40,19 +42,23 @@ public function __construct( * Validate a AuthInput and return an array of errors. * Empty array means valid. * - * @param AuthInput $input + * @param CommandInterface $command * * @return Result */ - public function validate(mixed $input): Result + public function validate(CommandInterface $command): Result { - $errors = $this->runValidations($input); + $errors = $this->runValidations($command); if (true !== empty($errors)) { return Result::error([HttpCodesEnum::AppTokenNotPresent->error()]); } + /** + * @todo add the docblock for $command here + */ + /** @var AuthLogoutPostCommand|AuthRefreshPostCommand $command */ /** @var string $token */ - $token = $input->token; + $token = $command->token; $tokenObject = $this->tokenManager->getObject($token); if (null === $tokenObject) { return Result::error([HttpCodesEnum::AppTokenNotValid->error()]); diff --git a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTrait.php similarity index 68% rename from src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php rename to src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTrait.php index 69fd2b9..c2fc0b9 100644 --- a/src/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTrait.php +++ b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTrait.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; trait UserValidatorTrait @@ -22,13 +22,13 @@ trait UserValidatorTrait * Validate a AuthInput and return an array of errors. * Empty array means valid. * - * @param AuthInput $input + * @param CommandInterface $command * * @return Result */ - public function validate(mixed $input): Result + public function validate(CommandInterface $command): Result { - $errors = $this->runValidations($input); + $errors = $this->runValidations($command); if (true !== empty($errors)) { return Result::error($errors); } @@ -37,9 +37,9 @@ public function validate(mixed $input): Result } /** - * @param AuthInput $input + * @param CommandInterface $command * * @return array */ - abstract protected function runValidations(mixed $input): array; + abstract protected function runValidations(CommandInterface $command): array; } diff --git a/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php index 2048065..bc15ea5 100644 --- a/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php +++ b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php @@ -13,8 +13,8 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; -use Phalcon\Api\Domain\Infrastructure\Enums\Validators\ValidatorEnumInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\ValidatorEnumInterface; use Phalcon\Filter\Validation\ValidationInterface; use Phalcon\Filter\Validation\ValidatorInterface as PhalconValidator; @@ -23,18 +23,18 @@ abstract class AbstractValidator implements ValidatorInterface protected string $fields = ''; public function __construct( - private ValidationInterface $validation + private readonly ValidationInterface $validation ) { } /** - * @param AuthInput $input + * @param CommandInterface $command * * @return list> */ - protected function runValidations(mixed $input): array + protected function runValidations(CommandInterface $command): array { - $enum = $this->fields; + $enum = $this->fields; /** @var ValidatorEnumInterface[] $elements */ $elements = $enum::cases(); @@ -43,12 +43,16 @@ protected function runValidations(mixed $input): array $validators = $element->validators(); foreach ($validators as $validator) { /** @var PhalconValidator $validatorObject */ - $validatorObject = new $validator(); + $validatorObject = new $validator( + [ + 'allowEmpty' => $element->allowEmpty(), + ] + ); $this->validation->add($element->name, $validatorObject); } } - $this->validation->validate($input); + $this->validation->validate($command); $messages = $this->validation->getMessages(); $results = []; diff --git a/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php b/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php index e79bacd..3177cd9 100644 --- a/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php +++ b/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php @@ -13,6 +13,8 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + /** * Validator contract. Accepts a DTO or input and returns a Result. */ @@ -21,9 +23,9 @@ interface ValidatorInterface /** * Validate a DTO or input structure. * - * @param mixed $input DTO or array + * @param CommandInterface $command DTO or array * * @return Result */ - public function validate(mixed $input): Result; + public function validate(CommandInterface $command): Result; } diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorTest.php new file mode 100644 index 0000000..cfb082e --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorTest.php @@ -0,0 +1,64 @@ + + * + * 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\Infrastructure\DataSource\Auth\Validator; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactory; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthLoginValidator; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class AuthLoginValidatorTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var AuthLoginValidator $validator */ + $validator = $this->container->get(AuthLoginValidator::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $input = []; + $authCommand = $factory->authenticate($input); + + $result = $validator->validate($authCommand); + $actual = $result->getErrors(); + + $expected = [ + HttpCodesEnum::AppIncorrectCredentials->error(), + ]; + + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var AuthLoginValidator $validator */ + $validator = $this->container->get(AuthLoginValidator::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + $faker = FakerFactory::create(); + + $input = [ + 'email' => $faker->safeEmail(), + 'password' => $faker->password(), + ]; + $authCommand = $factory->authenticate($input); + + $result = $validator->validate($authCommand); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorTest.php similarity index 51% rename from tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorTest.php index 333d74d..adbc0d0 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthTokenValidatorTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorTest.php @@ -11,42 +11,36 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\Validators; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\Auth\Validator; use Exception; -use Faker\Factory; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthTokenValidator; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactory; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthTokenValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCacheInterface; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Validation\ValidationInterface; +use Phalcon\Filter\Validation; final class AuthTokenValidatorTest extends AbstractUnitTestCase { public function testFailureTokenNotPresent(): void { - /** @var AuthSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::AUTH_SANITIZER); - /** @var TokenManager $tokenManager */ - $tokenManager = $this->container->get(Container::JWT_TOKEN_MANAGER); - /** @var UserRepository $repository */ - $repository = $this->container->get(Container::USER_REPOSITORY); - /** @var ValidationInterface $validation */ - $validation = $this->container->get(Container::VALIDATION); + /** @var AuthTokenValidator $validator */ + $validator = $this->container->get(AuthTokenValidator::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); - $input = []; - $userInput = AuthInput::new($sanitizer, $input); + $input = []; + $authCommand = $factory->logout($input); - $validator = new AuthTokenValidator($tokenManager, $repository, $validation); - $result = $validator->validate($userInput); - $actual = $result->getErrors(); + $result = $validator->validate($authCommand); + $actual = $result->getErrors(); $expected = [ HttpCodesEnum::AppTokenNotPresent->error(), @@ -58,22 +52,24 @@ public function testFailureTokenNotPresent(): void public function testFailureTokenNotValid(): void { /** @var EnvManager $env */ - $env = $this->container->get(Container::ENV); + $env = $this->container->get(EnvManager::class); /** @var TokenCacheInterface $tokenCache */ - $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + $tokenCache = $this->container->get(TokenCache::class); /** @var AuthSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::AUTH_SANITIZER); + $sanitizer = $this->container->get(AuthSanitizer::class); /** @var UserRepository $repository */ - $repository = $this->container->get(Container::USER_REPOSITORY); - /** @var ValidationInterface $validation */ - $validation = $this->container->get(Container::VALIDATION); + $repository = $this->container->get(UserRepository::class); + /** @var Validation $validator */ + $validation = $this->container->get(Validation::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); $mockJWTToken = $this ->getMockBuilder(JWTToken::class) ->disableOriginalConstructor() ->onlyMethods( [ - 'getObject' + 'getObject', ] ) ->getMock() @@ -83,19 +79,19 @@ public function testFailureTokenNotValid(): void ->willThrowException(new Exception('error')) ; - $userData = $this->getNewUserData(); + $userData = $this->getNewUserData(); $userData['usr_id'] = rand(1, 100); - $token = $this->getUserToken($userData); + $token = $this->getUserToken($userData); $tokenManager = new TokenManager($tokenCache, $env, $mockJWTToken); - $input = [ + $input = [ 'token' => $token, ]; - $userInput = AuthInput::new($sanitizer, $input); + $authCommand = $factory->logout($input); $validator = new AuthTokenValidator($tokenManager, $repository, $validation); - $result = $validator->validate($userInput); + $result = $validator->validate($authCommand); $actual = $result->getErrors(); $expected = [ diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php deleted file mode 100644 index db79869..0000000 --- a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validators/AuthLoginValidatorTest.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * 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\Infrastructure\DataSource\Auth\Validators; - -use Faker\Factory as FakerFactory; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthLoginValidator; -use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Validation\ValidationInterface; - -final class AuthLoginValidatorTest extends AbstractUnitTestCase -{ - public function testError(): void - { - /** @var AuthSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::AUTH_SANITIZER); - /** @var ValidationInterface $validation */ - $validation = $this->container->get(Container::VALIDATION); - - $input = []; - $userInput = AuthInput::new($sanitizer, $input); - - $validator = new AuthLoginValidator($validation); - $result = $validator->validate($userInput); - $actual = $result->getErrors(); - - $expected = [ - HttpCodesEnum::AppIncorrectCredentials->error(), - ]; - - $this->assertSame($expected, $actual); - } - - public function testSuccess(): void - { - /** @var AuthSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::AUTH_SANITIZER); - /** @var ValidationInterface $validation */ - $validation = $this->container->get(Container::VALIDATION); - $faker = FakerFactory::create(); - - $input = [ - 'email' => $faker->safeEmail(), - 'password' => $faker->password(), - ]; - $userInput = AuthInput::new($sanitizer, $input); - - $validator = new AuthLoginValidator($validation); - $result = $validator->validate($userInput); - $actual = $result->getErrors(); - - $expected = []; - $this->assertSame($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTest.php similarity index 52% rename from tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php rename to tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTest.php index 155fdd0..c81cd62 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Validators/UserValidatorTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTest.php @@ -11,31 +11,27 @@ declare(strict_types=1); -namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Validators; +namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\DataSource\User\Validator; use Faker\Factory as FakerFactory; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserValidator; use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Validation\ValidationInterface; final class UserValidatorTest extends AbstractUnitTestCase { public function testError(): void { - /** @var UserSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::USER_SANITIZER); - /** @var ValidationInterface $validation */ - $validation = $this->container->get(Container::VALIDATION); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + /** @var UserValidator $validator */ + $validator = $this->container->get(UserValidator::class); $input = []; - $userInput = UserInput::new($sanitizer, $input); + $userInput = $factory->insert($input); - $validator = new UserValidator($validation); - $result = $validator->validate($userInput); - $actual = $result->getErrors(); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); $expected = [ ['Field email is required'], @@ -51,10 +47,10 @@ public function testError(): void public function testSuccess(): void { - /** @var UserSanitizer $sanitizer */ - $sanitizer = $this->container->get(Container::USER_SANITIZER); - /** @var ValidationInterface $validation */ - $validation = $this->container->get(Container::VALIDATION); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + /** @var UserValidator $validator */ + $validator = $this->container->get(UserValidator::class); $faker = FakerFactory::create(); $input = [ @@ -65,11 +61,10 @@ public function testSuccess(): void 'tokenId' => $faker->uuid(), ]; - $userInput = UserInput::new($sanitizer, $input); + $userInput = $factory->insert($input); - $validator = new UserValidator($validation); - $result = $validator->validate($userInput); - $actual = $result->getErrors(); + $result = $validator->validate($userInput); + $actual = $result->getErrors(); $expected = []; $this->assertSame($expected, $actual); From bc0e7f799909c10c3177779fe34e90b0af8eae23 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:10:57 -0600 Subject: [PATCH 229/301] [#.x] - added auth command objects --- .../Auth/Command/AuthLoginPostCommand.php | 29 +++++++++++ .../Auth/Command/AuthLogoutPostCommand.php | 27 ++++++++++ .../Auth/Command/AuthRefreshPostCommand.php | 27 ++++++++++ .../Auth/Command/AuthLoginPostCommandTest.php | 49 +++++++++++++++++++ .../Command/AuthLogoutPostCommandTest.php | 42 ++++++++++++++++ .../Command/AuthRefreshPostCommandTest.php | 42 ++++++++++++++++ 6 files changed, 216 insertions(+) create mode 100644 src/Domain/Application/Auth/Command/AuthLoginPostCommand.php create mode 100644 src/Domain/Application/Auth/Command/AuthLogoutPostCommand.php create mode 100644 src/Domain/Application/Auth/Command/AuthRefreshPostCommand.php create mode 100644 tests/Unit/Domain/Application/Auth/Command/AuthLoginPostCommandTest.php create mode 100644 tests/Unit/Domain/Application/Auth/Command/AuthLogoutPostCommandTest.php create mode 100644 tests/Unit/Domain/Application/Auth/Command/AuthRefreshPostCommandTest.php diff --git a/src/Domain/Application/Auth/Command/AuthLoginPostCommand.php b/src/Domain/Application/Auth/Command/AuthLoginPostCommand.php new file mode 100644 index 0000000..ee02ab3 --- /dev/null +++ b/src/Domain/Application/Auth/Command/AuthLoginPostCommand.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\Domain\Application\Auth\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class AuthLoginPostCommand implements CommandInterface +{ + /** + * @param string|null $email + * @param string|null $password + */ + public function __construct( + public ?string $email, + public ?string $password, + ) { + } +} diff --git a/src/Domain/Application/Auth/Command/AuthLogoutPostCommand.php b/src/Domain/Application/Auth/Command/AuthLogoutPostCommand.php new file mode 100644 index 0000000..8eb47e2 --- /dev/null +++ b/src/Domain/Application/Auth/Command/AuthLogoutPostCommand.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\Application\Auth\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class AuthLogoutPostCommand implements CommandInterface +{ + /** + * @param string|null $token + */ + public function __construct( + public ?string $token + ) { + } +} diff --git a/src/Domain/Application/Auth/Command/AuthRefreshPostCommand.php b/src/Domain/Application/Auth/Command/AuthRefreshPostCommand.php new file mode 100644 index 0000000..2f16444 --- /dev/null +++ b/src/Domain/Application/Auth/Command/AuthRefreshPostCommand.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\Application\Auth\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class AuthRefreshPostCommand implements CommandInterface +{ + /** + * @param string|null $token + */ + public function __construct( + public ?string $token + ) { + } +} diff --git a/tests/Unit/Domain/Application/Auth/Command/AuthLoginPostCommandTest.php b/tests/Unit/Domain/Application/Auth/Command/AuthLoginPostCommandTest.php new file mode 100644 index 0000000..61bdc08 --- /dev/null +++ b/tests/Unit/Domain/Application/Auth/Command/AuthLoginPostCommandTest.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\Tests\Unit\Domain\Application\Auth\Command; + +use Phalcon\Api\Domain\Application\Auth\Command\AuthLoginPostCommand; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class AuthLoginPostCommandTest extends AbstractUnitTestCase +{ + public function testConstruct(): void + { + $email = 'user@example.com'; + $password = 's3cr3t'; + + $command = new AuthLoginPostCommand($email, $password); + + $expected = $email; + $actual = $command->email; + $this->assertSame($expected, $actual); + + $expected = $password; + $actual = $command->password; + $this->assertSame($expected, $actual); + } + + public function testConstructEmpty(): void + { + $command = new AuthLoginPostCommand(null, null); + + $expected = null; + $actual = $command->email; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $command->password; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Auth/Command/AuthLogoutPostCommandTest.php b/tests/Unit/Domain/Application/Auth/Command/AuthLogoutPostCommandTest.php new file mode 100644 index 0000000..c7c82fa --- /dev/null +++ b/tests/Unit/Domain/Application/Auth/Command/AuthLogoutPostCommandTest.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\Application\Auth\Command; + +use Phalcon\Api\Domain\Application\Auth\Command\AuthLogoutPostCommand; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function uniqid; + +final class AuthLogoutPostCommandTest extends AbstractUnitTestCase +{ + public function testConstruct(): void + { + $token = uniqid('token-'); + + $command = new AuthLogoutPostCommand($token); + + $expected = $token; + $actual = $command->token; + $this->assertSame($expected, $actual); + } + + public function testConstructEmpty(): void + { + $command = new AuthLogoutPostCommand(null); + + $expected = null; + $actual = $command->token; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Auth/Command/AuthRefreshPostCommandTest.php b/tests/Unit/Domain/Application/Auth/Command/AuthRefreshPostCommandTest.php new file mode 100644 index 0000000..9123345 --- /dev/null +++ b/tests/Unit/Domain/Application/Auth/Command/AuthRefreshPostCommandTest.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\Application\Auth\Command; + +use Phalcon\Api\Domain\Application\Auth\Command\AuthRefreshPostCommand; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function uniqid; + +final class AuthRefreshPostCommandTest extends AbstractUnitTestCase +{ + public function testConstruct(): void + { + $token = uniqid('token-'); + + $command = new AuthRefreshPostCommand($token); + + $expected = $token; + $actual = $command->token; + $this->assertSame($expected, $actual); + } + + public function testConstructEmpty(): void + { + $command = new AuthRefreshPostCommand(null); + + $expected = null; + $actual = $command->token; + $this->assertSame($expected, $actual); + } +} From 497e0b29afa96b6991f5cd0b8d9aa12c1e729270 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:11:51 -0600 Subject: [PATCH 230/301] [#.x] - added user command objects and factory --- .../User/Command/UserCommandFactory.php | 137 +++++++++ .../Command/UserCommandFactoryInterface.php | 50 ++++ .../User/Command/UserDeleteCommand.php | 27 ++ .../User/Command/UserGetCommand.php | 27 ++ .../User/Command/UserPostCommand.php | 59 ++++ .../User/Command/UserPutCommand.php | 59 ++++ .../User/Command/UserCommandFactoryTest.php | 267 ++++++++++++++++++ 7 files changed, 626 insertions(+) create mode 100644 src/Domain/Application/User/Command/UserCommandFactory.php create mode 100644 src/Domain/Application/User/Command/UserCommandFactoryInterface.php create mode 100644 src/Domain/Application/User/Command/UserDeleteCommand.php create mode 100644 src/Domain/Application/User/Command/UserGetCommand.php create mode 100644 src/Domain/Application/User/Command/UserPostCommand.php create mode 100644 src/Domain/Application/User/Command/UserPutCommand.php create mode 100644 tests/Unit/Domain/Application/User/Command/UserCommandFactoryTest.php diff --git a/src/Domain/Application/User/Command/UserCommandFactory.php b/src/Domain/Application/User/Command/UserCommandFactory.php new file mode 100644 index 0000000..1dae88e --- /dev/null +++ b/src/Domain/Application/User/Command/UserCommandFactory.php @@ -0,0 +1,137 @@ + + * + * 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\Application\User\Command; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\UserInput; + +/** + * @phpstan-import-type TUserInput from InputTypes + */ +final readonly class UserCommandFactory implements UserCommandFactoryInterface +{ + public function __construct( + private SanitizerInterface $sanitizer + ) { + } + + /** + * @param TUserInput $input + * + * @return UserDeleteCommand + */ + public function delete(array $input): UserDeleteCommand + { + /** + * Sanitize the input + */ + $dto = UserInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new UserDeleteCommand($dto->id); + } + + /** + * @param TUserInput $input + * + * @return UserGetCommand + */ + public function get(array $input): UserGetCommand + { + /** + * Sanitize the input + */ + $dto = UserInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new UserGetCommand($dto->id); + } + + /** + * @param TUserInput $input + * + * @return UserPostCommand + */ + public function insert(array $input): UserPostCommand + { + /** + * Sanitize the input + */ + $dto = UserInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new UserPostCommand( + null, + $dto->status, + $dto->email, + $dto->password, + $dto->namePrefix, + $dto->nameFirst, + $dto->nameMiddle, + $dto->nameLast, + $dto->nameSuffix, + $dto->issuer, + $dto->tokenPassword, + $dto->tokenId, + $dto->preferences, + $dto->createdDate, + $dto->createdUserId, + $dto->updatedDate, + $dto->updatedUserId, + ); + } + + /** + * @param TUserInput $input + * + * @return UserPutCommand + */ + public function update(array $input): UserPutCommand + { + /** + * Sanitize the input + */ + $dto = UserInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new UserPutCommand( + $dto->id, + $dto->status, + $dto->email, + $dto->password, + $dto->namePrefix, + $dto->nameFirst, + $dto->nameMiddle, + $dto->nameLast, + $dto->nameSuffix, + $dto->issuer, + $dto->tokenPassword, + $dto->tokenId, + $dto->preferences, + $dto->createdDate, + $dto->createdUserId, + $dto->updatedDate, + $dto->updatedUserId, + ); + } +} diff --git a/src/Domain/Application/User/Command/UserCommandFactoryInterface.php b/src/Domain/Application/User/Command/UserCommandFactoryInterface.php new file mode 100644 index 0000000..b8645f4 --- /dev/null +++ b/src/Domain/Application/User/Command/UserCommandFactoryInterface.php @@ -0,0 +1,50 @@ + + * + * 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\Application\User\Command; + +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TUserInput from InputTypes + */ +interface UserCommandFactoryInterface +{ + /** + * @param TUserInput $input + * + * @return UserDeleteCommand + */ + public function delete(array $input): UserDeleteCommand; + + /** + * @param TUserInput $input + * + * @return UserGetCommand + */ + public function get(array $input): UserGetCommand; + + /** + * @param TUserInput $input + * + * @return UserPostCommand + */ + public function insert(array $input): UserPostCommand; + + /** + * @param TUserInput $input + * + * @return UserPutCommand + */ + public function update(array $input): UserPutCommand; +} diff --git a/src/Domain/Application/User/Command/UserDeleteCommand.php b/src/Domain/Application/User/Command/UserDeleteCommand.php new file mode 100644 index 0000000..86db29c --- /dev/null +++ b/src/Domain/Application/User/Command/UserDeleteCommand.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\Application\User\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class UserDeleteCommand implements CommandInterface +{ + /** + * @param int $id + */ + public function __construct( + public int $id, + ) { + } +} diff --git a/src/Domain/Application/User/Command/UserGetCommand.php b/src/Domain/Application/User/Command/UserGetCommand.php new file mode 100644 index 0000000..33510b9 --- /dev/null +++ b/src/Domain/Application/User/Command/UserGetCommand.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\Application\User\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class UserGetCommand implements CommandInterface +{ + /** + * @param int $id + */ + public function __construct( + public int $id, + ) { + } +} diff --git a/src/Domain/Application/User/Command/UserPostCommand.php b/src/Domain/Application/User/Command/UserPostCommand.php new file mode 100644 index 0000000..c0ac15e --- /dev/null +++ b/src/Domain/Application/User/Command/UserPostCommand.php @@ -0,0 +1,59 @@ + + * + * 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\Application\User\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class UserPostCommand implements CommandInterface +{ + /** + * @param int|null $id + * @param int $status + * @param string|null $email + * @param string|null $password + * @param string|null $namePrefix + * @param string|null $nameFirst + * @param string|null $nameMiddle + * @param string|null $nameLast + * @param string|null $nameSuffix + * @param string|null $issuer + * @param string|null $tokenPassword + * @param string|null $tokenId + * @param string|null $preferences + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + */ + public function __construct( + public ?int $id, + public int $status, + public ?string $email, + public ?string $password, + public ?string $namePrefix, + public ?string $nameFirst, + public ?string $nameMiddle, + public ?string $nameLast, + public ?string $nameSuffix, + public ?string $issuer, + public ?string $tokenPassword, + public ?string $tokenId, + public ?string $preferences, + public ?string $createdDate, + public ?int $createdUserId, + public ?string $updatedDate, + public ?int $updatedUserId, + ) { + } +} diff --git a/src/Domain/Application/User/Command/UserPutCommand.php b/src/Domain/Application/User/Command/UserPutCommand.php new file mode 100644 index 0000000..11bc512 --- /dev/null +++ b/src/Domain/Application/User/Command/UserPutCommand.php @@ -0,0 +1,59 @@ + + * + * 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\Application\User\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class UserPutCommand implements CommandInterface +{ + /** + * @param int $id + * @param int $status + * @param string|null $email + * @param string|null $password + * @param string|null $namePrefix + * @param string|null $nameFirst + * @param string|null $nameMiddle + * @param string|null $nameLast + * @param string|null $nameSuffix + * @param string|null $issuer + * @param string|null $tokenPassword + * @param string|null $tokenId + * @param string|null $preferences + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + */ + public function __construct( + public int $id, + public int $status, + public ?string $email, + public ?string $password, + public ?string $namePrefix, + public ?string $nameFirst, + public ?string $nameMiddle, + public ?string $nameLast, + public ?string $nameSuffix, + public ?string $issuer, + public ?string $tokenPassword, + public ?string $tokenId, + public ?string $preferences, + public ?string $createdDate, + public ?int $createdUserId, + public ?string $updatedDate, + public ?int $updatedUserId, + ) { + } +} diff --git a/tests/Unit/Domain/Application/User/Command/UserCommandFactoryTest.php b/tests/Unit/Domain/Application/User/Command/UserCommandFactoryTest.php new file mode 100644 index 0000000..310519b --- /dev/null +++ b/tests/Unit/Domain/Application/User/Command/UserCommandFactoryTest.php @@ -0,0 +1,267 @@ + + * + * 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\Application\User\Command; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Application\User\Command\UserDeleteCommand; +use Phalcon\Api\Domain\Application\User\Command\UserGetCommand; +use Phalcon\Api\Domain\Application\User\Command\UserPostCommand; +use Phalcon\Api\Domain\Application\User\Command\UserPutCommand; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +use function json_encode; + +final class UserCommandFactoryTest extends AbstractUnitTestCase +{ + public function testDelete(): void + { + $sanitizer = $this->container->get(UserSanitizer::class); + $factory = new UserCommandFactory($sanitizer); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => " Foo.Bar+tag@Example.COM ", + 'password' => $faker->password(), + 'namePrefix' => $faker->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => $faker->suffix(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => json_encode(['k' => $faker->word()]), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + ]; + + /** @var UserDeleteCommand $command */ + $command = $factory->delete($input); + + $this->assertInstanceOf(UserDeleteCommand::class, $command); + + $expected = $input['id']; + $actual = $command->id; + $this->assertSame($expected, $actual); + } + + public function testGet(): void + { + $sanitizer = $this->container->get(UserSanitizer::class); + $factory = new UserCommandFactory($sanitizer); + $faker = FakerFactory::create(); + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => " Foo.Bar+tag@Example.COM ", + 'password' => $faker->password(), + 'namePrefix' => $faker->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => $faker->suffix(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => json_encode(['k' => $faker->word()]), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + ]; + + $command = $factory->get($input); + + $this->assertInstanceOf(UserGetCommand::class, $command); + $expected = $input['id']; + $actual = $command->id; + $this->assertSame($expected, $actual); + } + + public function testInsert(): void + { + $sanitizer = $this->container->get(UserSanitizer::class); + /** @var Filter $filter */ + $filter = $this->container->get(Container::FILTER); + $factory = new UserCommandFactory($sanitizer); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => " Foo.Bar+tag@Example.COM ", + 'password' => $faker->password(), + 'namePrefix' => $faker->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => $faker->suffix(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => json_encode(['k' => $faker->word()]), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + ]; + + /** @var UserPostCommand $command */ + $command = $factory->insert($input); + + $this->assertInstanceOf(UserPostCommand::class, $command); + + $expected = $input['status']; + $actual = $command->status; + $this->assertSame($expected, $actual); + $expected = $filter->email($input['email']); + $actual = $command->email; + $this->assertSame($expected, $actual); + $expected = $input['password']; + $actual = $command->password; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['namePrefix']); + $actual = $command->namePrefix; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['nameFirst']); + $actual = $command->nameFirst; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['nameMiddle']); + $actual = $command->nameMiddle; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['nameLast']); + $actual = $command->nameLast; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['nameSuffix']); + $actual = $command->nameSuffix; + $this->assertSame($expected, $actual); + $expected = $input['issuer']; + $actual = $command->issuer; + $this->assertSame($expected, $actual); + $expected = $input['tokenPassword']; + $actual = $command->tokenPassword; + $this->assertSame($expected, $actual); + $expected = $input['tokenId']; + $actual = $command->tokenId; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['preferences']); + $actual = $command->preferences; + $this->assertSame($expected, $actual); + $expected = $input['createdDate']; + $actual = $command->createdDate; + $this->assertSame($expected, $actual); + $expected = $filter->absint($input['createdUserId']); + $actual = $command->createdUserId; + $this->assertSame($expected, $actual); + $expected = $input['updatedDate']; + $actual = $command->updatedDate; + $this->assertSame($expected, $actual); + $expected = $filter->absint($input['updatedUserId']); + $actual = $command->updatedUserId; + $this->assertSame($expected, $actual); + } + + public function testUpdate(): void + { + $sanitizer = $this->container->get(UserSanitizer::class); + /** @var Filter $filter */ + $filter = $this->container->get(Container::FILTER); + $factory = new UserCommandFactory($sanitizer); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'status' => $faker->numberBetween(0, 9), + 'email' => " Foo.Bar+tag@Example.COM ", + 'password' => $faker->password(), + 'namePrefix' => $faker->title(), + 'nameFirst' => $faker->firstName(), + 'nameMiddle' => $faker->word(), + 'nameLast' => $faker->lastName(), + 'nameSuffix' => $faker->suffix(), + 'issuer' => $faker->company(), + 'tokenPassword' => $faker->password(), + 'tokenId' => $faker->uuid(), + 'preferences' => json_encode(['k' => $faker->word()]), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), // string to ensure ABSINT cast + ]; + + /** @var UserPutCommand $command */ + $command = $factory->update($input); + + $this->assertInstanceOf(UserPutCommand::class, $command); + $expected = $input['id']; + $actual = $command->id; + $this->assertSame($expected, $actual); + $expected = $input['status']; + $actual = $command->status; + $this->assertSame($expected, $actual); + $expected = $filter->email($input['email']); + $actual = $command->email; + $this->assertSame($expected, $actual); + $expected = $input['password']; + $actual = $command->password; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['namePrefix']); + $actual = $command->namePrefix; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['nameFirst']); + $actual = $command->nameFirst; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['nameMiddle']); + $actual = $command->nameMiddle; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['nameLast']); + $actual = $command->nameLast; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['nameSuffix']); + $actual = $command->nameSuffix; + $this->assertSame($expected, $actual); + $expected = $input['issuer']; + $actual = $command->issuer; + $this->assertSame($expected, $actual); + $expected = $input['tokenPassword']; + $actual = $command->tokenPassword; + $this->assertSame($expected, $actual); + $expected = $input['tokenId']; + $actual = $command->tokenId; + $this->assertSame($expected, $actual); + $expected = $filter->string($input['preferences']); + $actual = $command->preferences; + $this->assertSame($expected, $actual); + $expected = $input['createdDate']; + $actual = $command->createdDate; + $this->assertSame($expected, $actual); + $expected = $filter->absint($input['createdUserId']); + $actual = $command->createdUserId; + $this->assertSame($expected, $actual); + $expected = $input['updatedDate']; + $actual = $command->updatedDate; + $this->assertSame($expected, $actual); + $expected = $filter->absint($input['updatedUserId']); + $actual = $command->updatedUserId; + $this->assertSame($expected, $actual); + } +} From fc10c444b91d5ab331d03b57d6d5b7212c8d5a4d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:12:55 -0600 Subject: [PATCH 231/301] [#.x] - refactored services --- .../Auth/Service}/AbstractAuthService.php | 6 ++---- .../Auth/Service/AuthLoginPostService.php} | 6 +++--- .../Auth/Service/AuthLogoutPostService.php} | 6 +++--- .../Auth/Service/AuthRefreshPostService.php} | 6 +++--- 4 files changed, 11 insertions(+), 13 deletions(-) rename src/Domain/{Services/Auth => Application/Auth/Service}/AbstractAuthService.php (64%) rename src/Domain/{Services/Auth/LoginPostService.php => Application/Auth/Service/AuthLoginPostService.php} (76%) rename src/Domain/{Services/Auth/LogoutPostService.php => Application/Auth/Service/AuthLogoutPostService.php} (76%) rename src/Domain/{Services/Auth/RefreshPostService.php => Application/Auth/Service/AuthRefreshPostService.php} (76%) diff --git a/src/Domain/Services/Auth/AbstractAuthService.php b/src/Domain/Application/Auth/Service/AbstractAuthService.php similarity index 64% rename from src/Domain/Services/Auth/AbstractAuthService.php rename to src/Domain/Application/Auth/Service/AbstractAuthService.php index 8e4dd98..5ec195f 100644 --- a/src/Domain/Services/Auth/AbstractAuthService.php +++ b/src/Domain/Application/Auth/Service/AbstractAuthService.php @@ -11,17 +11,15 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Auth; +namespace Phalcon\Api\Domain\Application\Auth\Service; use Phalcon\Api\Domain\ADR\DomainInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Facades\AuthFacade; -use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Application\Auth\Facade\AuthFacade; abstract class AbstractAuthService implements DomainInterface { public function __construct( protected readonly AuthFacade $facade, - protected readonly ValidatorInterface $validator ) { } } diff --git a/src/Domain/Services/Auth/LoginPostService.php b/src/Domain/Application/Auth/Service/AuthLoginPostService.php similarity index 76% rename from src/Domain/Services/Auth/LoginPostService.php rename to src/Domain/Application/Auth/Service/AuthLoginPostService.php index 34b29ec..5cc8368 100644 --- a/src/Domain/Services/Auth/LoginPostService.php +++ b/src/Domain/Application/Auth/Service/AuthLoginPostService.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Auth; +namespace Phalcon\Api\Domain\Application\Auth\Service; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; @@ -19,7 +19,7 @@ /** * @phpstan-import-type TAuthLoginInput from InputTypes */ -final class LoginPostService extends AbstractAuthService +final class AuthLoginPostService extends AbstractAuthService { /** * @param TAuthLoginInput $input @@ -28,6 +28,6 @@ final class LoginPostService extends AbstractAuthService */ public function __invoke(array $input): Payload { - return $this->facade->authenticate($input, $this->validator); + return $this->facade->authenticate($input); } } diff --git a/src/Domain/Services/Auth/LogoutPostService.php b/src/Domain/Application/Auth/Service/AuthLogoutPostService.php similarity index 76% rename from src/Domain/Services/Auth/LogoutPostService.php rename to src/Domain/Application/Auth/Service/AuthLogoutPostService.php index 2ac289b..7f3594c 100644 --- a/src/Domain/Services/Auth/LogoutPostService.php +++ b/src/Domain/Application/Auth/Service/AuthLogoutPostService.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Auth; +namespace Phalcon\Api\Domain\Application\Auth\Service; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; @@ -19,7 +19,7 @@ /** * @phpstan-import-type TAuthLogoutInput from InputTypes */ -final class LogoutPostService extends AbstractAuthService +final class AuthLogoutPostService extends AbstractAuthService { /** * @param TAuthLogoutInput $input @@ -28,6 +28,6 @@ final class LogoutPostService extends AbstractAuthService */ public function __invoke(array $input): Payload { - return $this->facade->logout($input, $this->validator); + return $this->facade->logout($input); } } diff --git a/src/Domain/Services/Auth/RefreshPostService.php b/src/Domain/Application/Auth/Service/AuthRefreshPostService.php similarity index 76% rename from src/Domain/Services/Auth/RefreshPostService.php rename to src/Domain/Application/Auth/Service/AuthRefreshPostService.php index 7d50910..3f119e0 100644 --- a/src/Domain/Services/Auth/RefreshPostService.php +++ b/src/Domain/Application/Auth/Service/AuthRefreshPostService.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Services\Auth; +namespace Phalcon\Api\Domain\Application\Auth\Service; use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\ADR\Payload; @@ -19,7 +19,7 @@ /** * @phpstan-import-type TAuthRefreshInput from InputTypes */ -final class RefreshPostService extends AbstractAuthService +final class AuthRefreshPostService extends AbstractAuthService { /** * @param TAuthRefreshInput $input @@ -28,6 +28,6 @@ final class RefreshPostService extends AbstractAuthService */ public function __invoke(array $input): Payload { - return $this->facade->refresh($input, $this->validator); + return $this->facade->refresh($input); } } From deda103294eec409353e985bdac36955bd09caf3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:13:09 -0600 Subject: [PATCH 232/301] [#.x] - added auth command factory --- .../Auth/Command/AuthCommandFactory.php | 88 +++++++++++++++++++ .../Command/AuthCommandFactoryInterface.php | 45 ++++++++++ .../Auth/Command/AuthCommandFactoryTest.php | 87 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/Domain/Application/Auth/Command/AuthCommandFactory.php create mode 100644 src/Domain/Application/Auth/Command/AuthCommandFactoryInterface.php create mode 100644 tests/Unit/Domain/Application/Auth/Command/AuthCommandFactoryTest.php diff --git a/src/Domain/Application/Auth/Command/AuthCommandFactory.php b/src/Domain/Application/Auth/Command/AuthCommandFactory.php new file mode 100644 index 0000000..d7b47f1 --- /dev/null +++ b/src/Domain/Application/Auth/Command/AuthCommandFactory.php @@ -0,0 +1,88 @@ + + * + * 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\Application\Auth\Command; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerInterface; + +/** + * @phpstan-import-type TAuthLoginInput from InputTypes + * @phpstan-import-type TAuthLogoutInput from InputTypes + * @phpstan-import-type TAuthRefreshInput from InputTypes + */ +final readonly class AuthCommandFactory implements AuthCommandFactoryInterface +{ + /** + * @param SanitizerInterface $sanitizer + */ + public function __construct( + private SanitizerInterface $sanitizer, + ) { + } + + /** + * @param TAuthLoginInput $input + * + * @return AuthLoginPostCommand + */ + public function authenticate(array $input): AuthLoginPostCommand + { + /** + * Sanitize the input + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Return the command back + */ + return new AuthLoginPostCommand($dto->email, $dto->password); + } + + /** + * @param TAuthLogoutInput $input + * + * @return AuthLogoutPostCommand + */ + public function logout(array $input): AuthLogoutPostCommand + { + /** + * Sanitize the input + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Return the command back + */ + return new AuthLogoutPostCommand($dto->token); + } + + /** + * @param TAuthRefreshInput $input + * + * @return AuthRefreshPostCommand + */ + public function refresh(array $input): AuthRefreshPostCommand + { + /** + * Sanitize the input + */ + $dto = AuthInput::new($this->sanitizer, $input); + + /** + * Return the command back + */ + return new AuthRefreshPostCommand($dto->token); + } +} diff --git a/src/Domain/Application/Auth/Command/AuthCommandFactoryInterface.php b/src/Domain/Application/Auth/Command/AuthCommandFactoryInterface.php new file mode 100644 index 0000000..a8dbad8 --- /dev/null +++ b/src/Domain/Application/Auth/Command/AuthCommandFactoryInterface.php @@ -0,0 +1,45 @@ + + * + * 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\Application\Auth\Command; + +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TAuthLoginInput from InputTypes + * @phpstan-import-type TAuthLogoutInput from InputTypes + * @phpstan-import-type TAuthRefreshInput from InputTypes + */ +interface AuthCommandFactoryInterface +{ + /** + * @param TAuthLoginInput $input + * + * @return AuthLoginPostCommand + */ + public function authenticate(array $input): AuthLoginPostCommand; + + /** + * @param TAuthLogoutInput $input + * + * @return AuthLogoutPostCommand + */ + public function logout(array $input): AuthLogoutPostCommand; + + /** + * @param TAuthRefreshInput $input + * + * @return AuthRefreshPostCommand + */ + public function refresh(array $input): AuthRefreshPostCommand; +} diff --git a/tests/Unit/Domain/Application/Auth/Command/AuthCommandFactoryTest.php b/tests/Unit/Domain/Application/Auth/Command/AuthCommandFactoryTest.php new file mode 100644 index 0000000..6efb097 --- /dev/null +++ b/tests/Unit/Domain/Application/Auth/Command/AuthCommandFactoryTest.php @@ -0,0 +1,87 @@ + + * + * 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\Application\Auth\Command; + +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactory; +use Phalcon\Api\Domain\Application\Auth\Command\AuthLoginPostCommand; +use Phalcon\Api\Domain\Application\Auth\Command\AuthLogoutPostCommand; +use Phalcon\Api\Domain\Application\Auth\Command\AuthRefreshPostCommand; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function uniqid; + +final class AuthCommandFactoryTest extends AbstractUnitTestCase +{ + public function testAuthenticate(): void + { + $sanitizer = $this->container->get(AuthSanitizer::class); + $factory = new AuthCommandFactory($sanitizer); + + $input = [ + 'email' => 'user@example.com', + 'password' => 's3cr3t', + ]; + + $command = $factory->authenticate($input); + + $this->assertInstanceOf(AuthLoginPostCommand::class, $command); + + $expected = 'user@example.com'; + $actual = $command->email; + $this->assertSame($expected, $actual); + + $expected = 's3cr3t'; + $actual = $command->password; + $this->assertSame($expected, $actual); + } + + public function testLogout(): void + { + $sanitizer = $this->container->get(AuthSanitizer::class); + $factory = new AuthCommandFactory($sanitizer); + + $token = uniqid('token-'); + $input = [ + 'token' => $token, + ]; + + $command = $factory->logout($input); + + $this->assertInstanceOf(AuthLogoutPostCommand::class, $command); + + $expected = $token; + $actual = $command->token; + $this->assertSame($expected, $actual); + } + + public function testRefresh(): void + { + $sanitizer = $this->container->get(AuthSanitizer::class); + $factory = new AuthCommandFactory($sanitizer); + + $token = uniqid('token-'); + $input = [ + 'token' => $token, + ]; + + $command = $factory->refresh($input); + + $this->assertInstanceOf(AuthRefreshPostCommand::class, $command); + + $expected = $token; + $actual = $command->token; + $this->assertSame($expected, $actual); + } +} From 25e07421c8074b55cb7c8113344a7d88f2cbac12 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:13:36 -0600 Subject: [PATCH 233/301] [#.x] - refactored container definitions --- src/Domain/Infrastructure/Container.php | 212 ++++++------------ .../Enums/Container/AuthDefinitionsEnum.php | 116 +++++++--- .../Enums/Container/CommonDefinitionsEnum.php | 69 +++++- .../Container/DefinitionsEnumInterface.php | 33 +++ .../Enums/Container/UserDefinitionsEnum.php | 138 ++++++++---- 5 files changed, 341 insertions(+), 227 deletions(-) create mode 100644 src/Domain/Infrastructure/Enums/Container/DefinitionsEnumInterface.php diff --git a/src/Domain/Infrastructure/Container.php b/src/Domain/Infrastructure/Container.php index 6c290d7..a12e871 100644 --- a/src/Domain/Infrastructure/Container.php +++ b/src/Domain/Infrastructure/Container.php @@ -13,34 +13,22 @@ namespace Phalcon\Api\Domain\Infrastructure; +use Phalcon\Api\Domain\Infrastructure\CommandBus\ContainerHandlerLocator; use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mappers\UserMapper; -use Phalcon\Api\Domain\Infrastructure\Encryption\Security; use Phalcon\Api\Domain\Infrastructure\Enums\Container\AuthDefinitionsEnum; use Phalcon\Api\Domain\Infrastructure\Enums\Container\CommonDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\DefinitionsEnumInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Container\UserDefinitionsEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; -use Phalcon\Api\Domain\Infrastructure\Middleware\HealthMiddleware; -use Phalcon\Api\Domain\Infrastructure\Middleware\NotFoundMiddleware; -use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenClaimsMiddleware; -use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenPresenceMiddleware; -use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenRevokedMiddleware; -use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenStructureMiddleware; -use Phalcon\Api\Domain\Infrastructure\Middleware\ValidateTokenUserMiddleware; -use Phalcon\Api\Responder\JsonResponder; use Phalcon\Cache\AdapterFactory; use Phalcon\Cache\Cache; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Di\Di; use Phalcon\Di\Service; use Phalcon\Filter\FilterFactory; -use Phalcon\Filter\Validation; -use Phalcon\Http\Request; -use Phalcon\Http\Response; use Phalcon\Logger\Adapter\Stream; use Phalcon\Logger\Logger; use Phalcon\Storage\SerializerFactory; -use Phalcon\Support\Registry; use function sprintf; @@ -71,145 +59,52 @@ class Container extends Di /** @var string */ public const APPLICATION = 'application'; /** @var string */ - public const CACHE = 'cache'; - /** @var string */ - public const CONNECTION = 'connection'; - /** @var string */ - public const ENV = 'env'; - /** @var string */ public const EVENTS_MANAGER = 'eventsManager'; /** @var string */ public const FILTER = 'filter'; /** @var string */ - public const JWT_TOKEN = 'jwt.token'; - /** @var string */ - public const JWT_TOKEN_CACHE = 'jwt.token.cache'; - /** @var string */ - public const JWT_TOKEN_MANAGER = 'jwt.token.manager'; - /** @var string */ - public const LOGGER = 'logger'; - /** @var string */ - public const REGISTRY = 'registry'; - /** @var string */ public const REQUEST = 'request'; /** @var string */ public const RESPONSE = 'response'; /** @var string */ public const ROUTER = 'router'; /** @var string */ - public const SECURITY = Security::class; - /** @var string */ public const TIME = 'time'; - /** @var string */ - public const VALIDATION = Validation::class; - /** - * Middleware - */ - public const MIDDLEWARE_HEALTH = HealthMiddleware::class; - public const MIDDLEWARE_NOT_FOUND = NotFoundMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_CLAIMS = ValidateTokenClaimsMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_PRESENCE = ValidateTokenPresenceMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_REVOKED = ValidateTokenRevokedMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_STRUCTURE = ValidateTokenStructureMiddleware::class; - public const MIDDLEWARE_VALIDATE_TOKEN_USER = ValidateTokenUserMiddleware::class; - /** - * Facades - */ - public const AUTH_FACADE = 'auth.facade'; - public const USER_FACADE = 'user.facade'; - public const USER_FACADE_UPDATE = 'user.facade.update'; - /** - * Services - */ - public const AUTH_LOGIN_POST_SERVICE = 'service.auth.login.post'; - public const AUTH_LOGOUT_POST_SERVICE = 'service.auth.logout.post'; - public const AUTH_REFRESH_POST_SERVICE = 'service.auth.refresh.post'; - public const USER_DELETE_SERVICE = 'service.user.delete'; - public const USER_GET_SERVICE = 'service.user.get'; - public const USER_POST_SERVICE = 'service.user.post'; - public const USER_PUT_SERVICE = 'service.user.put'; - /** - * Mappers - */ - public const USER_MAPPER = UserMapper::class; - /** - * Responders - */ - public const RESPONDER_JSON = JsonResponder::class; - /** - * Repositories - */ - public const USER_REPOSITORY = 'user.repository'; - /** - * Sanitizers - */ - public const AUTH_SANITIZER = 'auth.sanitizer'; - public const USER_SANITIZER = 'user.sanitizer'; - /** - * Validators - */ - public const AUTH_LOGIN_VALIDATOR = 'auth.validator.login'; - public const AUTH_TOKEN_VALIDATOR = 'auth.validator.token'; - public const USER_VALIDATOR = 'user.validator.insert'; - public const USER_VALIDATOR_UPDATE = 'user.validator.update'; public function __construct() { - $this->services = [ - /** - * Base services - */ - self::CACHE => $this->getServiceCache(), - self::CONNECTION => $this->getServiceConnection(), - self::ENV => new Service(EnvManager::class, true), - self::EVENTS_MANAGER => new Service(CommonDefinitionsEnum::EventsManager->definition(), true), - self::FILTER => $this->getServiceFilter(), - self::JWT_TOKEN => new Service(CommonDefinitionsEnum::JWTToken->definition(), true), - self::JWT_TOKEN_CACHE => new Service(CommonDefinitionsEnum::JWTTokenCache->definition(), true), - self::JWT_TOKEN_MANAGER => new Service(CommonDefinitionsEnum::JWTTokenManager->definition(), true), - self::LOGGER => $this->getServiceLogger(), - self::REGISTRY => new Service(Registry::class, true), - self::REQUEST => new Service(Request::class, true), - self::RESPONSE => new Service(Response::class, true), - self::ROUTER => new Service(CommonDefinitionsEnum::Router->definition(), true), - - /** - * Facades - */ - self::AUTH_FACADE => new Service(AuthDefinitionsEnum::AuthFacade->definition()), - self::USER_FACADE => new Service(UserDefinitionsEnum::UserFacade->definition()), - self::USER_FACADE_UPDATE => new Service(UserDefinitionsEnum::UserFacadeUpdate->definition()), - - /** - * Repositories - */ - self::USER_REPOSITORY => new Service(UserDefinitionsEnum::UserRepository->definition()), - - /** - * Sanitizers - */ - self::AUTH_SANITIZER => new Service(AuthDefinitionsEnum::AuthSanitizer->definition()), - self::USER_SANITIZER => new Service(UserDefinitionsEnum::UserSanitizer->definition()), - - /** - * Validators - */ - self::AUTH_LOGIN_VALIDATOR => new Service(AuthDefinitionsEnum::AuthLoginValidator->definition()), - self::AUTH_TOKEN_VALIDATOR => new Service(AuthDefinitionsEnum::AuthTokenValidator->definition()), - self::USER_VALIDATOR => new Service(UserDefinitionsEnum::UserValidator->definition()), - self::USER_VALIDATOR_UPDATE => new Service(UserDefinitionsEnum::UserValidatorUpdate->definition()), - - /** - * Services - */ - self::AUTH_LOGIN_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthLoginPost->definition()), - self::AUTH_LOGOUT_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthLogoutPost->definition()), - self::AUTH_REFRESH_POST_SERVICE => new Service(AuthDefinitionsEnum::AuthRefreshPost->definition()), - self::USER_DELETE_SERVICE => new Service(UserDefinitionsEnum::UserDelete->definition()), - self::USER_GET_SERVICE => new Service(UserDefinitionsEnum::UserGet->definition()), - self::USER_POST_SERVICE => new Service(UserDefinitionsEnum::UserPost->definition()), - self::USER_PUT_SERVICE => new Service(UserDefinitionsEnum::UserPut->definition()), + /** + * Trying to keep this as clean and as configurable as possible. + * + * The enumerations that hold the definition for each service, also + * keep the sharable state (shared service or not) and also reference + * this file's constants for their values where necessary. Therefore, + * we can register the services with a simple loop for each enumeration. + * + * + * Base services + */ + $services = [ + Cache::class => $this->getServiceCache(), + Connection::class => $this->getServiceConnection(), + self::FILTER => $this->getServiceFilter(), + Logger::class => $this->getServiceLogger(), + ContainerHandlerLocator::class => $this->getServiceHandlerLocator(), ]; + /** + * Common services + */ + $services = $this->registerFromEnum(CommonDefinitionsEnum::class, $services); + /** + * Auth related services + */ + $services = $this->registerFromEnum(AuthDefinitionsEnum::class, $services); + /** + * User related services + */ + $services = $this->registerFromEnum(UserDefinitionsEnum::class, $services); + + $this->services = $services; parent::__construct(); } @@ -222,7 +117,7 @@ private function getServiceCache(): Service return new Service( function () { /** @var EnvManager $env */ - $env = $this->getShared(self::ENV); + $env = $this->getShared(EnvManager::class); /** @var string $prefix */ $prefix = $env->get('CACHE_PREFIX', '-rest-'); @@ -265,7 +160,7 @@ private function getServiceConnection(): Service return new Service( function () { /** @var EnvManager $env */ - $env = $this->getShared(self::ENV); + $env = $this->getShared(EnvManager::class); /** @var string $dbName */ $dbName = $env->get('DB_NAME', 'phalcon'); @@ -313,6 +208,18 @@ function () { ); } + /** + * @return Service + */ + private function getServiceHandlerLocator(): Service + { + return new Service( + function () { + return new ContainerHandlerLocator($this); + } + ); + } + /** * @return Service */ @@ -321,7 +228,7 @@ private function getServiceLogger(): Service return new Service( function () { /** @var EnvManager $env */ - $env = $this->getShared(self::ENV); + $env = $this->getShared(EnvManager::class); /** @var string $logName */ $logName = $env->get('LOG_FILENAME', 'rest-api'); @@ -338,4 +245,27 @@ function () { } ); } + + /** + * @param string $enum + * @param array $services + * + * @return array + */ + private function registerFromEnum(string $enum, array $services): array + { + /** @var DefinitionsEnumInterface[] $items */ + $items = $enum::cases(); + foreach ($items as $definition) { + /** @var string $serviceName */ + $serviceName = $definition->value; + + $services[$serviceName] = new Service( + $definition->definition(), + $definition->isShared() + ); + } + + return $services; + } } diff --git a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php index 12a0f0a..820ccbe 100644 --- a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php @@ -13,27 +13,40 @@ namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactory; +use Phalcon\Api\Domain\Application\Auth\Facade\AuthFacade; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthLoginPostHandler; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthLogoutPostHandler; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthRefreshPostHandler; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLogoutPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthRefreshPostService; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Facades\AuthFacade; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizers\AuthSanitizer; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthLoginValidator; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validators\AuthTokenValidator; -use Phalcon\Api\Domain\Services\Auth\LoginPostService; -use Phalcon\Api\Domain\Services\Auth\LogoutPostService; -use Phalcon\Api\Domain\Services\Auth\RefreshPostService; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthLoginValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthTokenValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Filter\Validation; /** * @phpstan-import-type TService from Container */ -enum AuthDefinitionsEnum +enum AuthDefinitionsEnum: string implements DefinitionsEnumInterface { - case AuthLoginPost; - case AuthLogoutPost; - case AuthRefreshPost; - case AuthFacade; - case AuthSanitizer; - case AuthLoginValidator; - case AuthTokenValidator; + case AuthCommandFactory = AuthCommandFactory::class; + case AuthLoginPost = AuthLoginPostService::class; + case AuthLogoutPost = AuthLogoutPostService::class; + case AuthRefreshPost = AuthRefreshPostService::class; + case AuthFacade = AuthFacade::class; + case AuthLoginHandler = AuthLoginPostHandler::class; + case AuthLogoutHandler = AuthLogoutPostHandler::class; + case AuthRefreshHandler = AuthRefreshPostHandler::class; + case AuthSanitizer = AuthSanitizer::class; + case AuthLoginValidator = AuthLoginValidator::class; + case AuthTokenValidator = AuthTokenValidator::class; /** * @return TService @@ -41,42 +54,54 @@ enum AuthDefinitionsEnum public function definition(): array { return match ($this) { - self::AuthLoginPost => [ - 'className' => LoginPostService::class, + self::AuthCommandFactory => [ + 'className' => AuthCommandFactory::class, 'arguments' => [ [ 'type' => 'service', - 'name' => Container::AUTH_FACADE, + 'name' => AuthSanitizer::class, + ], + ], + ], + self::AuthLoginPost => $this->getService(AuthLoginPostService::class), + self::AuthLogoutPost => $this->getService(AuthLogoutPostService::class), + self::AuthRefreshPost => $this->getService(AuthRefreshPostService::class), + self::AuthFacade => [ + 'className' => AuthFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CommandBus::class, ], [ 'type' => 'service', - 'name' => Container::AUTH_LOGIN_VALIDATOR, + 'name' => AuthCommandFactory::class, ], ], ], - self::AuthLogoutPost => $this->getService(LogoutPostService::class), - self::AuthRefreshPost => $this->getService(RefreshPostService::class), - self::AuthFacade => [ - 'className' => AuthFacade::class, + self::AuthLoginHandler => [ + 'className' => AuthLoginPostHandler::class, 'arguments' => [ [ 'type' => 'service', - 'name' => Container::USER_REPOSITORY, + 'name' => UserRepository::class, ], [ 'type' => 'service', - 'name' => Container::AUTH_SANITIZER, + 'name' => TokenManager::class, ], [ 'type' => 'service', - 'name' => Container::JWT_TOKEN_MANAGER, + 'name' => Security::class, ], [ 'type' => 'service', - 'name' => Container::SECURITY, + 'name' => AuthLoginValidator::class, ], ], ], + self::AuthLogoutHandler => $this->getHandlerService(AuthLogoutPostHandler::class), + self::AuthRefreshHandler => $this->getHandlerService(AuthRefreshPostHandler::class), self::AuthSanitizer => [ 'className' => AuthSanitizer::class, 'arguments' => [ @@ -91,7 +116,7 @@ public function definition(): array 'arguments' => [ [ 'type' => 'service', - 'name' => Container::VALIDATION, + 'name' => Validation::class, ], ], ], @@ -100,36 +125,59 @@ public function definition(): array 'arguments' => [ [ 'type' => 'service', - 'name' => Container::JWT_TOKEN_MANAGER, + 'name' => TokenManager::class, ], [ 'type' => 'service', - 'name' => Container::USER_REPOSITORY, + 'name' => UserRepository::class, ], [ 'type' => 'service', - 'name' => Container::VALIDATION, + 'name' => Validation::class, ], ], ], }; } + public function isShared(): bool + { + return false; + } + /** + * @param string $className + * * @return TService */ - private function getService(string $className): array + private function getHandlerService(string $className): array { return [ 'className' => $className, 'arguments' => [ [ 'type' => 'service', - 'name' => Container::AUTH_FACADE, + 'name' => TokenManager::class, ], [ 'type' => 'service', - 'name' => Container::AUTH_TOKEN_VALIDATOR, + 'name' => AuthTokenValidator::class, + ], + ], + ]; + } + + /** + * @return TService + */ + private function getService(string $className): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => AuthFacade::class, ], ], ]; diff --git a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php index c3f426f..c891497 100644 --- a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php @@ -13,23 +13,37 @@ namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; +use Phalcon\Api\Domain\Infrastructure\CommandBus\ContainerHandlerLocator; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Responder\JsonResponder; +use Phalcon\Cache\Cache; use Phalcon\Events\Manager as EventsManager; +use Phalcon\Http\Request; +use Phalcon\Http\Response; use Phalcon\Mvc\Router; +use Phalcon\Support\Registry; /** * @phpstan-import-type TService from Container */ -enum CommonDefinitionsEnum +enum CommonDefinitionsEnum: string implements DefinitionsEnumInterface { - case EventsManager; - case JWTToken; - case JWTTokenCache; - case JWTTokenManager; - case Router; + case CommandBus = CommandBus::class; + case EventsManager = Container::EVENTS_MANAGER; + case EnvManager = EnvManager::class; + case JsonResponder = JsonResponder::class; + case JWTToken = JWTToken::class; + case JWTTokenCache = TokenCache::class; + case JWTTokenManager = TokenManager::class; + case Registry = Registry::class; + case Request = Container::REQUEST; + case Response = Container::RESPONSE; + case Router = Container::ROUTER; /** * @return TService @@ -37,6 +51,15 @@ enum CommonDefinitionsEnum public function definition(): array { return match ($this) { + self::CommandBus => [ + 'className' => CommandBus::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => ContainerHandlerLocator::class, + ], + ], + ], self::EventsManager => [ 'className' => EventsManager::class, 'calls' => [ @@ -51,12 +74,18 @@ public function definition(): array ], ], ], + self::EnvManager => [ + 'className' => EnvManager::class, + ], + self::JsonResponder => [ + 'className' => JsonResponder::class, + ], self::JWTToken => [ 'className' => JWTToken::class, 'arguments' => [ [ 'type' => 'service', - 'name' => Container::ENV, + 'name' => EnvManager::class, ], ], ], @@ -65,7 +94,7 @@ public function definition(): array 'arguments' => [ [ 'type' => 'service', - 'name' => Container::CACHE, + 'name' => Cache::class, ], ], ], @@ -74,18 +103,27 @@ public function definition(): array 'arguments' => [ [ 'type' => 'service', - 'name' => Container::JWT_TOKEN_CACHE, + 'name' => TokenCache::class, ], [ 'type' => 'service', - 'name' => Container::ENV, + 'name' => EnvManager::class, ], [ 'type' => 'service', - 'name' => Container::JWT_TOKEN, + 'name' => JWTToken::class, ], ], ], + self::Registry => [ + 'className' => Registry::class, + ], + self::Request => [ + 'className' => Request::class, + ], + self::Response => [ + 'className' => Response::class, + ], self::Router => [ 'className' => Router::class, 'arguments' => [ @@ -97,4 +135,13 @@ public function definition(): array ] }; } + + public function isShared(): bool + { + return match ($this) { + self::JWTToken, + self::JWTTokenCache => false, + default => true, + }; + } } diff --git a/src/Domain/Infrastructure/Enums/Container/DefinitionsEnumInterface.php b/src/Domain/Infrastructure/Enums/Container/DefinitionsEnumInterface.php new file mode 100644 index 0000000..5afec2d --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Container/DefinitionsEnumInterface.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\Infrastructure\Enums\Container; + +use BackedEnum; +use Phalcon\Api\Domain\Infrastructure\Container; + +/** + * @phpstan-import-type TService from Container + */ +interface DefinitionsEnumInterface extends BackedEnum +{ + /** + * @return TService + */ + public function definition(): array; + + /** + * @return bool + */ + public function isShared(): bool; +} diff --git a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php index 38e3565..c741396 100644 --- a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php @@ -13,32 +13,48 @@ namespace Phalcon\Api\Domain\Infrastructure\Enums\Container; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Application\User\Facade\UserFacade; +use Phalcon\Api\Domain\Application\User\Handler\UserDeleteHandler; +use Phalcon\Api\Domain\Application\User\Handler\UserGetHandler; +use Phalcon\Api\Domain\Application\User\Handler\UserPostHandler; +use Phalcon\Api\Domain\Application\User\Handler\UserPutHandler; +use Phalcon\Api\Domain\Application\User\Service\UserDeleteService; +use Phalcon\Api\Domain\Application\User\Service\UserGetService; +use Phalcon\Api\Domain\Application\User\Service\UserPostService; +use Phalcon\Api\Domain\Application\User\Service\UserPutService; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Facades\UserFacade; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepository; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizers\UserSanitizer; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidator; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validators\UserValidatorUpdate; -use Phalcon\Api\Domain\Services\User\UserDeleteService; -use Phalcon\Api\Domain\Services\User\UserGetService; -use Phalcon\Api\Domain\Services\User\UserPostService; -use Phalcon\Api\Domain\Services\User\UserPutService; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserValidatorUpdate; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\Filter\Validation; +use Phalcon\Support\Registry; /** * @phpstan-import-type TService from Container */ -enum UserDefinitionsEnum +enum UserDefinitionsEnum: string implements DefinitionsEnumInterface { - case UserDelete; - case UserGet; - case UserPost; - case UserPut; - case UserFacade; - case UserFacadeUpdate; - case UserRepository; - case UserSanitizer; - case UserValidator; - case UserValidatorUpdate; + case UserCommandFactory = UserCommandFactory::class; + case UserDelete = UserDeleteService::class; + case UserGet = UserGetService::class; + case UserPost = UserPostService::class; + case UserPut = UserPutService::class; + case UserFacade = UserFacade::class; + case UserDeleteHandler = UserDeleteHandler::class; + case UserGetHandler = UserGetHandler::class; + case UserPostHandler = UserPostHandler::class; + case UserPutHandler = UserPutHandler::class; + case UserMapper = UserMapper::class; + case UserRepository = UserRepository::class; + case UserSanitizer = UserSanitizer::class; + case UserValidator = UserValidator::class; + case UserValidatorUpdate = UserValidatorUpdate::class; /** * @return TService @@ -46,78 +62,113 @@ enum UserDefinitionsEnum public function definition(): array { return match ($this) { + self::UserCommandFactory => [ + 'className' => UserCommandFactory::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserSanitizer::class, + ], + ], + ], self::UserDelete => $this->getService(UserDeleteService::class), self::UserGet => $this->getService(UserGetService::class), self::UserPost => $this->getService(UserPostService::class), - self::UserPut => [ - 'className' => UserPutService::class, + self::UserPut => $this->getService(UserPutService::class), + self::UserFacade => [ + 'className' => UserFacade::class, 'arguments' => [ [ 'type' => 'service', - 'name' => Container::USER_FACADE_UPDATE, + 'name' => CommandBus::class, + ], + [ + 'type' => 'service', + 'name' => UserCommandFactory::class, ], ], ], - self::UserFacade => [ - 'className' => UserFacade::class, + self::UserDeleteHandler => [ + 'className' => UserDeleteHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserRepository::class, + ], + ], + ], + self::UserGetHandler => [ + 'className' => UserGetHandler::class, 'arguments' => [ [ 'type' => 'service', - 'name' => Container::USER_SANITIZER, + 'name' => UserRepository::class, ], + ], + ], + self::UserPostHandler => [ + 'className' => UserPostHandler::class, + 'arguments' => [ [ 'type' => 'service', - 'name' => Container::USER_VALIDATOR, + 'name' => UserValidator::class, ], [ 'type' => 'service', - 'name' => Container::USER_MAPPER, + 'name' => UserMapper::class, ], [ 'type' => 'service', - 'name' => Container::USER_REPOSITORY, + 'name' => UserRepository::class, ], [ 'type' => 'service', - 'name' => Container::SECURITY, + 'name' => Registry::class, + ], + [ + 'type' => 'service', + 'name' => Security::class, ], ], ], - self::UserFacadeUpdate => [ - 'className' => UserFacade::class, + self::UserPutHandler => [ + 'className' => UserPutHandler::class, 'arguments' => [ [ 'type' => 'service', - 'name' => Container::USER_SANITIZER, + 'name' => UserValidatorUpdate::class, ], [ 'type' => 'service', - 'name' => Container::USER_VALIDATOR_UPDATE, + 'name' => UserMapper::class, ], [ 'type' => 'service', - 'name' => Container::USER_MAPPER, + 'name' => UserRepository::class, ], [ 'type' => 'service', - 'name' => Container::USER_REPOSITORY, + 'name' => Registry::class, ], [ 'type' => 'service', - 'name' => Container::SECURITY, + 'name' => Security::class, ], ], ], + self::UserMapper => [ + 'className' => UserMapper::class, + ], self::UserRepository => [ 'className' => UserRepository::class, 'arguments' => [ [ 'type' => 'service', - 'name' => Container::CONNECTION, + 'name' => Connection::class, ], [ 'type' => 'service', - 'name' => Container::USER_MAPPER, + 'name' => UserMapper::class, ], ], ], @@ -135,6 +186,11 @@ public function definition(): array }; } + public function isShared(): bool + { + return false; + } + /** * @param class-string $className * @@ -147,7 +203,7 @@ private function getService(string $className): array 'arguments' => [ [ 'type' => 'service', - 'name' => Container::USER_FACADE, + 'name' => UserFacade::class, ], ], ]; @@ -165,7 +221,7 @@ private function getServiceValidator(string $className): array 'arguments' => [ [ 'type' => 'service', - 'name' => Container::VALIDATION, + 'name' => Validation::class, ], ], ]; From fce538ec54e7f25a74cbdd5a9fca5a4a8e5a8e50 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:14:03 -0600 Subject: [PATCH 234/301] [#.x] - refactored facades to use the bus --- .../Application/Auth/Facade/AuthFacade.php | 72 +++++++ .../Application/User/Facade/UserFacade.php | 94 +++++++++ .../DataSource/Auth/Facades/AuthFacade.php | 194 ------------------ 3 files changed, 166 insertions(+), 194 deletions(-) create mode 100644 src/Domain/Application/Auth/Facade/AuthFacade.php create mode 100644 src/Domain/Application/User/Facade/UserFacade.php delete mode 100644 src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php diff --git a/src/Domain/Application/Auth/Facade/AuthFacade.php b/src/Domain/Application/Auth/Facade/AuthFacade.php new file mode 100644 index 0000000..c32dffb --- /dev/null +++ b/src/Domain/Application/Auth/Facade/AuthFacade.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\Domain\Application\Auth\Facade; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactoryInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBusInterface; + +/** + * @phpstan-import-type TAuthLoginInput from InputTypes + * @phpstan-import-type TAuthLogoutInput from InputTypes + * @phpstan-import-type TAuthRefreshInput from InputTypes + */ +final readonly class AuthFacade +{ + /** + * @param CommandBusInterface $bus + */ + public function __construct( + private CommandBusInterface $bus, + private AuthCommandFactoryInterface $factory + ) { + } + + /** + * Authenticates users (login) + * + * @param TAuthLoginInput $input + * + * @return Payload + */ + public function authenticate(array $input): Payload + { + return $this->bus->dispatch($this->factory->authenticate($input)); + } + + /** + * Logout: revoke refresh token after parsing/validation. + * + * @param TAuthLogoutInput $input + * + * @return Payload + */ + public function logout(array $input): Payload + { + return $this->bus->dispatch($this->factory->logout($input)); + } + + /** + * Refresh: validate refresh token, issue new tokens via TokenManager. + * + * @param TAuthLogoutInput $input + * + * @return Payload + */ + public function refresh(array $input): Payload + { + return $this->bus->dispatch($this->factory->refresh($input)); + } +} diff --git a/src/Domain/Application/User/Facade/UserFacade.php b/src/Domain/Application/User/Facade/UserFacade.php new file mode 100644 index 0000000..6f10a7b --- /dev/null +++ b/src/Domain/Application/User/Facade/UserFacade.php @@ -0,0 +1,94 @@ + + * + * 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\Application\User\Facade; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactoryInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBusInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +/** + * Orchestration for workflow + * + * - Sanitization + * - DTO creation + * - Validation + * - Pre-operation checks (when necessary) + * - Repository operation + * + * @phpstan-import-type TUserInput from InputTypes + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + */ +final readonly class UserFacade +{ + /** + * @param CommandBusInterface $bus + * @param UserCommandFactoryInterface $factory + */ + public function __construct( + private CommandBusInterface $bus, + private UserCommandFactoryInterface $factory, + ) { + } + + /** + * Delete a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function delete(array $input): Payload + { + return $this->bus->dispatch($this->factory->delete($input)); + } + + /** + * Get a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function get(array $input): Payload + { + return $this->bus->dispatch($this->factory->get($input)); + } + + /** + * Create a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function insert(array $input): Payload + { + return $this->bus->dispatch($this->factory->insert($input)); + } + + /** + * Create a user. + * + * @param TUserInput $input + * + * @return Payload + */ + public function update(array $input): Payload + { + return $this->bus->dispatch($this->factory->update($input)); + } +} diff --git a/src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php b/src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php deleted file mode 100644 index c4f24a8..0000000 --- a/src/Domain/Infrastructure/DataSource/Auth/Facades/AuthFacade.php +++ /dev/null @@ -1,194 +0,0 @@ - - * - * 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\Infrastructure\DataSource\Auth\Facades; - -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\ADR\Payload; -use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO\AuthInput; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interfaces\SanitizerInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repositories\UserRepositoryInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; -use Phalcon\Api\Domain\Infrastructure\Encryption\Security; -use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; -use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; - -/** - * @phpstan-import-type TAuthLoginInput from InputTypes - * @phpstan-import-type TAuthLogoutInput from InputTypes - * @phpstan-import-type TAuthRefreshInput from InputTypes - */ -final class AuthFacade -{ - /** - * @param UserRepositoryInterface $repository - * @param SanitizerInterface $sanitizer - * @param TokenManagerInterface $tokenManager - * @param Security $security - */ - public function __construct( - private readonly UserRepositoryInterface $repository, - private readonly SanitizerInterface $sanitizer, - private readonly TokenManagerInterface $tokenManager, - private readonly Security $security, - ) { - } - - /** - * Authenticates users (login) - * - * @param TAuthLoginInput $input - * @param ValidatorInterface $validator - * - * @return Payload - */ - public function authenticate( - array $input, - ValidatorInterface $validator - ): Payload { - /** - * Data Transfer Object - */ - $dto = AuthInput::new($this->sanitizer, $input); - - /** - * Validate - */ - $validation = $validator->validate($dto); - if (!$validation->isValid()) { - return Payload::unauthorized($validation->getErrors()); - } - - /** - * Find the user by email - */ - /** @var string $email */ - $email = $dto->email; - $domainUser = $this->repository->findByEmail($email); - if (null === $domainUser) { - return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); - } - - /** - * Verify the password - */ - /** @var string $suppliedPassword */ - $suppliedPassword = $dto->password; - /** @var string $dbPassword */ - $dbPassword = $domainUser->password; - if (true !== $this->security->verify($suppliedPassword, $dbPassword)) { - return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); - } - - /** - * Issue a new set of tokens - */ - $tokens = $this->tokenManager->issue($domainUser); - - /** - * Construct the response - */ - $results = [ - 'authenticated' => true, - 'user' => [ - 'id' => $domainUser->id, - 'name' => $domainUser->fullName(), - 'email' => $domainUser->email, - ], - 'jwt' => [ - 'token' => $tokens['token'], - 'refreshToken' => $tokens['refreshToken'], - ], - ]; - - return Payload::success($results); - } - - /** - * Logout: revoke refresh token after parsing/validation. - * - * @param TAuthLogoutInput $input - * @param ValidatorInterface $validator - * - * @return Payload - */ - public function logout( - array $input, - ValidatorInterface $validator - ): Payload { - /** - * Data Transfer Object - */ - $dto = AuthInput::new($this->sanitizer, $input); - - /** - * Validate - */ - $validation = $validator->validate($dto); - if (!$validation->isValid()) { - return Payload::unauthorized($validation->getErrors()); - } - - /** - * If we are here validation has passed and the Result object - * has the user in the meta store - */ - /** @var User $domainUser */ - $domainUser = $validation->getMeta('user'); - - $this->tokenManager->revoke($domainUser); - - return Payload::success(['authenticated' => false]); - } - - /** - * Refresh: validate refresh token, issue new tokens via TokenManager. - * - * @param TAuthLogoutInput $input - * @param ValidatorInterface $validator - * - * @return Payload - */ - public function refresh( - array $input, - ValidatorInterface $validator - ): Payload { - /** - * Data Transfer Object - */ - $dto = AuthInput::new($this->sanitizer, $input); - - /** - * Validate - */ - $validation = $validator->validate($dto); - if (!$validation->isValid()) { - return Payload::unauthorized($validation->getErrors()); - } - - /** - * If we are here validation has passed and the Result object - * has the user in the meta store - */ - /** @var User $domainUser */ - $domainUser = $validation->getMeta('user'); - - $tokens = $this->tokenManager->refresh($domainUser); - - return Payload::success([ - 'token' => $tokens['token'], - 'refreshToken' => $tokens['refreshToken'], - ]); - } -} From faba40e76eefdc967778ca2880555ccda579e78c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:14:42 -0600 Subject: [PATCH 235/301] [#.x] - added auth handlers --- .../AbstractAuthLogoutRefreshHandler.php | 35 ++ .../Auth/Handler/AuthLoginPostHandler.php | 108 ++++++ .../Auth/Handler/AuthLogoutPostHandler.php | 54 +++ .../Auth/Handler/AuthRefreshPostHandler.php | 57 ++++ .../Handler/AbstractUserPutPostHandler.php | 93 +++++ .../Auth/Handler/LoginPostHandlerTest.php | 182 ++++++++++ .../Auth/Handler/LogoutPostHandlerTest.php | 140 ++++++++ .../Auth/Handler/RefreshPostHandlerTest.php | 320 ++++++++++++++++++ 8 files changed, 989 insertions(+) create mode 100644 src/Domain/Application/Auth/Handler/AbstractAuthLogoutRefreshHandler.php create mode 100644 src/Domain/Application/Auth/Handler/AuthLoginPostHandler.php create mode 100644 src/Domain/Application/Auth/Handler/AuthLogoutPostHandler.php create mode 100644 src/Domain/Application/Auth/Handler/AuthRefreshPostHandler.php create mode 100644 src/Domain/Application/User/Handler/AbstractUserPutPostHandler.php create mode 100644 tests/Unit/Domain/Application/Auth/Handler/LoginPostHandlerTest.php create mode 100644 tests/Unit/Domain/Application/Auth/Handler/LogoutPostHandlerTest.php create mode 100644 tests/Unit/Domain/Application/Auth/Handler/RefreshPostHandlerTest.php diff --git a/src/Domain/Application/Auth/Handler/AbstractAuthLogoutRefreshHandler.php b/src/Domain/Application/Auth/Handler/AbstractAuthLogoutRefreshHandler.php new file mode 100644 index 0000000..9b190da --- /dev/null +++ b/src/Domain/Application/Auth/Handler/AbstractAuthLogoutRefreshHandler.php @@ -0,0 +1,35 @@ + + * + * 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\Application\Auth\Handler; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; + +/** + * @phpstan-import-type TAuthLogoutInput from InputTypes + */ +abstract class AbstractAuthLogoutRefreshHandler implements HandlerInterface +{ + /** + * @param TokenManagerInterface $tokenManager + * @param ValidatorInterface $validator + */ + public function __construct( + protected readonly TokenManagerInterface $tokenManager, + protected readonly ValidatorInterface $validator, + ) { + } +} diff --git a/src/Domain/Application/Auth/Handler/AuthLoginPostHandler.php b/src/Domain/Application/Auth/Handler/AuthLoginPostHandler.php new file mode 100644 index 0000000..0f7365b --- /dev/null +++ b/src/Domain/Application/Auth/Handler/AuthLoginPostHandler.php @@ -0,0 +1,108 @@ + + * + * 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\Application\Auth\Handler; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\Auth\Command\AuthLoginPostCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; + +/** + * @phpstan-import-type TAuthLoginInput from InputTypes + */ +final class AuthLoginPostHandler implements HandlerInterface +{ + /** + * @param UserRepositoryInterface $repository + * @param TokenManagerInterface $tokenManager + * @param Security $security + * @param ValidatorInterface $validator + */ + public function __construct( + private readonly UserRepositoryInterface $repository, + private readonly TokenManagerInterface $tokenManager, + private readonly Security $security, + private readonly ValidatorInterface $validator + ) { + } + + /** + * Authenticates users (login) + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + /** + * Validate + */ + /** @var AuthLoginPostCommand $command */ + $validation = $this->validator->validate($command); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * Find the user by email + */ + /** @var string $email */ + $email = $command->email; + $domainUser = $this->repository->findByEmail($email); + if (null === $domainUser) { + return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); + } + + /** + * Verify the password + */ + /** @var string $suppliedPassword */ + $suppliedPassword = $command->password; + /** @var string $dbPassword */ + $dbPassword = $domainUser->password; + if (true !== $this->security->verify($suppliedPassword, $dbPassword)) { + return Payload::unauthorized([HttpCodesEnum::AppIncorrectCredentials->error()]); + } + + /** + * Issue a new set of tokens + */ + $tokens = $this->tokenManager->issue($domainUser); + + /** + * Construct the response + */ + $results = [ + 'authenticated' => true, + 'user' => [ + 'id' => $domainUser->id, + 'name' => $domainUser->fullName(), + 'email' => $domainUser->email, + ], + 'jwt' => [ + 'token' => $tokens['token'], + 'refreshToken' => $tokens['refreshToken'], + ], + ]; + + return Payload::success($results); + } +} diff --git a/src/Domain/Application/Auth/Handler/AuthLogoutPostHandler.php b/src/Domain/Application/Auth/Handler/AuthLogoutPostHandler.php new file mode 100644 index 0000000..26f1dc7 --- /dev/null +++ b/src/Domain/Application/Auth/Handler/AuthLogoutPostHandler.php @@ -0,0 +1,54 @@ + + * + * 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\Application\Auth\Handler; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; + +/** + * @phpstan-import-type TAuthLogoutInput from InputTypes + */ +final class AuthLogoutPostHandler extends AbstractAuthLogoutRefreshHandler +{ + /** + * Logout: revoke refresh token after parsing/validation. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + /** + * Validate + */ + $validation = $this->validator->validate($command); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * If we are here validation has passed and the Result object + * has the user in the meta store + */ + /** @var User $domainUser */ + $domainUser = $validation->getMeta('user'); + + $this->tokenManager->revoke($domainUser); + + return Payload::success(['authenticated' => false]); + } +} diff --git a/src/Domain/Application/Auth/Handler/AuthRefreshPostHandler.php b/src/Domain/Application/Auth/Handler/AuthRefreshPostHandler.php new file mode 100644 index 0000000..be368c1 --- /dev/null +++ b/src/Domain/Application/Auth/Handler/AuthRefreshPostHandler.php @@ -0,0 +1,57 @@ + + * + * 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\Application\Auth\Handler; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; + +/** + * @phpstan-import-type TAuthRefreshInput from InputTypes + */ +final class AuthRefreshPostHandler extends AbstractAuthLogoutRefreshHandler +{ + /** + * Refresh: validate refresh token, issue new tokens via TokenManager. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + /** + * Validate + */ + $validation = $this->validator->validate($command); + if (!$validation->isValid()) { + return Payload::unauthorized($validation->getErrors()); + } + + /** + * If we are here validation has passed and the Result object + * has the user in the meta store + */ + /** @var User $domainUser */ + $domainUser = $validation->getMeta('user'); + + $tokens = $this->tokenManager->refresh($domainUser); + + return Payload::success([ + 'token' => $tokens['token'], + 'refreshToken' => $tokens['refreshToken'], + ]); + } +} diff --git a/src/Domain/Application/User/Handler/AbstractUserPutPostHandler.php b/src/Domain/Application/User/Handler/AbstractUserPutPostHandler.php new file mode 100644 index 0000000..aa89c39 --- /dev/null +++ b/src/Domain/Application/User/Handler/AbstractUserPutPostHandler.php @@ -0,0 +1,93 @@ + + * + * 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\Application\User\Handler; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Support\Registry; + +use function array_filter; + +/** + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + */ +abstract class AbstractUserPutPostHandler implements HandlerInterface +{ + /** + * @param ValidatorInterface $validator + * @param MapperInterface $mapper + * @param UserRepositoryInterface $repository + * @param Security $security + */ + public function __construct( + protected readonly ValidatorInterface $validator, + protected readonly MapperInterface $mapper, + protected readonly UserRepositoryInterface $repository, + protected readonly Registry $registry, + private readonly Security $security, + ) { + } + + /** + * @param TUserDbRecordOptional $row + * + * @return TUserDbRecordOptional + */ + protected function cleanupFields(array $row): array + { + unset($row['usr_id']); + + return array_filter( + $row, + static fn($v) => $v !== null && $v !== '' + ); + } + + /** + * @param HttpCodesEnum $item + * @param string $message + * + * @return Payload + */ + protected function getErrorPayload( + HttpCodesEnum $item, + string $message + ): Payload { + return Payload::error([[$item->text() . $message]]); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDomainToDbRecord + */ + protected function processPassword(array $input): array + { + if (null !== $input['usr_password']) { + $plain = $input['usr_password']; + $hashed = $this->security->hash($plain); + + $input['usr_password'] = $hashed; + } + + return $input; + } +} diff --git a/tests/Unit/Domain/Application/Auth/Handler/LoginPostHandlerTest.php b/tests/Unit/Domain/Application/Auth/Handler/LoginPostHandlerTest.php new file mode 100644 index 0000000..99285e0 --- /dev/null +++ b/tests/Unit/Domain/Application/Auth/Handler/LoginPostHandlerTest.php @@ -0,0 +1,182 @@ + + * + * 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\Application\Auth\Handler; + +use Faker\Factory; +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactory; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthLoginPostHandler; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; + +final class LoginPostHandlerTest extends AbstractUnitTestCase +{ + public function testHandlerEmptyCredentials(): void + { + /** @var AuthLoginPostHandler $handler */ + $handler = $this->container->get(AuthLoginPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $command = $factory->authenticate([]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppIncorrectCredentials->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testHandlerWithCredentials(): void + { + /** @var AuthLoginPostHandler $handler */ + $handler = $this->container->get(AuthLoginPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $migration = new UsersMigration($this->getConnection()); + + /** + * Setting the password to something we know + */ + $password = 'password'; + + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; + $payload = [ + 'email' => $email, + 'password' => $password, + ]; + + $command = $factory->authenticate($payload); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey('authenticated', $data); + $this->assertArrayHasKey('user', $data); + $this->assertArrayHasKey('jwt', $data); + + $authenticated = $data['authenticated']; + $user = $data['user']; + $jwt = $data['jwt']; + + $actual = $authenticated; + $this->assertTrue($actual); + + $expected = $dbUser['usr_id']; + $actual = $user['id']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_email']; + $actual = $user['email']; + $this->assertSame($expected, $actual); + + $expected = trim( + $dbUser['usr_name_last'] + . ', ' + . $dbUser['usr_name_first'] + . ' ' + . $dbUser['usr_name_middle'] + ); + + $actual = $user['name']; + $this->assertSame($expected, $actual); + + $this->assertArrayHasKey('token', $jwt); + $this->assertArrayHasKey('refreshToken', $jwt); + + $this->assertNotEmpty($jwt['token']); + $this->assertNotEmpty($jwt['refreshToken']); + } + + public function testHandlerWrongCredentials(): void + { + /** @var AuthLoginPostHandler $handler */ + $handler = $this->container->get(AuthLoginPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + $faker = Factory::create(); + + /** + * Issue a wrong password + */ + $payload = [ + 'email' => $faker->email(), + 'password' => $faker->password(), + ]; + $command = $factory->authenticate($payload); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppIncorrectCredentials->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testHandlerWrongCredentialsForUser(): void + { + /** @var AuthLoginPostHandler $handler */ + $handler = $this->container->get(AuthLoginPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + $migration = new UsersMigration($this->getConnection()); + + $password = 'password'; + + /** + * Issue a wrong password + */ + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; + $payload = [ + 'email' => $email, + 'password' => $password . '2', + ]; + + $command = $factory->authenticate($payload); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppIncorrectCredentials->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Auth/Handler/LogoutPostHandlerTest.php b/tests/Unit/Domain/Application/Auth/Handler/LogoutPostHandlerTest.php new file mode 100644 index 0000000..8dddcab --- /dev/null +++ b/tests/Unit/Domain/Application/Auth/Handler/LogoutPostHandlerTest.php @@ -0,0 +1,140 @@ + + * + * 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\Application\Auth\Handler; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactory; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthLoginPostHandler; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthLogoutPostHandler; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Cache\Cache; + +final class LogoutPostHandlerTest extends AbstractUnitTestCase +{ + public function testHandlerEmptyToken(): void + { + /** @var AuthLogoutPostHandler $handler */ + $handler = $this->container->get(AuthLogoutPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $command = $factory->authenticate([]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppTokenNotPresent->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testHandlerSuccess(): void + { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var AuthLoginPostHandler $handler */ + $loginHandler = $this->container->get(AuthLoginPostHandler::class); + /** @var Cache $cache */ + $cache = $this->container->getShared(Cache::class); + /** @var AuthLogoutPostHandler $handler */ + $handler = $this->container->get(AuthLogoutPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + $migration = new UsersMigration($this->getConnection()); + + /** + * Setting the password to something we know + */ + $password = 'password'; + + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; + $input = [ + 'email' => $email, + 'password' => $password, + ]; + + $command = $factory->authenticate($input); + $payload = $loginHandler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey('authenticated', $data); + + $authenticated = $data['authenticated']; + + $actual = $authenticated; + $this->assertTrue($actual); + + $token = $data['jwt']['token']; + $domainUser = $userMapper->domain($dbUser); + $tokenKey = CacheConstants::getCacheTokenKey($domainUser, $token); + + $actual = $cache->has($tokenKey); + $this->assertTrue($actual); + + $refreshToken = $data['jwt']['refreshToken']; + $tokenKey = CacheConstants::getCacheTokenKey($domainUser, $refreshToken); + + $actual = $cache->has($tokenKey); + $this->assertTrue($actual); + + $refreshToken = $data['jwt']['refreshToken']; + + /** + * Logout now + */ + $input = [ + 'token' => $refreshToken, + ]; + $command = $factory->logout($input); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey('authenticated', $data); + + $authenticated = $data['authenticated']; + + $actual = $authenticated; + $this->assertFalse($actual); + + $actual = $cache->has($tokenKey); + $this->assertFalse($actual); + } +} diff --git a/tests/Unit/Domain/Application/Auth/Handler/RefreshPostHandlerTest.php b/tests/Unit/Domain/Application/Auth/Handler/RefreshPostHandlerTest.php new file mode 100644 index 0000000..bfbf86e --- /dev/null +++ b/tests/Unit/Domain/Application/Auth/Handler/RefreshPostHandlerTest.php @@ -0,0 +1,320 @@ + + * + * 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\Application\Auth\Handler; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactory; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthLoginPostHandler; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthRefreshPostHandler; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Encryption\Security\JWT\Token\Item; +use Phalcon\Encryption\Security\JWT\Token\Token; + +final class RefreshPostHandlerTest extends AbstractUnitTestCase +{ + public function testHandlerEmptyToken(): void + { + /** @var AuthRefreshPostHandler $handler */ + $handler = $this->container->get(AuthRefreshPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $command = $factory->refresh([]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppTokenNotPresent->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testHandlerInvalidToken(): void + { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + $user = $this->getNewUserData(); + $user['usr_id'] = 1; + $domainUser = $userMapper->domain($user); + $errors = [ + ['Incorrect token data'], + ]; + + /** + * Set up mock handlers + */ + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(true); + + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + 'validate', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + $mockJWT->method('getUser')->willReturn($domainUser); + $mockJWT->method('validate')->willReturn($errors); + + + /** + * Replace the handler with the mocked one + */ + $this->container->set(JWTToken::class, $mockJWT); + + /** @var AuthRefreshPostHandler $handler */ + $handler = $this->container->get(AuthRefreshPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $command = $factory->refresh(['token' => '1234']); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = ['Incorrect token data']; + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testHandlerNotRefreshToken(): void + { + /** + * Set up mock handlers + */ + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(false); + + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + + /** + * Replace the handler with the mocked one + */ + $this->container->set(JWTToken::class, $mockJWT); + + /** @var AuthRefreshPostHandler $handler */ + $handler = $this->container->get(AuthRefreshPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $command = $factory->refresh(['token' => '1234']); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppTokenNotValid->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } + + public function testHandlerWithCredentials(): void + { + /** @var AuthLoginPostHandler $handler */ + $loginHandler = $this->container->get(AuthLoginPostHandler::class); + /** @var AuthRefreshPostHandler $handler */ + $handler = $this->container->get(AuthRefreshPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $migration = new UsersMigration($this->getConnection()); + + /** + * Setting the password to something we know + */ + $password = 'password'; + + $dbUser = $this->getNewUser($migration, ['usr_password' => $password]); + $email = $dbUser['usr_email']; + $payload = [ + 'email' => $email, + 'password' => $password, + ]; + + $command = $factory->authenticate($payload); + $payload = $loginHandler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $result = $payload->getResult(); + $jwt = $result['data']['jwt']; + + $command = $factory->refresh(['token' => $jwt['refreshToken']]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey('token', $data); + $this->assertArrayHasKey('refreshToken', $data); + + $this->assertIsString($data['token']); + $this->assertIsString($data['refreshToken']); + $this->assertNotEmpty($data['token']); + $this->assertNotEmpty($data['refreshToken']); + } + + public function testHandlerWrongUser(): void + { + /** + * Set up mock handlers + */ + $mockItem = $this + ->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'get', + ] + ) + ->getMock() + ; + $mockItem->method('get')->willReturn(true); + + $mockToken = $this + ->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getClaims', + ] + ) + ->getMock() + ; + $mockToken->method('getClaims')->willReturn($mockItem); + + $mockJWT = $this + ->getMockBuilder(JWTToken::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'getObject', + 'getUser', + ] + ) + ->getMock() + ; + $mockJWT->method('getObject')->willReturn($mockToken); + $mockJWT->method('getUser')->willReturn(null); + + + /** + * Replace the handler with the mocked one + */ + $this->container->set(JWTToken::class, $mockJWT); + + /** @var AuthRefreshPostHandler $handler */ + $handler = $this->container->get(AuthRefreshPostHandler::class); + /** @var AuthCommandFactory $factory */ + $factory = $this->container->get(AuthCommandFactory::class); + + $command = $factory->refresh(['token' => '1234']); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UNAUTHORIZED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $expected = HttpCodesEnum::AppTokenInvalidUser->error(); + $actual = $actual['errors'][0]; + $this->assertSame($expected, $actual); + } +} From c0cca08183580b4aedf8e80f031d62e18d46478f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:14:59 -0600 Subject: [PATCH 236/301] [#.x] - added user handlers --- .../User/Handler/UserDeleteHandler.php | 64 +++ .../User/Handler/UserGetHandler.php | 60 +++ .../User/Handler/UserPostHandler.php | 119 ++++ .../User/Handler/UserPutHandler.php | 129 +++++ .../User/Handler/UserHandlerDeleteTest.php | 99 ++++ .../User/Handler/UserHandlerGetTest.php | 165 ++++++ .../User/Handler/UserHandlerPostTest.php | 408 ++++++++++++++ .../User/Handler/UserHandlerPutTest.php | 510 ++++++++++++++++++ 8 files changed, 1554 insertions(+) create mode 100644 src/Domain/Application/User/Handler/UserDeleteHandler.php create mode 100644 src/Domain/Application/User/Handler/UserGetHandler.php create mode 100644 src/Domain/Application/User/Handler/UserPostHandler.php create mode 100644 src/Domain/Application/User/Handler/UserPutHandler.php create mode 100644 tests/Unit/Domain/Application/User/Handler/UserHandlerDeleteTest.php create mode 100644 tests/Unit/Domain/Application/User/Handler/UserHandlerGetTest.php create mode 100644 tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php create mode 100644 tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php diff --git a/src/Domain/Application/User/Handler/UserDeleteHandler.php b/src/Domain/Application/User/Handler/UserDeleteHandler.php new file mode 100644 index 0000000..bb830fe --- /dev/null +++ b/src/Domain/Application/User/Handler/UserDeleteHandler.php @@ -0,0 +1,64 @@ + + * + * 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\Application\User\Handler; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\User\Command\UserDeleteCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; + +final readonly class UserDeleteHandler implements HandlerInterface +{ + /** + * @param UserRepositoryInterface $repository + */ + public function __construct( + private UserRepositoryInterface $repository, + ) { + } + + /** + * Get a user. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + /** @var UserDeleteCommand $command */ + $userId = $command->id; + + /** + * Success + */ + if ($userId > 0) { + $rowCount = $this->repository->deleteById($userId); + + if (0 !== $rowCount) { + return Payload::deleted( + [ + 'Record deleted successfully [#' . $userId . '].', + ], + ); + } + } + + /** + * 404 + */ + return Payload::notFound(); + } +} diff --git a/src/Domain/Application/User/Handler/UserGetHandler.php b/src/Domain/Application/User/Handler/UserGetHandler.php new file mode 100644 index 0000000..4638e99 --- /dev/null +++ b/src/Domain/Application/User/Handler/UserGetHandler.php @@ -0,0 +1,60 @@ + + * + * 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\Application\User\Handler; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\User\Command\UserGetCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; + +final class UserGetHandler implements HandlerInterface +{ + /** + * @param UserRepositoryInterface $repository + */ + public function __construct( + private readonly UserRepositoryInterface $repository, + ) { + } + + /** + * Get a user. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + /** @var UserGetCommand $command */ + $userId = $command->id; + + /** + * Success + */ + if ($userId > 0) { + $user = $this->repository->findById($userId); + + if (null !== $user) { + return Payload::success([$user->id => $user->toArray()]); + } + } + + /** + * 404 + */ + return Payload::notFound(); + } +} diff --git a/src/Domain/Application/User/Handler/UserPostHandler.php b/src/Domain/Application/User/Handler/UserPostHandler.php new file mode 100644 index 0000000..a0e42b6 --- /dev/null +++ b/src/Domain/Application/User/Handler/UserPostHandler.php @@ -0,0 +1,119 @@ + + * + * 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\Application\User\Handler; + +use PDOException; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; + +/** + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + */ +final class UserPostHandler extends AbstractUserPutPostHandler +{ + /** + * Create a user. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + $validation = $this->validator->validate($command); + if (!$validation->isValid()) { + return Payload::invalid($validation->getErrors()); + } + + /** + * Array for inserting + */ + $user = $this->mapper->db($command); + + /** + * Pre-insert checks and manipulations + */ + $user = $this->preInsert($user); + + /** + * Insert the record + */ + try { + $userId = $this->repository->insert($user); + } catch (PDOException $ex) { + /** + * @todo send generic response and log the error + */ + return $this->getErrorPayload( + HttpCodesEnum::AppCannotCreateDatabaseRecord, + $ex->getMessage() + ); + } + + if ($userId < 1) { + return $this->getErrorPayload( + HttpCodesEnum::AppCannotCreateDatabaseRecord, + 'No id returned' + ); + } + + /** + * Get the user from the database + */ + /** @var User $domainUser */ + $domainUser = $this->repository->findById($userId); + + /** + * Return the user back + */ + return Payload::created([$domainUser->id => $domainUser->toArray()]); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDbRecordOptional + */ + private function preInsert(array $input): array + { + $result = $this->processPassword($input); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + /** @var User $sessionUser */ + $sessionUser = $this->registry->get('user'); + + /** + * Created and updated user IDs come from the user in the session + */ + $result['usr_created_usr_id'] = $sessionUser->id; + $result['usr_updated_usr_id'] = $sessionUser->id; + + /** + * Set the created/updated dates if need be + */ + if (true === empty($result['usr_created_date'])) { + $result['usr_created_date'] = $now; + } + if (true === empty($result['usr_updated_date'])) { + $result['usr_updated_date'] = $now; + } + + /** @var TUserDbRecordOptional $result */ + return $this->cleanupFields($result); + } +} diff --git a/src/Domain/Application/User/Handler/UserPutHandler.php b/src/Domain/Application/User/Handler/UserPutHandler.php new file mode 100644 index 0000000..12ad103 --- /dev/null +++ b/src/Domain/Application/User/Handler/UserPutHandler.php @@ -0,0 +1,129 @@ + + * + * 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\Application\User\Handler; + +use PDOException; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\User\Command\UserPutCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; + +/** + * @phpstan-import-type TUserDomainToDbRecord from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + */ +final class UserPutHandler extends AbstractUserPutPostHandler +{ + /** + * Delete a user. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + $validation = $this->validator->validate($command); + if (!$validation->isValid()) { + return Payload::invalid($validation->getErrors()); + } + + /** + * Check if the user exists, If not, return an error + */ + /** @var UserPutCommand $command */ + $domainUser = $this->repository->findById($command->id); + + if (null === $domainUser) { + return Payload::notFound(); + } + + /** + * Array for updating + */ + $user = $this->mapper->db($command); + + /** + * Pre-update checks and manipulations + */ + $user = $this->preUpdate($user); + + /** + * Update the record + */ + try { + $userId = $this->repository->update($command->id, $user); + } catch (PDOException $ex) { + /** + * @todo send generic response and log the error + */ + return $this->getErrorPayload( + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + $ex->getMessage() + ); + } + + if ($userId < 1) { + return $this->getErrorPayload( + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + 'No id returned' + ); + } + + /** + * Get the user from the database + */ + /** @var User $domainUser */ + $domainUser = $this->repository->findById($userId); + + /** + * Return the user back + */ + return Payload::updated([$domainUser->id => $domainUser->toArray()]); + } + + /** + * @param TUserDomainToDbRecord $input + * + * @return TUserDbRecordOptional + */ + private function preUpdate(array $input): array + { + $result = $this->processPassword($input); + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + /** @var User $sessionUser */ + $sessionUser = $this->registry->get('user'); + + $result['usr_updated_usr_id'] = $sessionUser->id; + + /** + * Set updated date to now if it has not been set + */ + if (true === empty($result['usr_updated_date'])) { + $result['usr_updated_date'] = $now; + } + + /** + * Remove createdDate and createdUserId - cannot be changed. This + * needs to be here because we don't want to touch those fields. + */ + unset($result['usr_created_date'], $result['usr_created_usr_id']); + + + return $this->cleanupFields($result); + } +} diff --git a/tests/Unit/Domain/Application/User/Handler/UserHandlerDeleteTest.php b/tests/Unit/Domain/Application/User/Handler/UserHandlerDeleteTest.php new file mode 100644 index 0000000..ef51fa8 --- /dev/null +++ b/tests/Unit/Domain/Application/User/Handler/UserHandlerDeleteTest.php @@ -0,0 +1,99 @@ + + * + * 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\Application\User\Handler; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Application\User\Handler\UserDeleteHandler; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; + +final class UserHandlerDeleteTest extends AbstractUnitTestCase +{ + public function testHandlerWithUserId(): void + { + /** @var UserDeleteHandler $handler */ + $handler = $this->container->get(UserDeleteHandler::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + /** + * We need to ask for a user to be deleted with an ID that does not + * exist in the database. To ensure that, we will create a user, + * delete it and then try to delete the same user with that ID + */ + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + + $command = $factory->delete(['id' => $userId]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::DELETED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $expected = [ + 'Record deleted successfully [#' . $userId . '].', + ]; + $actual = $data; + $this->assertSame($expected, $actual); + + /** + * Now deleting it again, will result in a 404 + */ + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerZeroUserId(): void + { + /** @var UserDeleteHandler $handler */ + $handler = $this->container->get(UserDeleteHandler::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $command = $factory->delete(['id' => 0]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/User/Handler/UserHandlerGetTest.php b/tests/Unit/Domain/Application/User/Handler/UserHandlerGetTest.php new file mode 100644 index 0000000..3161502 --- /dev/null +++ b/tests/Unit/Domain/Application/User/Handler/UserHandlerGetTest.php @@ -0,0 +1,165 @@ + + * + * 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\Application\User\Handler; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Application\User\Handler\UserGetHandler; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; + +final class UserHandlerGetTest extends AbstractUnitTestCase +{ + public function testHandlerEmptyUserId(): void + { + /** @var UserGetHandler $handler */ + $handler = $this->container->get(UserGetHandler::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $command = $factory->get([]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerWithUserId(): void + { + /** @var UserGetHandler $handler */ + $handler = $this->container->get(UserGetHandler::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + + $command = $factory->get(['id' => $userId]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $user = $actual['data']; + $key = array_key_first($user); + $user = $user[$key]; + + $expected = $dbUser['usr_id']; + $actual = $user['id']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_status_flag']; + $actual = $user['status']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_email']; + $actual = $user['email']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_password']; + $actual = $user['password']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_prefix']; + $actual = $user['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_first']; + $actual = $user['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_middle']; + $actual = $user['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_last']; + $actual = $user['nameLast']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_name_suffix']; + $actual = $user['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_issuer']; + $actual = $user['issuer']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_token_password']; + $actual = $user['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_token_id']; + $actual = $user['tokenId']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_preferences']; + $actual = $user['preferences']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_date']; + $actual = $user['createdDate']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_created_usr_id']; + $actual = $user['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_date']; + $actual = $user['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $dbUser['usr_updated_usr_id']; + $actual = $user['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testHandlerWrongUserId(): void + { + /** @var UserGetHandler $handler */ + $handler = $this->container->get(UserGetHandler::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $command = $factory->get(['id' => 999999]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php b/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php new file mode 100644 index 0000000..cb9256e --- /dev/null +++ b/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php @@ -0,0 +1,408 @@ + + * + * 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\Application\User\Handler; + +use DateTimeImmutable; +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Application\User\Handler\UserPostHandler; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Support\Registry; + +use function htmlspecialchars; + +final class UserHandlerPostTest extends AbstractUnitTestCase +{ + public function testHandlerFailureNoIdReturned(): void + { + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'insert', + ] + ) + ->getMock() + ; + $userRepository->method('insert')->willReturn(0); + + $this->container->setShared(UserRepository::class, $userRepository); + + /** @var UserPostHandler $handler */ + $handler = $this->container->get(UserPostHandler::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainData = $domainUser->toArray(); + $command = $factory->insert($domainData); + + $payload = $handler->__invoke($command); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot create database record: No id returned']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerFailurePdoError(): void + { + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'insert', + ] + ) + ->getMock() + ; + $userRepository + ->method('insert') + ->willThrowException(new PDOException('abcde')) + ; + + $this->container->setShared(UserRepository::class, $userRepository); + + /** @var UserPostHandler $handler */ + $handler = $this->container->get(UserPostHandler::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainData = $domainUser->toArray(); + $command = $factory->insert($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot create database record: abcde']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerFailureValidation(): void + { + /** @var UserPostHandler $handler */ + $handler = $this->container->get(UserPostHandler::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $userData = $this->getNewUserData(); + + unset( + $userData['usr_email'], + $userData['usr_password'], + $userData['usr_issuer'], + $userData['usr_token_password'], + $userData['usr_token_id'] + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + $domainData = $domainUser->toArray(); + $command = $factory->insert($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + ['Field email is required'], + ['Field email must be an email address'], + ['Field password is required'], + ['Field issuer is required'], + ['Field tokenPassword is required'], + ['Field tokenId is required'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerSuccess(): void + { + /** @var UserPostHandler $handler */ + $handler = $this->container->get(UserPostHandler::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $userData = $this->getNewUserData(); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainData = $domainUser->toArray(); + $command = $factory->insert($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $userId = array_key_first($data); + + $this->assertGreaterThan(0, $userId); + + $data = $data[$userId]; + + $expected = $domainData['status']; + $actual = $data['status']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $actual = str_starts_with($data['password'], '$argon2i$'); + $this->assertTrue($actual); + + $expected = htmlspecialchars($domainData['namePrefix']); + $actual = $data['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameFirst']); + $actual = $data['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameMiddle']); + $actual = $data['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameLast']); + $actual = $data['nameLast']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameSuffix']); + $actual = $data['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['issuer']; + $actual = $data['issuer']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenPassword']; + $actual = $data['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenId']; + $actual = $data['tokenId']; + $this->assertSame($expected, $actual); + + $actual = $data['preferences']; + $this->assertNull($actual); + + $expected = $domainData['createdDate']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + $expected = $domainUser->id; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['updatedDate']; + $actual = $data['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $domainUser->id; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testHandlerSuccessEmptyDates(): void + { + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); + /** @var UserPostHandler $handler */ + $handler = $this->container->get(UserPostHandler::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $userData = $this->getNewUserData(); + unset( + $userData['usr_created_date'], + $userData['usr_updated_date'], + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainData = $domainUser->toArray(); + $command = $factory->insert($domainData); + + $payload = $handler->__invoke($command); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $userId = array_key_first($data); + + $this->assertGreaterThan(0, $userId); + + $data = $data[$userId]; + + $expected = $domainData['status']; + $actual = $data['status']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $actual = str_starts_with($data['password'], '$argon2i$'); + $this->assertTrue($actual); + + $expected = htmlspecialchars($domainData['namePrefix']); + $actual = $data['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameFirst']); + $actual = $data['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameMiddle']); + $actual = $data['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameLast']); + $actual = $data['nameLast']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameSuffix']); + $actual = $data['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['issuer']; + $actual = $data['issuer']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenPassword']; + $actual = $data['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenId']; + $actual = $data['tokenId']; + $this->assertSame($expected, $actual); + + $actual = $data['preferences']; + $this->assertNull($actual); + + $actual = $data['createdDate']; + $this->assertStringContainsString($today, $actual); + + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $actual = $data['updatedDate']; + $this->assertStringContainsString($today, $actual); + + $expected = 0; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php b/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php new file mode 100644 index 0000000..7be5e9b --- /dev/null +++ b/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php @@ -0,0 +1,510 @@ + + * + * 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\Application\User\Handler; + +use DateTimeImmutable; +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Application\User\Handler\UserPutHandler; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Support\Registry; + +final class UserHandlerPutTest extends AbstractUnitTestCase +{ + public function testHandlerFailureNoIdReturned(): void + { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $findByUser = $userMapper->domain($userData); + + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'update', + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('update')->willReturn(0); + $userRepository->method('findById')->willReturn($findByUser); + + $this->container->setShared(UserRepository::class, $userRepository); + + /** @var UserPutHandler $handler */ + $handler = $this->container->get(UserPutHandler::class); + + /** + * Update user + */ + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + + $updateUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $updateUser); + + $updateUser = $updateUser->toArray(); + $command = $factory->update($updateUser); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot update database record: No id returned']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerFailurePdoError(): void + { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $findByUser = $userMapper->domain($userData); + + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'update', + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('findById')->willReturn($findByUser); + $userRepository + ->method('update') + ->willThrowException(new PDOException('abcde')) + ; + + $this->container->setShared(UserRepository::class, $userRepository); + + /** @var UserPutHandler $handler */ + $handler = $this->container->get(UserPutHandler::class); + + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + + /** + * $userData is a db record. We need a domain object here + */ + $updateUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $updateUser); + + $updateUser = $updateUser->toArray(); + $command = $factory->update($updateUser); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot update database record: abcde']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerFailureRecordNotFound(): void + { + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + $userData = $this->getNewUserData(); + + $userData['usr_id'] = 1; + + $userRepository = $this + ->getMockBuilder(UserRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('findById')->willReturn(null); + + $this->container->setShared(UserRepository::class, $userRepository); + + /** @var UserPutHandler $handler */ + $handler = $this->container->get(UserPutHandler::class); + + /** + * Update user + */ + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + + $updateUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $updateUser); + + $updateUser = $updateUser->toArray(); + $command = $factory->update($updateUser); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerFailureValidation(): void + { + /** @var UserPutHandler $handler */ + $handler = $this->container->get(UserPutHandler::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + $userData = $this->getNewUserData(); + + $userData['usr_email'] = 'wrong-email'; + unset($userData['usr_id']); + + /** + * $userData is a db record. We need a domain object here + */ + $updateUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $updateUser); + + $updateUser = $updateUser->toArray(); + $command = $factory->update($updateUser); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + ['Field id is not a valid absolute integer and greater than 0'], + ['Field email must be an email address'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerSuccess(): void + { + /** @var UserPutHandler $handler */ + $handler = $this->container->get(UserPutHandler::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + $userId = $dbUser['usr_id']; + $userData = $this->getNewUserData(); + $userData['usr_id'] = $userId; + /** + * Don't hash the password + */ + $userData['usr_password'] = $this->getStrongPassword(); + + $userData['usr_created_usr_id'] = 4; + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainData = $domainUser->toArray(); + $command = $factory->update($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey($userId, $data); + + $data = $data[$userId]; + + $expected = $domainData['status']; + $actual = $data['status']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $actual = str_starts_with($data['password'], '$argon2i$'); + $this->assertTrue($actual); + + $expected = htmlspecialchars($domainData['namePrefix']); + $actual = $data['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameFirst']); + $actual = $data['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameMiddle']); + $actual = $data['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameLast']); + $actual = $data['nameLast']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameSuffix']); + $actual = $data['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['issuer']; + $actual = $data['issuer']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenPassword']; + $actual = $data['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenId']; + $actual = $data['tokenId']; + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = $data['preferences']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = $dbUser['usr_created_date']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['updatedDate']; + $actual = $data['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $domainUser->id; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testHandlerSuccessEmptyDates(): void + { + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); + /** @var UserPutHandler $handler */ + $handler = $this->container->get(UserPutHandler::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserCommandFactory $factory */ + $factory = $this->container->get(UserCommandFactory::class); + + $migration = new UsersMigration($this->getConnection()); + $dbUser = $this->getNewUser($migration); + + $userId = $dbUser['usr_id']; + $userData = $this->getNewUserData(); + $userData['usr_id'] = $userId; + /** + * Don't hash the password + */ + $userData['usr_password'] = $this->getStrongPassword(); + + unset( + $userData['usr_updated_date'], + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainData = $domainUser->toArray(); + $command = $factory->update($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey($userId, $data); + + $data = $data[$userId]; + + $expected = $domainData['status']; + $actual = $data['status']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $actual = str_starts_with($data['password'], '$argon2i$'); + $this->assertTrue($actual); + + $expected = htmlspecialchars($domainData['namePrefix']); + $actual = $data['namePrefix']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameFirst']); + $actual = $data['nameFirst']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameMiddle']); + $actual = $data['nameMiddle']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameLast']); + $actual = $data['nameLast']; + $this->assertSame($expected, $actual); + + $expected = htmlspecialchars($domainData['nameSuffix']); + $actual = $data['nameSuffix']; + $this->assertSame($expected, $actual); + + $expected = $domainData['issuer']; + $actual = $data['issuer']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenPassword']; + $actual = $data['tokenPassword']; + $this->assertSame($expected, $actual); + + $expected = $domainData['tokenId']; + $actual = $data['tokenId']; + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = $data['preferences']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = $dbUser['usr_created_date']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $today = date('Y-m-d '); + $actual = $data['updatedDate']; + $this->assertStringContainsString($today, $actual); + + $expected = $domainUser->id; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } +} From e2343a479f4384a68a4cdc68382c077fda5d46fe Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:15:18 -0600 Subject: [PATCH 237/301] [#.x] - adjusted test --- .../Encryption/TokenManagerTest.php | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php b/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php index a35a98c..af5e024 100644 --- a/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php +++ b/tests/Unit/Domain/Infrastructure/Encryption/TokenManagerTest.php @@ -14,31 +14,51 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\Encryption; use Exception; -use Phalcon\Api\Domain\Infrastructure\Container; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; -use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCacheInterface; use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCacheInterface; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Encryption\Security\JWT\Token\Token; use function rand; final class TokenManagerTest extends AbstractUnitTestCase { + public function testGetObjectSuccess(): void + { + /** @var EnvManager $env */ + $env = $this->container->get(EnvManager::class); + /** @var TokenCacheInterface $tokenCache */ + $tokenCache = $this->container->get(TokenCache::class); + /** @var JWTToken $jwtToken */ + $jwtToken = $this->container->get(JwtToken::class); + $userData = $this->getNewUserData(); + $userData['usr_id'] = rand(1, 100); + + $token = $this->getUserToken($userData); + + $manager = new TokenManager($tokenCache, $env, $jwtToken); + + $expected = Token::class; + $actual = $manager->getObject($token); + $this->assertInstanceOf($expected, $actual); + } + public function testGetObjectTokenEmpty(): void { /** @var EnvManager $env */ - $env = $this->container->get(Container::ENV); + $env = $this->container->get(EnvManager::class); /** @var TokenCacheInterface $tokenCache */ - $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + $tokenCache = $this->container->get(TokenCache::class); /** @var JWTToken $jwtToken */ - $jwtToken = $this->container->get(Container::JWT_TOKEN); + $jwtToken = $this->container->get(JwtToken::class); $manager = new TokenManager($tokenCache, $env, $jwtToken); $expected = null; - $actual = $manager->getObject(''); + $actual = $manager->getObject(''); $this->assertSame($expected, $actual); $actual = $manager->getObject(null); @@ -48,9 +68,9 @@ public function testGetObjectTokenEmpty(): void public function testGetObjectWithException(): void { /** @var EnvManager $env */ - $env = $this->container->get(Container::ENV); + $env = $this->container->get(EnvManager::class); /** @var TokenCacheInterface $tokenCache */ - $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); + $tokenCache = $this->container->get(TokenCache::class); $mockJWTToken = $this ->getMockBuilder(JWTToken::class) ->disableOriginalConstructor() @@ -69,30 +89,10 @@ public function testGetObjectWithException(): void $manager = new TokenManager($tokenCache, $env, $mockJWTToken); $expected = null; - $actual = $manager->getObject(''); + $actual = $manager->getObject(''); $this->assertSame($expected, $actual); $actual = $manager->getObject(null); $this->assertSame($expected, $actual); } - - public function testGetObjectSuccess(): void - { - /** @var EnvManager $env */ - $env = $this->container->get(Container::ENV); - /** @var TokenCacheInterface $tokenCache */ - $tokenCache = $this->container->get(Container::JWT_TOKEN_CACHE); - /** @var JWTToken $jwtToken */ - $jwtToken = $this->container->get(Container::JWT_TOKEN); - $userData = $this->getNewUserData(); - $userData['usr_id'] = rand(1, 100); - - $token = $this->getUserToken($userData); - - $manager = new TokenManager($tokenCache, $env, $jwtToken); - - $expected = Token::class; - $actual = $manager->getObject($token); - $this->assertInstanceOf($expected, $actual); - } } From c8f99e374f8fccab272350a29030706ab1b40703 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 20:15:36 -0600 Subject: [PATCH 238/301] [#.x] - added abstract user service --- .../User/Service/AbstractUserService.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/Domain/Application/User/Service/AbstractUserService.php diff --git a/src/Domain/Application/User/Service/AbstractUserService.php b/src/Domain/Application/User/Service/AbstractUserService.php new file mode 100644 index 0000000..32ebeb5 --- /dev/null +++ b/src/Domain/Application/User/Service/AbstractUserService.php @@ -0,0 +1,28 @@ + + * + * 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\Application\User\Service; + +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\Application\User\Facade\UserFacade; + +abstract class AbstractUserService implements DomainInterface +{ + /** + * @param UserFacade $facade + */ + public function __construct( + protected readonly UserFacade $facade, + ) { + } +} From 80bf78025a500165becc98ca3bdf8a9dd653c663 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 28 Nov 2025 22:05:35 -0600 Subject: [PATCH 239/301] [#.x] - added more tests --- .../CommandBus/ContainerHandlerLocatorTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Unit/Domain/Infrastructure/CommandBus/ContainerHandlerLocatorTest.php b/tests/Unit/Domain/Infrastructure/CommandBus/ContainerHandlerLocatorTest.php index 89fd346..48c3d30 100644 --- a/tests/Unit/Domain/Infrastructure/CommandBus/ContainerHandlerLocatorTest.php +++ b/tests/Unit/Domain/Infrastructure/CommandBus/ContainerHandlerLocatorTest.php @@ -14,6 +14,7 @@ namespace Phalcon\Api\Tests\Unit\Domain\Infrastructure\CommandBus; use Phalcon\Api\Domain\Infrastructure\CommandBus\ContainerHandlerLocator; +use Phalcon\Api\Domain\Infrastructure\Exceptions\HandlerRuntimeException; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Application\Command\CommandFixture; use Phalcon\Api\Tests\Fixtures\Domain\Application\Handler\HandlerFixture; @@ -40,4 +41,20 @@ public function testDispatch(): void $actual = $result; $this->assertInstanceOf($expected, $actual); } + + public function testDispatchError(): void + { + $this->expectException(HandlerRuntimeException::class); + $this->expectExceptionMessage( + 'No handler configured for ' . CommandFixture::class + ); + + $name = uniqid('name-'); + $command = new CommandFixture($name); + + /** @var ContainerHandlerLocator $locator */ + $locator = $this->container->get(ContainerHandlerLocator::class); + + $locator->resolve($command); + } } From 33dff5321be51f59f021489cd89063f5dee62221 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 29 Nov 2025 09:07:18 -0600 Subject: [PATCH 240/301] [#.x] - changed sanitizing to strip_tags and adjusted tests --- .../Enums/Sanitizer/UserSanitizerEnum.php | 2 +- .../User/Command/UserCommandFactoryTest.php | 24 +++++++++---------- .../User/Handler/UserHandlerPostTest.php | 22 ++++++++--------- .../User/Service/UserServicePostTest.php | 22 ++++++++--------- .../User/Sanitizer/UserSanitizerTest.php | 18 +++++++------- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php b/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php index ee3eea5..498e66e 100644 --- a/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php +++ b/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php @@ -57,7 +57,7 @@ public function sanitizer(): string self::password, self::tokenId, self::tokenPassword => '', // Password will be distorted - default => Filter::FILTER_STRING + default => Filter::FILTER_STRIPTAGS }; } } diff --git a/tests/Unit/Domain/Application/User/Command/UserCommandFactoryTest.php b/tests/Unit/Domain/Application/User/Command/UserCommandFactoryTest.php index 310519b..f1ff36e 100644 --- a/tests/Unit/Domain/Application/User/Command/UserCommandFactoryTest.php +++ b/tests/Unit/Domain/Application/User/Command/UserCommandFactoryTest.php @@ -139,19 +139,19 @@ public function testInsert(): void $expected = $input['password']; $actual = $command->password; $this->assertSame($expected, $actual); - $expected = $filter->string($input['namePrefix']); + $expected = $filter->striptags($input['namePrefix']); $actual = $command->namePrefix; $this->assertSame($expected, $actual); - $expected = $filter->string($input['nameFirst']); + $expected = $filter->striptags($input['nameFirst']); $actual = $command->nameFirst; $this->assertSame($expected, $actual); - $expected = $filter->string($input['nameMiddle']); + $expected = $filter->striptags($input['nameMiddle']); $actual = $command->nameMiddle; $this->assertSame($expected, $actual); - $expected = $filter->string($input['nameLast']); + $expected = $filter->striptags($input['nameLast']); $actual = $command->nameLast; $this->assertSame($expected, $actual); - $expected = $filter->string($input['nameSuffix']); + $expected = $filter->striptags($input['nameSuffix']); $actual = $command->nameSuffix; $this->assertSame($expected, $actual); $expected = $input['issuer']; @@ -163,7 +163,7 @@ public function testInsert(): void $expected = $input['tokenId']; $actual = $command->tokenId; $this->assertSame($expected, $actual); - $expected = $filter->string($input['preferences']); + $expected = $filter->striptags($input['preferences']); $actual = $command->preferences; $this->assertSame($expected, $actual); $expected = $input['createdDate']; @@ -224,19 +224,19 @@ public function testUpdate(): void $expected = $input['password']; $actual = $command->password; $this->assertSame($expected, $actual); - $expected = $filter->string($input['namePrefix']); + $expected = $filter->striptags($input['namePrefix']); $actual = $command->namePrefix; $this->assertSame($expected, $actual); - $expected = $filter->string($input['nameFirst']); + $expected = $filter->striptags($input['nameFirst']); $actual = $command->nameFirst; $this->assertSame($expected, $actual); - $expected = $filter->string($input['nameMiddle']); + $expected = $filter->striptags($input['nameMiddle']); $actual = $command->nameMiddle; $this->assertSame($expected, $actual); - $expected = $filter->string($input['nameLast']); + $expected = $filter->striptags($input['nameLast']); $actual = $command->nameLast; $this->assertSame($expected, $actual); - $expected = $filter->string($input['nameSuffix']); + $expected = $filter->striptags($input['nameSuffix']); $actual = $command->nameSuffix; $this->assertSame($expected, $actual); $expected = $input['issuer']; @@ -248,7 +248,7 @@ public function testUpdate(): void $expected = $input['tokenId']; $actual = $command->tokenId; $this->assertSame($expected, $actual); - $expected = $filter->string($input['preferences']); + $expected = $filter->striptags($input['preferences']); $actual = $command->preferences; $this->assertSame($expected, $actual); $expected = $input['createdDate']; diff --git a/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php b/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php index cb9256e..0a4ac1a 100644 --- a/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php +++ b/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php @@ -23,7 +23,7 @@ use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Support\Registry; -use function htmlspecialchars; +use function strip_tags; final class UserHandlerPostTest extends AbstractUnitTestCase { @@ -244,23 +244,23 @@ public function testHandlerSuccess(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = htmlspecialchars($domainData['namePrefix']); + $expected = strip_tags($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameFirst']); + $expected = strip_tags($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameMiddle']); + $expected = strip_tags($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameLast']); + $expected = strip_tags($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameSuffix']); + $expected = strip_tags($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -356,23 +356,23 @@ public function testHandlerSuccessEmptyDates(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = htmlspecialchars($domainData['namePrefix']); + $expected = strip_tags($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameFirst']); + $expected = strip_tags($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameMiddle']); + $expected = strip_tags($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameLast']); + $expected = strip_tags($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameSuffix']); + $expected = strip_tags($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php b/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php index 2bfa6bc..bef59fb 100644 --- a/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php @@ -22,7 +22,7 @@ use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Support\Registry; -use function htmlspecialchars; +use function strip_tags; final class UserServicePostTest extends AbstractUnitTestCase { @@ -234,23 +234,23 @@ public function testServiceSuccess(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = htmlspecialchars($domainData['namePrefix']); + $expected = strip_tags($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameFirst']); + $expected = strip_tags($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameMiddle']); + $expected = strip_tags($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameLast']); + $expected = strip_tags($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameSuffix']); + $expected = strip_tags($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -343,23 +343,23 @@ public function testServiceSuccessEmptyDates(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = htmlspecialchars($domainData['namePrefix']); + $expected = strip_tags($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameFirst']); + $expected = strip_tags($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameMiddle']); + $expected = strip_tags($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameLast']); + $expected = strip_tags($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameSuffix']); + $expected = strip_tags($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php index 9e63b12..f9252cc 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php @@ -80,18 +80,18 @@ public function testObject(): void 'status' => 4, 'email' => 'John.Doenewsletter+spam@example.COM', 'password' => 'some ', - 'namePrefix' => 'some <value>', - 'nameFirst' => 'some <value>', - 'nameLast' => 'some <value>', - 'nameMiddle' => 'some <value>', - 'nameSuffix' => 'some <value>', - 'issuer' => 'some <value>', + 'namePrefix' => 'some ', + 'nameFirst' => 'some ', + 'nameLast' => 'some ', + 'nameMiddle' => 'some ', + 'nameSuffix' => 'some ', + 'issuer' => 'some ', 'tokenPassword' => 'some ', 'tokenId' => 'some ', - 'preferences' => 'some <value>', - 'createdDate' => 'some <value>', + 'preferences' => 'some ', + 'createdDate' => 'some ', 'createdUserId' => 123, - 'updatedDate' => 'some <value>', + 'updatedDate' => 'some ', 'updatedUserId' => 123, ]; $actual = $sanitizer->sanitize($userData); From 16924b61d548711ebafbf2c36cd7601fc35d1ada Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 29 Nov 2025 15:32:12 -0600 Subject: [PATCH 241/301] [#.x] - adjusting tests; phpcs; docblocks; minor corrections --- .../CommandBus/ContainerHandlerLocator.php | 9 ++ .../Validation/ValidatorInterface.php | 6 +- .../Enums/Container/AuthDefinitionsEnum.php | 2 +- .../Enums/Container/CommonDefinitionsEnum.php | 6 +- .../Enums/Container/UserDefinitionsEnum.php | 93 +++++++++---------- .../Infrastructure/Enums/Http/RoutesEnum.php | 6 +- .../Middleware/AbstractMiddleware.php | 2 +- .../Middleware/HealthMiddleware.php | 4 +- .../ValidateTokenPresenceMiddleware.php | 4 +- .../ValidateTokenStructureMiddleware.php | 4 +- .../ValidateTokenUserMiddleware.php | 3 +- .../User/Handler/UserHandlerPutTest.php | 20 ++-- .../User/Service/UserServiceDispatchTest.php | 6 ++ .../User/Service/UserServiceGetTest.php | 2 + .../User/Service/UserServicePostTest.php | 2 + .../User/Service/UserServicePutTest.php | 24 +++-- 16 files changed, 103 insertions(+), 90 deletions(-) diff --git a/src/Domain/Infrastructure/CommandBus/ContainerHandlerLocator.php b/src/Domain/Infrastructure/CommandBus/ContainerHandlerLocator.php index adf46ee..2ee5363 100644 --- a/src/Domain/Infrastructure/CommandBus/ContainerHandlerLocator.php +++ b/src/Domain/Infrastructure/CommandBus/ContainerHandlerLocator.php @@ -35,6 +35,15 @@ public function resolve(CommandInterface $command): HandlerInterface { $commandClass = get_class($command); + /** + * Phalcon\Api\Domain\Application\User\Command\UserGetCommand + * + * becomes + * + * Phalcon\Api\Domain\Application\User\Handler\UserGetHandler + * + * The location of files DOES matter + */ $handlerClass = str_replace( 'Command', 'Handler', diff --git a/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php b/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php index 3177cd9..59eff73 100644 --- a/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php +++ b/src/Domain/Infrastructure/DataSource/Validation/ValidatorInterface.php @@ -16,14 +16,14 @@ use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; /** - * Validator contract. Accepts a DTO or input and returns a Result. + * Validator contract. Accepts a Command and returns a Result. */ interface ValidatorInterface { /** - * Validate a DTO or input structure. + * Validate a Command object * - * @param CommandInterface $command DTO or array + * @param CommandInterface $command * * @return Result */ diff --git a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php index 820ccbe..2f09a74 100644 --- a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php @@ -146,7 +146,7 @@ public function isShared(): bool } /** - * @param string $className + * @param class-string $className * * @return TService */ diff --git a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php index c891497..70f95ea 100644 --- a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php @@ -138,10 +138,6 @@ public function definition(): array public function isShared(): bool { - return match ($this) { - self::JWTToken, - self::JWTTokenCache => false, - default => true, - }; + return true; } } diff --git a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php index c741396..2e19070 100644 --- a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php @@ -106,56 +106,14 @@ public function definition(): array ], ], ], - self::UserPostHandler => [ - 'className' => UserPostHandler::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => UserValidator::class, - ], - [ - 'type' => 'service', - 'name' => UserMapper::class, - ], - [ - 'type' => 'service', - 'name' => UserRepository::class, - ], - [ - 'type' => 'service', - 'name' => Registry::class, - ], - [ - 'type' => 'service', - 'name' => Security::class, - ], - ], - ], - self::UserPutHandler => [ - 'className' => UserPutHandler::class, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => UserValidatorUpdate::class, - ], - [ - 'type' => 'service', - 'name' => UserMapper::class, - ], - [ - 'type' => 'service', - 'name' => UserRepository::class, - ], - [ - 'type' => 'service', - 'name' => Registry::class, - ], - [ - 'type' => 'service', - 'name' => Security::class, - ], - ], - ], + self::UserPostHandler => $this->getServicePutPost( + UserPostHandler::class, + UserValidator::class + ), + self::UserPutHandler => $this->getServicePutPost( + UserPutHandler::class, + UserValidatorUpdate::class + ), self::UserMapper => [ 'className' => UserMapper::class, ], @@ -209,6 +167,41 @@ private function getService(string $className): array ]; } + /** + * @param class-string $className + * @param class-string $validatorName + * + * @return TService + */ + private function getServicePutPost(string $className, string $validatorName): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => $validatorName, + ], + [ + 'type' => 'service', + 'name' => UserMapper::class, + ], + [ + 'type' => 'service', + 'name' => UserRepository::class, + ], + [ + 'type' => 'service', + 'name' => Registry::class, + ], + [ + 'type' => 'service', + 'name' => Security::class, + ], + ], + ]; + } + /** * @param class-string $className * diff --git a/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php index 42c805c..48660c4 100644 --- a/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php +++ b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php @@ -44,9 +44,9 @@ enum RoutesEnum: int */ public const EVENT_BEFORE = 'before'; public const EVENT_FINISH = 'finish'; - public const GET = 'get'; - public const POST = 'post'; - public const PUT = 'put'; + public const GET = 'get'; + public const POST = 'post'; + public const PUT = 'put'; case authLoginPost = 11; case authLogoutPost = 12; diff --git a/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php index 22cdba4..fafaaeb 100644 --- a/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php @@ -89,7 +89,7 @@ protected function halt( /** @var ResponseInterface $response */ $response = $application->getSharedService(Container::RESPONSE); /** @var ResponderInterface $responder */ - $responder = $application->getService(JsonResponder::class); + $responder = $application->getSharedService(JsonResponder::class); $application->stop(); diff --git a/src/Domain/Infrastructure/Middleware/HealthMiddleware.php b/src/Domain/Infrastructure/Middleware/HealthMiddleware.php index 9093fdd..ed46a0d 100644 --- a/src/Domain/Infrastructure/Middleware/HealthMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/HealthMiddleware.php @@ -16,7 +16,7 @@ use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Events\Exception as EventsException; -use Phalcon\Http\Request; +use Phalcon\Http\RequestInterface; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; @@ -31,7 +31,7 @@ final class HealthMiddleware extends AbstractMiddleware */ public function call(Micro $application): bool { - /** @var Request $request */ + /** @var RequestInterface $request */ $request = $application->getSharedService(Container::REQUEST); if ( diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php index e4e361b..401e45e 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php @@ -17,7 +17,7 @@ use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Events\Exception as EventsException; -use Phalcon\Http\Request; +use Phalcon\Http\RequestInterface; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; @@ -32,7 +32,7 @@ final class ValidateTokenPresenceMiddleware extends AbstractMiddleware */ public function call(Micro $application): bool { - /** @var Request $request */ + /** @var RequestInterface $request */ $request = $application->getSharedService(Container::REQUEST); /** @var EnvManager $env */ $env = $application->getSharedService(EnvManager::class); diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php index fdb9db6..26a3db9 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php @@ -19,7 +19,7 @@ use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Events\Exception as EventsException; -use Phalcon\Http\Request; +use Phalcon\Http\RequestInterface; use Phalcon\Http\Response\Exception; use Phalcon\Mvc\Micro; use Phalcon\Support\Registry; @@ -35,7 +35,7 @@ final class ValidateTokenStructureMiddleware extends AbstractMiddleware */ public function call(Micro $application): bool { - /** @var Request $request */ + /** @var RequestInterface $request */ $request = $application->getSharedService(Container::REQUEST); /** @var EnvManager $env */ $env = $application->getSharedService(EnvManager::class); diff --git a/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php index e0899dd..7b22239 100644 --- a/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/ValidateTokenUserMiddleware.php @@ -14,6 +14,7 @@ namespace Phalcon\Api\Domain\Infrastructure\Middleware; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; @@ -36,7 +37,7 @@ final class ValidateTokenUserMiddleware extends AbstractMiddleware */ public function call(Micro $application): bool { - /** @var UserRepository $repository */ + /** @var UserRepositoryInterface $repository */ $repository = $application->getSharedService(UserRepository::class); /** @var Registry $registry */ $registry = $application->getSharedService(Registry::class); diff --git a/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php b/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php index 7be5e9b..015cf82 100644 --- a/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php +++ b/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php @@ -324,23 +324,23 @@ public function testHandlerSuccess(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = htmlspecialchars($domainData['namePrefix']); + $expected = strip_tags($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameFirst']); + $expected = strip_tags($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameMiddle']); + $expected = strip_tags($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameLast']); + $expected = strip_tags($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameSuffix']); + $expected = strip_tags($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -449,23 +449,23 @@ public function testHandlerSuccessEmptyDates(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = htmlspecialchars($domainData['namePrefix']); + $expected = strip_tags($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameFirst']); + $expected = strip_tags($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameMiddle']); + $expected = strip_tags($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameLast']); + $expected = strip_tags($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameSuffix']); + $expected = strip_tags($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); diff --git a/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php b/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php index 78798f3..6e615af 100644 --- a/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php @@ -24,6 +24,12 @@ use Phalcon\Cache\Cache; use PHPUnit\Framework\Attributes\BackupGlobals; +use function json_decode; +use function ob_get_clean; +use function ob_start; +use function restore_error_handler; +use function time; + #[BackupGlobals(true)] final class UserServiceDispatchTest extends AbstractUnitTestCase { diff --git a/tests/Unit/Domain/Application/User/Service/UserServiceGetTest.php b/tests/Unit/Domain/Application/User/Service/UserServiceGetTest.php index 95a5cde..7657e24 100644 --- a/tests/Unit/Domain/Application/User/Service/UserServiceGetTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServiceGetTest.php @@ -18,6 +18,8 @@ use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use function array_key_first; + final class UserServiceGetTest extends AbstractUnitTestCase { public function testServiceEmptyUserId(): void diff --git a/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php b/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php index bef59fb..d025312 100644 --- a/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php @@ -22,6 +22,8 @@ use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Support\Registry; +use function array_key_first; +use function str_starts_with; use function strip_tags; final class UserServicePostTest extends AbstractUnitTestCase diff --git a/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php b/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php index aea8afb..6efc574 100644 --- a/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php @@ -23,6 +23,10 @@ use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Support\Registry; +use function date; +use function str_starts_with; +use function strip_tags; + final class UserServicePutTest extends AbstractUnitTestCase { public function testServiceFailureNoIdReturned(): void @@ -313,23 +317,23 @@ public function testServiceSuccess(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = htmlspecialchars($domainData['namePrefix']); + $expected = strip_tags($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameFirst']); + $expected = strip_tags($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameMiddle']); + $expected = strip_tags($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameLast']); + $expected = strip_tags($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameSuffix']); + $expected = strip_tags($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); @@ -436,23 +440,23 @@ public function testServiceSuccessEmptyDates(): void $actual = str_starts_with($data['password'], '$argon2i$'); $this->assertTrue($actual); - $expected = htmlspecialchars($domainData['namePrefix']); + $expected = strip_tags($domainData['namePrefix']); $actual = $data['namePrefix']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameFirst']); + $expected = strip_tags($domainData['nameFirst']); $actual = $data['nameFirst']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameMiddle']); + $expected = strip_tags($domainData['nameMiddle']); $actual = $data['nameMiddle']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameLast']); + $expected = strip_tags($domainData['nameLast']); $actual = $data['nameLast']; $this->assertSame($expected, $actual); - $expected = htmlspecialchars($domainData['nameSuffix']); + $expected = strip_tags($domainData['nameSuffix']); $actual = $data['nameSuffix']; $this->assertSame($expected, $actual); From 0109698f7b36daeca3fdd9b4064488eaaa11688c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 29 Nov 2025 15:42:28 -0600 Subject: [PATCH 242/301] [#.x] - adding missing .env file --- tests/Fixtures/Domain/Infrastructure/Env/.env | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/Fixtures/Domain/Infrastructure/Env/.env diff --git a/tests/Fixtures/Domain/Infrastructure/Env/.env b/tests/Fixtures/Domain/Infrastructure/Env/.env new file mode 100644 index 0000000..b707f0d --- /dev/null +++ b/tests/Fixtures/Domain/Infrastructure/Env/.env @@ -0,0 +1,4 @@ +SAMPLE_STRING=sample_value +SAMPLE_INT=1 +SAMPLE_TRUE=true +SAMPLE_FALSE=false From 3f6f432b07842ec11f667324690fafe8cd4d6e3d Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 30 Nov 2025 16:46:00 -0600 Subject: [PATCH 243/301] [#.x] - added more tests --- .../Container/AuthDefinitionsEnumTest.php | 287 +++++++++++++ .../Container/CommonDefinitionsEnumTest.php | 235 +++++++++++ .../Container/UserDefinitionsEnumTest.php | 381 ++++++++++++++++++ .../Enums/Sanitizer/AuthSanitizerEnumTest.php | 52 +++ .../Enums/Sanitizer/UserSanitizerEnumTest.php | 172 ++++++++ .../Validator/AuthLoginValidatorEnumTest.php | 50 +++ .../Enums/Validator/UserInsertEnumTest.php | 80 ++++ .../Enums/Validator/UserUpdateEnumTest.php | 83 ++++ 8 files changed, 1340 insertions(+) create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Container/UserDefinitionsEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Validator/UserInsertEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Validator/UserUpdateEnumTest.php diff --git a/tests/Unit/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnumTest.php new file mode 100644 index 0000000..1d5688b --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnumTest.php @@ -0,0 +1,287 @@ + + * + * 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\Infrastructure\Enums\Container; + +use Phalcon\Api\Domain\Application\Auth\Command\AuthCommandFactory; +use Phalcon\Api\Domain\Application\Auth\Facade\AuthFacade; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthLoginPostHandler; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthLogoutPostHandler; +use Phalcon\Api\Domain\Application\Auth\Handler\AuthRefreshPostHandler; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthLogoutPostService; +use Phalcon\Api\Domain\Application\Auth\Service\AuthRefreshPostService; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthLoginValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthTokenValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\AuthDefinitionsEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation; + +final class AuthDefinitionsEnumTest extends AbstractUnitTestCase +{ + public function testDefinition(): void + { + $expected = [ + 'className' => AuthCommandFactory::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => AuthSanitizer::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthCommandFactory->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthLoginPostService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => AuthFacade::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthLoginPost->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthLogoutPostService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => AuthFacade::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthLogoutPost->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthRefreshPostService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => AuthFacade::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthRefreshPost->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CommandBus::class, + ], + [ + 'type' => 'service', + 'name' => AuthCommandFactory::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthFacade->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthLoginPostHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserRepository::class, + ], + [ + 'type' => 'service', + 'name' => TokenManager::class, + ], + [ + 'type' => 'service', + 'name' => Security::class, + ], + [ + 'type' => 'service', + 'name' => AuthLoginValidator::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthLoginHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthLogoutPostHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => TokenManager::class, + ], + [ + 'type' => 'service', + 'name' => AuthTokenValidator::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthLogoutHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthRefreshPostHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => TokenManager::class, + ], + [ + 'type' => 'service', + 'name' => AuthTokenValidator::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthRefreshHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthSanitizer::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::FILTER, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthSanitizer->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthLoginValidator::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Validation::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthLoginValidator->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthTokenValidator::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => TokenManager::class, + ], + [ + 'type' => 'service', + 'name' => UserRepository::class, + ], + [ + 'type' => 'service', + 'name' => Validation::class, + ], + ], + ]; + $actual = AuthDefinitionsEnum::AuthTokenValidator->definition(); + $this->assertSame($expected, $actual); + } + + public function testIsShared(): void + { + $actual = AuthDefinitionsEnum::AuthCommandFactory->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthLoginPost->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthLogoutPost->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthRefreshPost->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthFacade->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthLoginHandler->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthLogoutHandler->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthRefreshHandler->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthSanitizer->isShared(); + $this->assertTrue($actual); + + $actual = AuthDefinitionsEnum::AuthLoginValidator->isShared(); + $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthTokenValidator->isShared(); + $this->assertFalse($actual); + } + + public function testValues(): void + { + $expected = AuthCommandFactory::class; + $actual = AuthDefinitionsEnum::AuthCommandFactory->value; + $this->assertSame($expected, $actual); + + $expected = AuthLoginPostService::class; + $actual = AuthDefinitionsEnum::AuthLoginPost->value; + $this->assertSame($expected, $actual); + + $expected = AuthLogoutPostService::class; + $actual = AuthDefinitionsEnum::AuthLogoutPost->value; + $this->assertSame($expected, $actual); + + $expected = AuthRefreshPostService::class; + $actual = AuthDefinitionsEnum::AuthRefreshPost->value; + $this->assertSame($expected, $actual); + + $expected = AuthFacade::class; + $actual = AuthDefinitionsEnum::AuthFacade->value; + $this->assertSame($expected, $actual); + + $expected = AuthLoginPostHandler::class; + $actual = AuthDefinitionsEnum::AuthLoginHandler->value; + $this->assertSame($expected, $actual); + + $expected = AuthLogoutPostHandler::class; + $actual = AuthDefinitionsEnum::AuthLogoutHandler->value; + $this->assertSame($expected, $actual); + + $expected = AuthRefreshPostHandler::class; + $actual = AuthDefinitionsEnum::AuthRefreshHandler->value; + $this->assertSame($expected, $actual); + + $expected = AuthSanitizer::class; + $actual = AuthDefinitionsEnum::AuthSanitizer->value; + $this->assertSame($expected, $actual); + + $expected = AuthLoginValidator::class; + $actual = AuthDefinitionsEnum::AuthLoginValidator->value; + $this->assertSame($expected, $actual); + + $expected = AuthTokenValidator::class; + $actual = AuthDefinitionsEnum::AuthTokenValidator->value; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnumTest.php new file mode 100644 index 0000000..4bf4a50 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnumTest.php @@ -0,0 +1,235 @@ + + * + * 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\Infrastructure\Enums\Container; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; +use Phalcon\Api\Domain\Infrastructure\CommandBus\ContainerHandlerLocator; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; +use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\CommonDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Responder\JsonResponder; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Cache\Cache; +use Phalcon\Events\Manager as EventsManager; +use Phalcon\Http\Request; +use Phalcon\Http\Response; +use Phalcon\Mvc\Router; +use Phalcon\Support\Registry; + +final class CommonDefinitionsEnumTest extends AbstractUnitTestCase +{ + public function testDefinition(): void + { + $expected = [ + 'className' => CommandBus::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => ContainerHandlerLocator::class, + ], + ], + ]; + $actual = CommonDefinitionsEnum::CommandBus->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => EventsManager::class, + 'calls' => [ + [ + 'method' => 'enablePriorities', + 'arguments' => [ + [ + 'type' => 'parameter', + 'value' => true, + ], + ], + ], + ], + ]; + $actual = CommonDefinitionsEnum::EventsManager->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => EnvManager::class, + ]; + $actual = CommonDefinitionsEnum::EnvManager->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => JsonResponder::class, + ]; + $actual = CommonDefinitionsEnum::JsonResponder->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => JWTToken::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => EnvManager::class, + ], + ], + ]; + $actual = CommonDefinitionsEnum::JWTToken->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => TokenCache::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Cache::class, + ], + ], + ]; + $actual = CommonDefinitionsEnum::JWTTokenCache->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => TokenManager::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => TokenCache::class, + ], + [ + 'type' => 'service', + 'name' => EnvManager::class, + ], + [ + 'type' => 'service', + 'name' => JWTToken::class, + ], + ], + ]; + $actual = CommonDefinitionsEnum::JWTTokenManager->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => Registry::class, + ]; + $actual = CommonDefinitionsEnum::Registry->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => Request::class, + ]; + $actual = CommonDefinitionsEnum::Request->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => Response::class, + ]; + $actual = CommonDefinitionsEnum::Response->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => Router::class, + 'arguments' => [ + [ + 'type' => 'parameter', + 'value' => false, + ], + ], + ]; + $actual = CommonDefinitionsEnum::Router->definition(); + $this->assertSame($expected, $actual); + } + + public function testIsShared(): void + { + $actual = CommonDefinitionsEnum::CommandBus->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::EventsManager->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::EnvManager->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::JsonResponder->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::JWTToken->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::JWTTokenCache->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::JWTTokenManager->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::Registry->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::Request->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::Response->isShared(); + $this->assertTrue($actual); + + $actual = CommonDefinitionsEnum::Router->isShared(); + $this->assertTrue($actual); + } + + public function testValues(): void + { + $expected = CommandBus::class; + $actual = CommonDefinitionsEnum::CommandBus->value; + $this->assertSame($expected, $actual); + + $expected = Container::EVENTS_MANAGER; + $actual = CommonDefinitionsEnum::EventsManager->value; + $this->assertSame($expected, $actual); + + $expected = EnvManager::class; + $actual = CommonDefinitionsEnum::EnvManager->value; + $this->assertSame($expected, $actual); + + $expected = JsonResponder::class; + $actual = CommonDefinitionsEnum::JsonResponder->value; + $this->assertSame($expected, $actual); + + $expected = JWTToken::class; + $actual = CommonDefinitionsEnum::JWTToken->value; + $this->assertSame($expected, $actual); + + $expected = TokenCache::class; + $actual = CommonDefinitionsEnum::JWTTokenCache->value; + $this->assertSame($expected, $actual); + + $expected = TokenManager::class; + $actual = CommonDefinitionsEnum::JWTTokenManager->value; + $this->assertSame($expected, $actual); + + $expected = Registry::class; + $actual = CommonDefinitionsEnum::Registry->value; + $this->assertSame($expected, $actual); + + $expected = Container::REQUEST; + $actual = CommonDefinitionsEnum::Request->value; + $this->assertSame($expected, $actual); + + $expected = Container::RESPONSE; + $actual = CommonDefinitionsEnum::Response->value; + $this->assertSame($expected, $actual); + + $expected = Container::ROUTER; + $actual = CommonDefinitionsEnum::Router->value; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Container/UserDefinitionsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Container/UserDefinitionsEnumTest.php new file mode 100644 index 0000000..9d90df8 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Container/UserDefinitionsEnumTest.php @@ -0,0 +1,381 @@ + + * + * 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\Infrastructure\Enums\Container; + +use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; +use Phalcon\Api\Domain\Application\User\Facade\UserFacade; +use Phalcon\Api\Domain\Application\User\Handler\UserDeleteHandler; +use Phalcon\Api\Domain\Application\User\Handler\UserGetHandler; +use Phalcon\Api\Domain\Application\User\Handler\UserPostHandler; +use Phalcon\Api\Domain\Application\User\Handler\UserPutHandler; +use Phalcon\Api\Domain\Application\User\Service\UserDeleteService; +use Phalcon\Api\Domain\Application\User\Service\UserGetService; +use Phalcon\Api\Domain\Application\User\Service\UserPostService; +use Phalcon\Api\Domain\Application\User\Service\UserPutService; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserValidatorUpdate; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\UserDefinitionsEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\Filter\Validation; +use Phalcon\Support\Registry; + +final class UserDefinitionsEnumTest extends AbstractUnitTestCase +{ + public function testDefinition(): void + { + $expected = [ + 'className' => UserCommandFactory::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserSanitizer::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserCommandFactory->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserDeleteService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserFacade::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserDelete->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserGetService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserFacade::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserGet->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserPostService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserFacade::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserPost->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserPutService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserFacade::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserPut->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CommandBus::class, + ], + [ + 'type' => 'service', + 'name' => UserCommandFactory::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserFacade->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserDeleteHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserRepository::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserDeleteHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserGetHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserRepository::class, + ], + [ + 'type' => 'service', + 'name' => Container::EVENTS_MANAGER, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserGetHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserPostHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserValidator::class, + ], + [ + 'type' => 'service', + 'name' => UserMapper::class, + ], + [ + 'type' => 'service', + 'name' => UserRepository::class, + ], + [ + 'type' => 'service', + 'name' => Container::EVENTS_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Registry::class, + ], + [ + 'type' => 'service', + 'name' => Security::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserPostHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserPutHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserValidatorUpdate::class, + ], + [ + 'type' => 'service', + 'name' => UserMapper::class, + ], + [ + 'type' => 'service', + 'name' => UserRepository::class, + ], + [ + 'type' => 'service', + 'name' => Container::EVENTS_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Registry::class, + ], + [ + 'type' => 'service', + 'name' => Security::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserPutHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserMapper::class, + ]; + $actual = UserDefinitionsEnum::UserMapper->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserRepository::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Connection::class, + ], + [ + 'type' => 'service', + 'name' => UserMapper::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserRepository->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserSanitizer::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::FILTER, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserSanitizer->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserValidator::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Validation::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserValidator->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => UserValidatorUpdate::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Validation::class, + ], + ], + ]; + $actual = UserDefinitionsEnum::UserValidatorUpdate->definition(); + $this->assertSame($expected, $actual); + } + + public function testIsShared(): void + { + $actual = UserDefinitionsEnum::UserCommandFactory->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserDelete->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserGet->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserPost->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserPut->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserFacade->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserDeleteHandler->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserGetHandler->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserPostHandler->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserPutHandler->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserMapper->isShared(); + $this->assertTrue($actual); + + $actual = UserDefinitionsEnum::UserRepository->isShared(); + $this->assertTrue($actual); + + $actual = UserDefinitionsEnum::UserSanitizer->isShared(); + $this->assertTrue($actual); + + $actual = UserDefinitionsEnum::UserValidator->isShared(); + $this->assertFalse($actual); + + $actual = UserDefinitionsEnum::UserValidatorUpdate->isShared(); + $this->assertFalse($actual); + } + + public function testValue(): void + { + $expected = UserCommandFactory::class; + $actual = UserDefinitionsEnum::UserCommandFactory->value; + $this->assertSame($expected, $actual); + + $expected = UserDeleteService::class; + $actual = UserDefinitionsEnum::UserDelete->value; + $this->assertSame($expected, $actual); + + $expected = UserGetService::class; + $actual = UserDefinitionsEnum::UserGet->value; + $this->assertSame($expected, $actual); + + $expected = UserPostService::class; + $actual = UserDefinitionsEnum::UserPost->value; + $this->assertSame($expected, $actual); + + $expected = UserPutService::class; + $actual = UserDefinitionsEnum::UserPut->value; + $this->assertSame($expected, $actual); + + $expected = UserFacade::class; + $actual = UserDefinitionsEnum::UserFacade->value; + $this->assertSame($expected, $actual); + + $expected = UserDeleteHandler::class; + $actual = UserDefinitionsEnum::UserDeleteHandler->value; + $this->assertSame($expected, $actual); + + $expected = UserGetHandler::class; + $actual = UserDefinitionsEnum::UserGetHandler->value; + $this->assertSame($expected, $actual); + + $expected = UserPostHandler::class; + $actual = UserDefinitionsEnum::UserPostHandler->value; + $this->assertSame($expected, $actual); + + $expected = UserPutHandler::class; + $actual = UserDefinitionsEnum::UserPutHandler->value; + $this->assertSame($expected, $actual); + + $expected = UserMapper::class; + $actual = UserDefinitionsEnum::UserMapper->value; + $this->assertSame($expected, $actual); + + $expected = UserRepository::class; + $actual = UserDefinitionsEnum::UserRepository->value; + $this->assertSame($expected, $actual); + + $expected = UserSanitizer::class; + $actual = UserDefinitionsEnum::UserSanitizer->value; + $this->assertSame($expected, $actual); + + $expected = UserValidator::class; + $actual = UserDefinitionsEnum::UserValidator->value; + $this->assertSame($expected, $actual); + + $expected = UserValidatorUpdate::class; + $actual = UserDefinitionsEnum::UserValidatorUpdate->value; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnumTest.php new file mode 100644 index 0000000..9da9dd7 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnumTest.php @@ -0,0 +1,52 @@ + + * + * 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\Infrastructure\Enums\Sanitizer; + +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\AuthSanitizerEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class AuthSanitizerEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 3; + $actual = AuthSanitizerEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = Filter::FILTER_EMAIL; + $actual = AuthSanitizerEnum::email->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = AuthSanitizerEnum::email->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = AuthSanitizerEnum::password->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = AuthSanitizerEnum::password->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = AuthSanitizerEnum::token->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = AuthSanitizerEnum::token->default(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnumTest.php new file mode 100644 index 0000000..c034e5d --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnumTest.php @@ -0,0 +1,172 @@ + + * + * 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\Infrastructure\Enums\Sanitizer; + +use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\UserSanitizerEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class UserSanitizerEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 17; + $actual = UserSanitizerEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = UserSanitizerEnum::id->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = UserSanitizerEnum::id->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_EMAIL; + $actual = UserSanitizerEnum::email->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::email->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = UserSanitizerEnum::status->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = UserSanitizerEnum::status->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_EMAIL; + $actual = UserSanitizerEnum::email->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::email->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = UserSanitizerEnum::password->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::password->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::namePrefix->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::namePrefix->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::nameFirst->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::nameFirst->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::nameLast->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::nameLast->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::nameMiddle->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::nameMiddle->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::nameSuffix->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::nameSuffix->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = UserSanitizerEnum::issuer->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::issuer->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = UserSanitizerEnum::tokenPassword->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::tokenPassword->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = UserSanitizerEnum::tokenId->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::tokenId->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::preferences->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::preferences->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = UserSanitizerEnum::createdUserId->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = UserSanitizerEnum::createdUserId->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::createdDate->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::createdDate->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = UserSanitizerEnum::updatedUserId->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = UserSanitizerEnum::updatedUserId->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::updatedDate->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::updatedDate->default(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnumTest.php new file mode 100644 index 0000000..c1994cc --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnumTest.php @@ -0,0 +1,50 @@ + + * + * 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\Infrastructure\Enums\Validator; + +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\AuthLoginValidatorEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class AuthLoginValidatorEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 2; + $actual = AuthLoginValidatorEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + Email::class, + ]; + $actual = AuthLoginValidatorEnum::email->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = AuthLoginValidatorEnum::email->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = AuthLoginValidatorEnum::password->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = AuthLoginValidatorEnum::password->allowEmpty(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Validator/UserInsertEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Validator/UserInsertEnumTest.php new file mode 100644 index 0000000..cee7514 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Validator/UserInsertEnumTest.php @@ -0,0 +1,80 @@ + + * + * 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\Infrastructure\Enums\Validator; + +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\UserInsertEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class UserInsertEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 5; + $actual = UserInsertEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + Email::class, + ]; + $actual = UserInsertEnum::email->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::email->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = UserInsertEnum::password->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::password->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = UserInsertEnum::issuer->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::issuer->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = UserInsertEnum::tokenPassword->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::tokenPassword->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = UserInsertEnum::tokenId->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::tokenId->allowEmpty(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Validator/UserUpdateEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Validator/UserUpdateEnumTest.php new file mode 100644 index 0000000..dcba9a7 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Validator/UserUpdateEnumTest.php @@ -0,0 +1,83 @@ + + * + * 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\Infrastructure\Enums\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\UserUpdateEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class UserUpdateEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 6; + $actual = UserUpdateEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + AbsInt::class, + ]; + $actual = UserUpdateEnum::id->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserUpdateEnum::id->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + Email::class, + ]; + $actual = UserUpdateEnum::email->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::email->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = []; + $actual = UserUpdateEnum::password->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::password->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = []; + $actual = UserUpdateEnum::issuer->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::issuer->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = []; + $actual = UserUpdateEnum::tokenPassword->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::tokenPassword->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = []; + $actual = UserUpdateEnum::tokenId->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::tokenId->allowEmpty(); + $this->assertSame($expected, $actual); + } +} From d67e9b589f5e0d96ea66acc3cf259c4ef2f5c6fb Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 30 Nov 2025 16:46:24 -0600 Subject: [PATCH 244/301] [#.x] - fixing issuer filter --- src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php | 1 + .../DataSource/User/Sanitizer/UserSanitizerTest.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php b/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php index 498e66e..fbb2f3c 100644 --- a/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php +++ b/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php @@ -56,6 +56,7 @@ public function sanitizer(): string self::email => Filter::FILTER_EMAIL, self::password, self::tokenId, + self::issuer, self::tokenPassword => '', // Password will be distorted default => Filter::FILTER_STRIPTAGS }; diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php index f9252cc..170aa25 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerTest.php @@ -85,7 +85,7 @@ public function testObject(): void 'nameLast' => 'some ', 'nameMiddle' => 'some ', 'nameSuffix' => 'some ', - 'issuer' => 'some ', + 'issuer' => 'some ', 'tokenPassword' => 'some ', 'tokenId' => 'some ', 'preferences' => 'some ', From 973b48123cc8026854710b0f6fd12380c8aa5f00 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 30 Nov 2025 16:47:10 -0600 Subject: [PATCH 245/301] [#.x] - adjusted test --- .../Validator/AuthTokenValidatorEnumTest.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnumTest.php diff --git a/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnumTest.php new file mode 100644 index 0000000..7dd00db --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnumTest.php @@ -0,0 +1,38 @@ + + * + * 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\Infrastructure\Enums\Validator; + +use Phalcon\Api\Domain\Infrastructure\Enums\Validator\AuthTokenValidatorEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class AuthTokenValidatorEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 1; + $actual = AuthTokenValidatorEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = AuthTokenValidatorEnum::token->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = AuthTokenValidatorEnum::token->allowEmpty(); + $this->assertSame($expected, $actual); + } +} From 9d4e86898c20e1462b031acb194140fcd773b83b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sun, 30 Nov 2025 16:48:00 -0600 Subject: [PATCH 246/301] [#.x] - adjusting shared services --- .../Enums/Container/AuthDefinitionsEnum.php | 5 ++++- .../Enums/Container/UserDefinitionsEnum.php | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php index 2f09a74..2172ad8 100644 --- a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php @@ -142,7 +142,10 @@ public function definition(): array public function isShared(): bool { - return false; + return match ($this) { + self::AuthSanitizer => true, + default => false, + }; } /** diff --git a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php index 2e19070..799e4aa 100644 --- a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php @@ -146,7 +146,12 @@ public function definition(): array public function isShared(): bool { - return false; + return match ($this) { + self::UserMapper, + self::UserRepository, + self::UserSanitizer => true, + default => false, + }; } /** @@ -190,6 +195,10 @@ private function getServicePutPost(string $className, string $validatorName): ar 'type' => 'service', 'name' => UserRepository::class, ], + [ + 'type' => 'service', + 'name' => Container::EVENTS_MANAGER, + ], [ 'type' => 'service', 'name' => Registry::class, From f41ddc069185cdb3865a246d7f2df4694a3e5ecf Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Wed, 24 Dec 2025 17:37:09 -0600 Subject: [PATCH 247/301] [#.x] - support for 8.5 --- .github/workflows/main.yml | 2 ++ docker-compose.yml | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 300d307..4eae2c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,6 +41,7 @@ jobs: - '8.2' - '8.3' - '8.4' + - '8.5' steps: - uses: actions/checkout@v4 @@ -81,6 +82,7 @@ jobs: - '8.2' - '8.3' - '8.4' + - '8.5' services: mariadb: diff --git a/docker-compose.yml b/docker-compose.yml index b96a807..e9c9e40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,7 +71,34 @@ services: networks: - app-network environment: - - PHP_IDE_CONFIG=serverName=rest-api-app-83 + - PHP_IDE_CONFIG=serverName=rest-api-app-84 + - 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.5: + build: + dockerfile: ./resources/docker/Dockerfile + args: + PHP_VERSION: 8.5 + hostname: rest-api-app-85 + container_name: "${PROJECT_NAME}-api-8.5" + tty: true + working_dir: /app + volumes: + - ./:/app + depends_on: + - app-db + - app-cache + networks: + - app-network + environment: + - PHP_IDE_CONFIG=serverName=rest-api-app-85 - APP_ENV=development - APP_ENV_ADAPTER=dotenv healthcheck: From ed450404a22a5a749b5c58a7dd8b04f618ceb6a6 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:46:39 -0600 Subject: [PATCH 248/301] [#.x] - moving files to new locations --- .../Auth}/Sanitizer/AuthSanitizerEnum.php | 5 +- .../Validator/AuthLoginValidatorEnum.php | 3 +- .../Interface/SanitizerEnumInterface.php} | 4 +- .../Interface}/ValidatorEnumInterface.php | 2 +- .../Mapper/UserMapperInterface.php} | 11 +- .../User}/Sanitizer/UserSanitizerEnum.php | 5 +- .../User/Validator/UserValidatorTrait.php | 45 ----- .../Enums/Validator/UserInsertEnum.php | 42 ----- .../Enums/Validator/UserUpdateEnum.php | 50 ----- .../Enums/Sanitizer/AuthSanitizerEnumTest.php | 52 ------ .../Enums/Sanitizer/UserSanitizerEnumTest.php | 172 ------------------ .../Validator/AuthLoginValidatorEnumTest.php | 50 ----- .../Validator/AuthTokenValidatorEnumTest.php | 38 ---- .../Enums/Validator/UserInsertEnumTest.php | 80 -------- .../Enums/Validator/UserUpdateEnumTest.php | 83 --------- 15 files changed, 13 insertions(+), 629 deletions(-) rename src/Domain/Infrastructure/{Enums => DataSource/Auth}/Sanitizer/AuthSanitizerEnum.php (73%) rename src/Domain/Infrastructure/{Enums => DataSource/Auth}/Validator/AuthLoginValidatorEnum.php (83%) rename src/Domain/Infrastructure/{Enums/Sanitizer/SanitizersEnumInterface.php => DataSource/Interface/SanitizerEnumInterface.php} (75%) rename src/Domain/Infrastructure/{Enums/Validator => DataSource/Interface}/ValidatorEnumInterface.php (88%) rename src/Domain/Infrastructure/DataSource/{Interface/MapperInterface.php => User/Mapper/UserMapperInterface.php} (81%) rename src/Domain/Infrastructure/{Enums => DataSource/User}/Sanitizer/UserSanitizerEnum.php (86%) delete mode 100644 src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTrait.php delete mode 100644 src/Domain/Infrastructure/Enums/Validator/UserInsertEnum.php delete mode 100644 src/Domain/Infrastructure/Enums/Validator/UserUpdateEnum.php delete mode 100644 tests/Unit/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnumTest.php delete mode 100644 tests/Unit/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnumTest.php delete mode 100644 tests/Unit/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnumTest.php delete mode 100644 tests/Unit/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnumTest.php delete mode 100644 tests/Unit/Domain/Infrastructure/Enums/Validator/UserInsertEnumTest.php delete mode 100644 tests/Unit/Domain/Infrastructure/Enums/Validator/UserUpdateEnumTest.php diff --git a/src/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnum.php b/src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerEnum.php similarity index 73% rename from src/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnum.php rename to src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerEnum.php index 5baf791..93dbb72 100644 --- a/src/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnum.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerEnum.php @@ -11,11 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerEnumInterface; use Phalcon\Filter\Filter; -enum AuthSanitizerEnum implements SanitizersEnumInterface +enum AuthSanitizerEnum implements SanitizerEnumInterface { case email; case password; diff --git a/src/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnum.php b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorEnum.php similarity index 83% rename from src/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnum.php rename to src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorEnum.php index 3043b4c..44f7e39 100644 --- a/src/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnum.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorEnum.php @@ -11,8 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Validator; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValidatorEnumInterface; use Phalcon\Filter\Validation\Validator\Email; use Phalcon\Filter\Validation\Validator\PresenceOf; diff --git a/src/Domain/Infrastructure/Enums/Sanitizer/SanitizersEnumInterface.php b/src/Domain/Infrastructure/DataSource/Interface/SanitizerEnumInterface.php similarity index 75% rename from src/Domain/Infrastructure/Enums/Sanitizer/SanitizersEnumInterface.php rename to src/Domain/Infrastructure/DataSource/Interface/SanitizerEnumInterface.php index bdc84f8..894960d 100644 --- a/src/Domain/Infrastructure/Enums/Sanitizer/SanitizersEnumInterface.php +++ b/src/Domain/Infrastructure/DataSource/Interface/SanitizerEnumInterface.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Interface; use UnitEnum; -interface SanitizersEnumInterface extends UnitEnum +interface SanitizerEnumInterface extends UnitEnum { public function default(): mixed; diff --git a/src/Domain/Infrastructure/Enums/Validator/ValidatorEnumInterface.php b/src/Domain/Infrastructure/DataSource/Interface/ValidatorEnumInterface.php similarity index 88% rename from src/Domain/Infrastructure/Enums/Validator/ValidatorEnumInterface.php rename to src/Domain/Infrastructure/DataSource/Interface/ValidatorEnumInterface.php index 50ccb95..73c4c3f 100644 --- a/src/Domain/Infrastructure/Enums/Validator/ValidatorEnumInterface.php +++ b/src/Domain/Infrastructure/DataSource/Interface/ValidatorEnumInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Validator; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Interface; use UnitEnum; diff --git a/src/Domain/Infrastructure/DataSource/Interface/MapperInterface.php b/src/Domain/Infrastructure/DataSource/User/Mapper/UserMapperInterface.php similarity index 81% rename from src/Domain/Infrastructure/DataSource/Interface/MapperInterface.php rename to src/Domain/Infrastructure/DataSource/User/Mapper/UserMapperInterface.php index edf829a..5a2eaa4 100644 --- a/src/Domain/Infrastructure/DataSource/Interface/MapperInterface.php +++ b/src/Domain/Infrastructure/DataSource/User/Mapper/UserMapperInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\DataSource\Interface; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper; use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; @@ -24,7 +24,7 @@ * @phpstan-import-type TUserDbRecord from UserTypes * @phpstan-import-type TUserDomainToDbRecord from UserTypes */ -interface MapperInterface +interface UserMapperInterface { /** * Map Domain User -> DB row (usr_*) @@ -39,11 +39,4 @@ public function db(CommandInterface $user): array; * @param TUserDbRecord|array{} $row */ public function domain(array $row): User; - - /** - * Map input row -> Domain User - * - * @param TUser $row - */ - public function input(array $row): User; } diff --git a/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php b/src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerEnum.php similarity index 86% rename from src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php rename to src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerEnum.php index fbb2f3c..c561838 100644 --- a/src/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnum.php +++ b/src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerEnum.php @@ -11,11 +11,12 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerEnumInterface; use Phalcon\Filter\Filter; -enum UserSanitizerEnum implements SanitizersEnumInterface +enum UserSanitizerEnum implements SanitizerEnumInterface { case id; case status; diff --git a/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTrait.php b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTrait.php deleted file mode 100644 index c2fc0b9..0000000 --- a/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorTrait.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * 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\Infrastructure\DataSource\User\Validator; - -use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; - -trait UserValidatorTrait -{ - /** - * Validate a AuthInput and return an array of errors. - * Empty array means valid. - * - * @param CommandInterface $command - * - * @return Result - */ - public function validate(CommandInterface $command): Result - { - $errors = $this->runValidations($command); - if (true !== empty($errors)) { - return Result::error($errors); - } - - return Result::success(); - } - - /** - * @param CommandInterface $command - * - * @return array - */ - abstract protected function runValidations(CommandInterface $command): array; -} diff --git a/src/Domain/Infrastructure/Enums/Validator/UserInsertEnum.php b/src/Domain/Infrastructure/Enums/Validator/UserInsertEnum.php deleted file mode 100644 index f91b54c..0000000 --- a/src/Domain/Infrastructure/Enums/Validator/UserInsertEnum.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * 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\Infrastructure\Enums\Validator; - -use Phalcon\Filter\Validation\Validator\Email; -use Phalcon\Filter\Validation\Validator\PresenceOf; - -enum UserInsertEnum implements ValidatorEnumInterface -{ - case email; - case password; - case issuer; - case tokenPassword; - case tokenId; - - public function allowEmpty(): bool - { - return false; - } - - public function validators(): array - { - return match ($this) { - self::email => [ - PresenceOf::class, - Email::class, - ], - default => [PresenceOf::class], - }; - } -} diff --git a/src/Domain/Infrastructure/Enums/Validator/UserUpdateEnum.php b/src/Domain/Infrastructure/Enums/Validator/UserUpdateEnum.php deleted file mode 100644 index 2ce9983..0000000 --- a/src/Domain/Infrastructure/Enums/Validator/UserUpdateEnum.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * 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\Infrastructure\Enums\Validator; - -use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; -use Phalcon\Filter\Validation\Validator\Email; -use Phalcon\Filter\Validation\Validator\PresenceOf; - -enum UserUpdateEnum implements ValidatorEnumInterface -{ - case id; - case email; - case password; - case issuer; - case tokenPassword; - case tokenId; - - public function allowEmpty(): bool - { - return match ($this) { - self::id => false, - default => true, - }; - } - - public function validators(): array - { - return match ($this) { - self::id => [ - PresenceOf::class, - AbsInt::class, - ], - self::email => [ - Email::class, - ], - default => [], - }; - } -} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnumTest.php deleted file mode 100644 index 9da9dd7..0000000 --- a/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/AuthSanitizerEnumTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * 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\Infrastructure\Enums\Sanitizer; - -use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\AuthSanitizerEnum; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Filter; - -final class AuthSanitizerEnumTest extends AbstractUnitTestCase -{ - public function testEnum(): void - { - $expected = 3; - $actual = AuthSanitizerEnum::cases(); - $this->assertCount($expected, $actual); - - $expected = Filter::FILTER_EMAIL; - $actual = AuthSanitizerEnum::email->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = AuthSanitizerEnum::email->default(); - $this->assertSame($expected, $actual); - - $expected = ''; - $actual = AuthSanitizerEnum::password->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = AuthSanitizerEnum::password->default(); - $this->assertSame($expected, $actual); - - $expected = ''; - $actual = AuthSanitizerEnum::token->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = AuthSanitizerEnum::token->default(); - $this->assertSame($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnumTest.php deleted file mode 100644 index c034e5d..0000000 --- a/tests/Unit/Domain/Infrastructure/Enums/Sanitizer/UserSanitizerEnumTest.php +++ /dev/null @@ -1,172 +0,0 @@ - - * - * 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\Infrastructure\Enums\Sanitizer; - -use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\UserSanitizerEnum; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Filter; - -final class UserSanitizerEnumTest extends AbstractUnitTestCase -{ - public function testEnum(): void - { - $expected = 17; - $actual = UserSanitizerEnum::cases(); - $this->assertCount($expected, $actual); - - $expected = Filter::FILTER_ABSINT; - $actual = UserSanitizerEnum::id->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = 0; - $actual = UserSanitizerEnum::id->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_EMAIL; - $actual = UserSanitizerEnum::email->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::email->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_ABSINT; - $actual = UserSanitizerEnum::status->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = 0; - $actual = UserSanitizerEnum::status->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_EMAIL; - $actual = UserSanitizerEnum::email->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::email->default(); - $this->assertSame($expected, $actual); - - $expected = ''; - $actual = UserSanitizerEnum::password->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::password->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_STRIPTAGS; - $actual = UserSanitizerEnum::namePrefix->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::namePrefix->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_STRIPTAGS; - $actual = UserSanitizerEnum::nameFirst->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::nameFirst->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_STRIPTAGS; - $actual = UserSanitizerEnum::nameLast->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::nameLast->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_STRIPTAGS; - $actual = UserSanitizerEnum::nameMiddle->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::nameMiddle->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_STRIPTAGS; - $actual = UserSanitizerEnum::nameSuffix->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::nameSuffix->default(); - $this->assertSame($expected, $actual); - - $expected = ''; - $actual = UserSanitizerEnum::issuer->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::issuer->default(); - $this->assertSame($expected, $actual); - - $expected = ''; - $actual = UserSanitizerEnum::tokenPassword->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::tokenPassword->default(); - $this->assertSame($expected, $actual); - - $expected = ''; - $actual = UserSanitizerEnum::tokenId->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::tokenId->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_STRIPTAGS; - $actual = UserSanitizerEnum::preferences->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::preferences->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_ABSINT; - $actual = UserSanitizerEnum::createdUserId->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = 0; - $actual = UserSanitizerEnum::createdUserId->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_STRIPTAGS; - $actual = UserSanitizerEnum::createdDate->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::createdDate->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_ABSINT; - $actual = UserSanitizerEnum::updatedUserId->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = 0; - $actual = UserSanitizerEnum::updatedUserId->default(); - $this->assertSame($expected, $actual); - - $expected = Filter::FILTER_STRIPTAGS; - $actual = UserSanitizerEnum::updatedDate->sanitizer(); - $this->assertSame($expected, $actual); - - $expected = null; - $actual = UserSanitizerEnum::updatedDate->default(); - $this->assertSame($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnumTest.php deleted file mode 100644 index c1994cc..0000000 --- a/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthLoginValidatorEnumTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * 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\Infrastructure\Enums\Validator; - -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\AuthLoginValidatorEnum; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Validation\Validator\Email; -use Phalcon\Filter\Validation\Validator\PresenceOf; - -final class AuthLoginValidatorEnumTest extends AbstractUnitTestCase -{ - public function testEnum(): void - { - $expected = 2; - $actual = AuthLoginValidatorEnum::cases(); - $this->assertCount($expected, $actual); - - $expected = [ - PresenceOf::class, - Email::class, - ]; - $actual = AuthLoginValidatorEnum::email->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = AuthLoginValidatorEnum::email->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = [ - PresenceOf::class, - ]; - $actual = AuthLoginValidatorEnum::password->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = AuthLoginValidatorEnum::password->allowEmpty(); - $this->assertSame($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnumTest.php deleted file mode 100644 index 7dd00db..0000000 --- a/tests/Unit/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnumTest.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * 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\Infrastructure\Enums\Validator; - -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\AuthTokenValidatorEnum; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Validation\Validator\PresenceOf; - -final class AuthTokenValidatorEnumTest extends AbstractUnitTestCase -{ - public function testEnum(): void - { - $expected = 1; - $actual = AuthTokenValidatorEnum::cases(); - $this->assertCount($expected, $actual); - - $expected = [ - PresenceOf::class, - ]; - $actual = AuthTokenValidatorEnum::token->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = AuthTokenValidatorEnum::token->allowEmpty(); - $this->assertSame($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Validator/UserInsertEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Validator/UserInsertEnumTest.php deleted file mode 100644 index cee7514..0000000 --- a/tests/Unit/Domain/Infrastructure/Enums/Validator/UserInsertEnumTest.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * 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\Infrastructure\Enums\Validator; - -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\UserInsertEnum; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Validation\Validator\Email; -use Phalcon\Filter\Validation\Validator\PresenceOf; - -final class UserInsertEnumTest extends AbstractUnitTestCase -{ - public function testEnum(): void - { - $expected = 5; - $actual = UserInsertEnum::cases(); - $this->assertCount($expected, $actual); - - $expected = [ - PresenceOf::class, - Email::class, - ]; - $actual = UserInsertEnum::email->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = UserInsertEnum::email->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = [ - PresenceOf::class, - ]; - $actual = UserInsertEnum::password->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = UserInsertEnum::password->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = [ - PresenceOf::class, - ]; - $actual = UserInsertEnum::issuer->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = UserInsertEnum::issuer->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = [ - PresenceOf::class, - ]; - $actual = UserInsertEnum::tokenPassword->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = UserInsertEnum::tokenPassword->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = [ - PresenceOf::class, - ]; - $actual = UserInsertEnum::tokenId->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = UserInsertEnum::tokenId->allowEmpty(); - $this->assertSame($expected, $actual); - } -} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Validator/UserUpdateEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Validator/UserUpdateEnumTest.php deleted file mode 100644 index dcba9a7..0000000 --- a/tests/Unit/Domain/Infrastructure/Enums/Validator/UserUpdateEnumTest.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * 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\Infrastructure\Enums\Validator; - -use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\UserUpdateEnum; -use Phalcon\Api\Tests\AbstractUnitTestCase; -use Phalcon\Filter\Validation\Validator\Email; -use Phalcon\Filter\Validation\Validator\PresenceOf; - -final class UserUpdateEnumTest extends AbstractUnitTestCase -{ - public function testEnum(): void - { - $expected = 6; - $actual = UserUpdateEnum::cases(); - $this->assertCount($expected, $actual); - - $expected = [ - PresenceOf::class, - AbsInt::class, - ]; - $actual = UserUpdateEnum::id->validators(); - $this->assertSame($expected, $actual); - - $expected = false; - $actual = UserUpdateEnum::id->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = [ - Email::class, - ]; - $actual = UserUpdateEnum::email->validators(); - $this->assertSame($expected, $actual); - - $expected = true; - $actual = UserUpdateEnum::email->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = []; - $actual = UserUpdateEnum::password->validators(); - $this->assertSame($expected, $actual); - - $expected = true; - $actual = UserUpdateEnum::password->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = []; - $actual = UserUpdateEnum::issuer->validators(); - $this->assertSame($expected, $actual); - - $expected = true; - $actual = UserUpdateEnum::issuer->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = []; - $actual = UserUpdateEnum::tokenPassword->validators(); - $this->assertSame($expected, $actual); - - $expected = true; - $actual = UserUpdateEnum::tokenPassword->allowEmpty(); - $this->assertSame($expected, $actual); - - $expected = []; - $actual = UserUpdateEnum::tokenId->validators(); - $this->assertSame($expected, $actual); - - $expected = true; - $actual = UserUpdateEnum::tokenId->allowEmpty(); - $this->assertSame($expected, $actual); - } -} From b31e81a6422b13730e8e0f027344c9f691212d0b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:47:45 -0600 Subject: [PATCH 249/301] [#.x] - simplifying validators; adjusting test; moving enum to its new location --- .../Validator/AuthTokenValidatorEnum.php | 3 ++- .../User/Validator/UserValidator.php | 3 --- .../User/Validator/UserValidatorUpdate.php | 3 --- .../Validation/AbstractValidator.php | 20 ++++++++++++++++++- .../Validator/UserValidatorUpdateTest.php | 3 --- 5 files changed, 21 insertions(+), 11 deletions(-) rename src/Domain/Infrastructure/{Enums => DataSource/Auth}/Validator/AuthTokenValidatorEnum.php (78%) diff --git a/src/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnum.php b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorEnum.php similarity index 78% rename from src/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnum.php rename to src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorEnum.php index 0ba003b..bab53fb 100644 --- a/src/Domain/Infrastructure/Enums/Validator/AuthTokenValidatorEnum.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorEnum.php @@ -11,8 +11,9 @@ declare(strict_types=1); -namespace Phalcon\Api\Domain\Infrastructure\Enums\Validator; +namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValidatorEnumInterface; use Phalcon\Filter\Validation\Validator\PresenceOf; enum AuthTokenValidatorEnum implements ValidatorEnumInterface diff --git a/src/Domain/Infrastructure/DataSource/User/Validator/UserValidator.php b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidator.php index a22c578..52d9a00 100644 --- a/src/Domain/Infrastructure/DataSource/User/Validator/UserValidator.php +++ b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidator.php @@ -14,11 +14,8 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\UserInsertEnum; final class UserValidator extends AbstractValidator { - use UserValidatorTrait; - protected string $fields = UserInsertEnum::class; } diff --git a/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdate.php b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdate.php index 4bded48..34f7d03 100644 --- a/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdate.php +++ b/src/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdate.php @@ -14,11 +14,8 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\UserUpdateEnum; final class UserValidatorUpdate extends AbstractValidator { - use UserValidatorTrait; - protected string $fields = UserUpdateEnum::class; } diff --git a/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php index bc15ea5..bc6bbd6 100644 --- a/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php +++ b/src/Domain/Infrastructure/DataSource/Validation/AbstractValidator.php @@ -14,7 +14,7 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\ValidatorEnumInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValidatorEnumInterface; use Phalcon\Filter\Validation\ValidationInterface; use Phalcon\Filter\Validation\ValidatorInterface as PhalconValidator; @@ -27,6 +27,24 @@ public function __construct( ) { } + /** + * Validate a Command and return an array of errors. + * Empty array means valid. + * + * @param CommandInterface $command + * + * @return Result + */ + public function validate(CommandInterface $command): Result + { + $errors = $this->runValidations($command); + if (true !== empty($errors)) { + return Result::error($errors); + } + + return Result::success(); + } + /** * @param CommandInterface $command * diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdateTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdateTest.php index 4cbe259..240acfa 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdateTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserValidatorUpdateTest.php @@ -15,7 +15,6 @@ use Faker\Factory as FakerFactory; use Phalcon\Api\Domain\Application\User\Command\UserCommandFactory; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserValidatorUpdate; use Phalcon\Api\Tests\AbstractUnitTestCase; @@ -23,8 +22,6 @@ final class UserValidatorUpdateTest extends AbstractUnitTestCase { public function testError(): void { - /** @var UserSanitizer $sanitizer */ - $sanitizer = $this->container->get(UserSanitizer::class); /** @var UserCommandFactory $factory */ $factory = $this->container->get(UserCommandFactory::class); /** @var UserValidatorUpdate $validator */ From eb37f52c4a4743c07acd131eaded2e5d3a9d96fe Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:48:58 -0600 Subject: [PATCH 250/301] [#.x] - adjusting user mapper with new interface --- .../DataSource/User/Mapper/UserMapper.php | 33 +------ .../DataSource/User/Mapper/UserMapperTest.php | 99 ------------------- 2 files changed, 3 insertions(+), 129 deletions(-) diff --git a/src/Domain/Infrastructure/DataSource/User/Mapper/UserMapper.php b/src/Domain/Infrastructure/DataSource/User/Mapper/UserMapper.php index 70866ff..53ec88f 100644 --- a/src/Domain/Infrastructure/DataSource/User/Mapper/UserMapper.php +++ b/src/Domain/Infrastructure/DataSource/User/Mapper/UserMapper.php @@ -15,7 +15,6 @@ use Phalcon\Api\Domain\Application\User\Command\UserPutCommand; use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\MapperInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; @@ -24,11 +23,13 @@ * @phpstan-import-type TUserDbRecord from UserTypes * @phpstan-import-type TUserDomainToDbRecord from UserTypes */ -final class UserMapper implements MapperInterface +final class UserMapper implements UserMapperInterface { /** * Map Domain User -> DB row (usr_*) * + * @param CommandInterface $user + * * @return TUserDomainToDbRecord */ public function db(CommandInterface $user): array @@ -82,32 +83,4 @@ public function domain(array $row): User isset($row['usr_updated_usr_id']) ? (int)$row['usr_updated_usr_id'] : null, ); } - - /** - * Map input row -> Domain User - * - * @param TUser $row - */ - public function input(array $row): User - { - return new User( - $row['id'], - $row['status'], - (string)$row['email'], - (string)$row['password'], - $row['namePrefix'], - $row['nameFirst'], - $row['nameMiddle'], - $row['nameLast'], - $row['nameSuffix'], - $row['issuer'], - $row['tokenPassword'], - $row['tokenId'], - $row['preferences'], - $row['createdDate'], - $row['createdUserId'], - $row['updatedDate'], - $row['updatedUserId'], - ); - } } diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Mapper/UserMapperTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Mapper/UserMapperTest.php index 6e0040a..2b89b44 100644 --- a/tests/Unit/Domain/Infrastructure/DataSource/User/Mapper/UserMapperTest.php +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Mapper/UserMapperTest.php @@ -163,103 +163,4 @@ public function testDomain(): void $actual = $user->updatedUserId; $this->assertSame($expected, $actual); } - - public function testInput(): void - { - $faker = FakerFactory::create(); - $mapper = $this->container->get(UserMapper::class); - - $preferences = ['k' => $faker->word()]; - $preferencesJson = json_encode($preferences); - - $input = [ - 'id' => $faker->numberBetween(1, 1000), - 'status' => $faker->numberBetween(0, 9), - 'email' => $faker->safeEmail(), - 'password' => $faker->password(), - 'namePrefix' => $faker->optional()->title(), - 'nameFirst' => $faker->firstName(), - 'nameMiddle' => $faker->optional()->word(), - 'nameLast' => $faker->lastName(), - 'nameSuffix' => null, - 'issuer' => $faker->company(), - 'tokenPassword' => $faker->optional()->password(), - 'tokenId' => $faker->uuid(), - 'preferences' => $preferencesJson, - 'createdDate' => $faker->date(), - 'createdUserId' => $faker->numberBetween(1, 1000), - 'updatedDate' => $faker->date(), - 'updatedUserId' => $faker->numberBetween(1, 1000), - ]; - - $user = $mapper->input($input); - - $expected = $input['id']; - $actual = $user->id; - $this->assertSame($expected, $actual); - - $expected = $input['status']; - $actual = $user->status; - $this->assertSame($expected, $actual); - - $expected = $input['email']; - $actual = $user->email; - $this->assertSame($expected, $actual); - - $expected = $input['password']; - $actual = $user->password; - $this->assertSame($expected, $actual); - - $expected = $input['namePrefix']; - $actual = $user->namePrefix; - $this->assertSame($expected, $actual); - - $expected = $input['nameFirst']; - $actual = $user->nameFirst; - $this->assertSame($expected, $actual); - - $expected = $input['nameMiddle']; - $actual = $user->nameMiddle; - $this->assertSame($expected, $actual); - - $expected = $input['nameLast']; - $actual = $user->nameLast; - $this->assertSame($expected, $actual); - - $expected = null; - $actual = $user->nameSuffix; - $this->assertSame($expected, $actual); - - $expected = $input['issuer']; - $actual = $user->issuer; - $this->assertSame($expected, $actual); - - $expected = $input['tokenPassword']; - $actual = $user->tokenPassword; - $this->assertSame($expected, $actual); - - $expected = $input['tokenId']; - $actual = $user->tokenId; - $this->assertSame($expected, $actual); - - $expected = $input['preferences']; - $actual = $user->preferences; - $this->assertSame($expected, $actual); - - $expected = $input['createdDate']; - $actual = $user->createdDate; - $this->assertSame($expected, $actual); - - $expected = $input['createdUserId']; - $actual = $user->createdUserId; - $this->assertSame($expected, $actual); - - $expected = $input['updatedDate']; - $actual = $user->updatedDate; - $this->assertSame($expected, $actual); - - $expected = $input['updatedUserId']; - $actual = $user->updatedUserId; - $this->assertSame($expected, $actual); - } } From 5c1eb65a2c63292a48426139277fdf3e51fa34f8 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:49:59 -0600 Subject: [PATCH 251/301] [#.x] - adjusting docblocks --- src/Domain/Application/Auth/Facade/AuthFacade.php | 3 ++- src/Domain/Infrastructure/DataSource/AbstractSanitizer.php | 6 +++--- src/Domain/Infrastructure/DataSource/Validation/AbsInt.php | 2 ++ src/Domain/Infrastructure/Middleware/AbstractMiddleware.php | 4 ---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Domain/Application/Auth/Facade/AuthFacade.php b/src/Domain/Application/Auth/Facade/AuthFacade.php index c32dffb..b0cc23b 100644 --- a/src/Domain/Application/Auth/Facade/AuthFacade.php +++ b/src/Domain/Application/Auth/Facade/AuthFacade.php @@ -26,7 +26,8 @@ final readonly class AuthFacade { /** - * @param CommandBusInterface $bus + * @param CommandBusInterface $bus + * @param AuthCommandFactoryInterface $factory */ public function __construct( private CommandBusInterface $bus, diff --git a/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php b/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php index 18f2bb8..a5ef880 100644 --- a/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/AbstractSanitizer.php @@ -14,8 +14,8 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource; use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerEnumInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerInterface; -use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\SanitizersEnumInterface; use Phalcon\Filter\FilterInterface; /** @@ -38,7 +38,7 @@ public function __construct( public function sanitize(array $input): array { $enum = $this->enum; - /** @var SanitizersEnumInterface[] $fields */ + /** @var SanitizerEnumInterface[] $fields */ $fields = $enum::cases(); /** @@ -53,7 +53,7 @@ public function sanitize(array $input): array * * If there is no sanitizer defined, the value will be left intact. */ - /** @var SanitizersEnumInterface $field */ + /** @var SanitizerEnumInterface $field */ foreach ($fields as $field) { $value = $input[$field->name] ?? $field->default(); diff --git a/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php b/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php index 2344291..38f0018 100644 --- a/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php +++ b/src/Domain/Infrastructure/DataSource/Validation/AbsInt.php @@ -13,6 +13,7 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\Validation; +use Phalcon\Di\Exception as DIException; use Phalcon\Filter\Validation; use Phalcon\Filter\Validation\Validator\Numericality; @@ -31,6 +32,7 @@ final class AbsInt extends Numericality * * @return bool * @throws Validation\Exception + * @throws DIException */ public function validate(Validation $validation, string $field): bool { diff --git a/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php index fafaaeb..605af96 100644 --- a/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php +++ b/src/Domain/Infrastructure/Middleware/AbstractMiddleware.php @@ -19,9 +19,7 @@ use Phalcon\Api\Responder\JsonResponder; use Phalcon\Api\Responder\ResponderInterface; use Phalcon\Api\Responder\ResponderTypes; -use Phalcon\Events\Exception as EventsException; use Phalcon\Http\RequestInterface; -use Phalcon\Http\Response\Exception as ResponseException; use Phalcon\Http\ResponseInterface; use Phalcon\Mvc\Micro; use Phalcon\Mvc\Micro\MiddlewareInterface; @@ -76,8 +74,6 @@ public function isEmptyBearerToken( * @param TErrors $errors * * @return void - * @throws EventsException - * @throws ResponseException */ protected function halt( Micro $application, From e1a6e63d96a213f38ab99cd4b2699a3ba2814339 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:50:39 -0600 Subject: [PATCH 252/301] [#.x] - new abstract value objects --- .../DataSource/AbstractInputValueObject.php | 75 +++++++++++++++++++ .../DataSource/AbstractValueObject.php | 43 ++++------- 2 files changed, 88 insertions(+), 30 deletions(-) create mode 100644 src/Domain/Infrastructure/DataSource/AbstractInputValueObject.php diff --git a/src/Domain/Infrastructure/DataSource/AbstractInputValueObject.php b/src/Domain/Infrastructure/DataSource/AbstractInputValueObject.php new file mode 100644 index 0000000..76f032c --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/AbstractInputValueObject.php @@ -0,0 +1,75 @@ + + * + * 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\Infrastructure\DataSource; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValueObjectInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +use function get_object_vars; + +/** + * Base factory for value objects such as input DTOs. + * + * Concrete DTOs must implement protected static function + * + * `fromArray(array $sanitized): static` + * + * and keep themselves immutable/read\-only. + * + * @phpstan-import-type TInputSanitize from InputTypes + * @phpstan-import-type TAuthInput from InputTypes + * @phpstan-import-type TCompany from CompanyTypes + * @phpstan-import-type TUser from UserTypes + * @phpstan-type TInputs TAuthInput|TCompany|TUser + */ +abstract class AbstractInputValueObject implements ValueObjectInterface +{ + /** + * Factory that accepts a SanitizerInterface and returns the concrete DTO. + * + * @param SanitizerInterface $sanitizer + * @param TInputSanitize $input + * + * @return static + */ + public static function new(SanitizerInterface $sanitizer, array $input): static + { + $sanitized = $sanitizer->sanitize($input); + + return static::fromArray($sanitized); + } + + /** + * @return TInputs + */ + public function toArray(): array + { + /** @var TInputs $vars */ + $vars = get_object_vars($this); + + return $vars; + } + + /** + * Build the concrete DTO from a sanitized array. + * + * @param TInputSanitize $sanitized + * + * @return static + */ + abstract protected static function fromArray(array $sanitized): static; +} diff --git a/src/Domain/Infrastructure/DataSource/AbstractValueObject.php b/src/Domain/Infrastructure/DataSource/AbstractValueObject.php index f0f7e0d..4810697 100644 --- a/src/Domain/Infrastructure/DataSource/AbstractValueObject.php +++ b/src/Domain/Infrastructure/DataSource/AbstractValueObject.php @@ -13,43 +13,26 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource; -use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValueObjectInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +use function get_object_vars; /** - * Base factory for value objects such as input DTOs. - * - * Concrete DTOs must implement protected static function - * - * `fromArray(array $sanitized): static` - * - * and keep themselves immutable/read\-only. - * - * @phpstan-import-type TInputSanitize from InputTypes + * @phpstan-import-type TCompany from CompanyTypes + * @phpstan-import-type TUser from UserTypes */ -abstract class AbstractValueObject +abstract class AbstractValueObject implements ValueObjectInterface { /** - * Factory that accepts a SanitizerInterface and returns the concrete DTO. - * - * @param SanitizerInterface $sanitizer - * @param TInputSanitize $input - * - * @return static + * @return TUser|TCompany */ - public static function new(SanitizerInterface $sanitizer, array $input): static + public function toArray(): array { - $sanitized = $sanitizer->sanitize($input); + /** @var TUser|TCompany $vars */ + $vars = get_object_vars($this); - return static::fromArray($sanitized); + return $vars; } - - /** - * Build the concrete DTO from a sanitized array. - * - * @param TInputSanitize $sanitized - * - * @return static - */ - abstract protected static function fromArray(array $sanitized): static; } From c7f4054e4ea94dd946e0a983ee84cb3aedf0ecf9 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:51:20 -0600 Subject: [PATCH 253/301] [#.x] - adjusting dtos --- .../DataSource/User/DTO/User.php | 41 +++++++++++-------- .../DataSource/User/DTO/UserInput.php | 19 +-------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/Domain/Infrastructure/DataSource/User/DTO/User.php b/src/Domain/Infrastructure/DataSource/User/DTO/User.php index 1c14fd3..81ab31b 100644 --- a/src/Domain/Infrastructure/DataSource/User/DTO/User.php +++ b/src/Domain/Infrastructure/DataSource/User/DTO/User.php @@ -13,16 +13,31 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractValueObject; -use function get_object_vars; use function trim; -/** - * @phpstan-import-type TUser from UserTypes - */ -final readonly class User +final class User extends AbstractValueObject { + /** + * @param int $id + * @param int $status + * @param string $email + * @param string $password + * @param string|null $namePrefix + * @param string|null $nameFirst + * @param string|null $nameMiddle + * @param string|null $nameLast + * @param string|null $nameSuffix + * @param string|null $issuer + * @param string|null $tokenPassword + * @param string|null $tokenId + * @param string|null $preferences + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + */ public function __construct( public int $id, public int $status, @@ -44,21 +59,13 @@ public function __construct( ) { } + /** + * @return string + */ public function fullName(): string { return trim( ($this->nameLast ?? '') . ', ' . ($this->nameFirst ?? '') . ' ' . ($this->nameMiddle ?? '') ); } - - /** - * @return TUser - */ - public function toArray(): array - { - /** @var TUser $vars */ - $vars = get_object_vars($this); - - return $vars; - } } diff --git a/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php b/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php index cce2cca..a92a65c 100644 --- a/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php +++ b/src/Domain/Infrastructure/DataSource/User/DTO/UserInput.php @@ -14,18 +14,14 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractValueObject; -use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; - -use function get_object_vars; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractInputValueObject; /** * Value object for input * * @phpstan-import-type TUserInput from InputTypes - * @phpstan-import-type TUser from UserTypes */ -final class UserInput extends AbstractValueObject +final class UserInput extends AbstractInputValueObject { /** * @param int $id @@ -67,17 +63,6 @@ public function __construct( ) { } - /** - * @return TUser - */ - public function toArray(): array - { - /** @var TUser $vars */ - $vars = get_object_vars($this); - - return $vars; - } - /** * Build the concrete DTO from a sanitized array. * From 4e2c0113e84631801ad72b4b3166c891e83cd749 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:51:40 -0600 Subject: [PATCH 254/301] [#.x] - moving common code in --- .../DataSource/AbstractRepository.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/Domain/Infrastructure/DataSource/AbstractRepository.php b/src/Domain/Infrastructure/DataSource/AbstractRepository.php index 4ac90dc..2ac92b3 100644 --- a/src/Domain/Infrastructure/DataSource/AbstractRepository.php +++ b/src/Domain/Infrastructure/DataSource/AbstractRepository.php @@ -13,12 +13,19 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\DataMapper\Pdo\Exception\Exception as DataMapperException; use Phalcon\DataMapper\Query\Delete; +use Phalcon\DataMapper\Query\Insert; +use Phalcon\DataMapper\Query\Update; /** * @phpstan-import-type TCriteria from UserTypes + * @phpstan-import-type TUserDbRecordOptional from UserTypes + * @phpstan-import-type TCompanyDbRecordOptional from CompanyTypes + * @phpstan-type TDbRecordOptional TCompanyDbRecordOptional|TUserDbRecordOptional */ abstract class AbstractRepository { @@ -40,6 +47,7 @@ public function __construct( * @param TCriteria $criteria * * @return int + * @throws DataMapperException */ public function deleteBy(array $criteria): int { @@ -67,4 +75,43 @@ public function deleteById(int $recordId): int ] ); } + + /** + * + * @param TDbRecordOptional $columns + * + * @return int + * @throws DataMapperException + */ + public function insert(array $columns): int + { + $insert = Insert::new($this->connection); + $insert + ->into($this->table) + ->columns($columns) + ->perform() + ; + + return (int)$insert->getLastInsertId(); + } + + /** + * @param int $recordId + * @param TDbRecordOptional $columns + * + * @return int + * @throws DataMapperException + */ + public function update(int $recordId, array $columns): int + { + $update = Update::new($this->connection); + $update + ->table($this->table) + ->columns($columns) + ->where($this->idField . ' = ', $recordId) + ->perform() + ; + + return $recordId; + } } From b9d31f36c10ae7ef828d265e56d79814c19e8edf Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:52:03 -0600 Subject: [PATCH 255/301] [#.x] - added companies test migration --- tests/AbstractUnitTestCase.php | 66 ++++++++++++ .../Domain/Migrations/CompaniesMigration.php | 100 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 tests/Fixtures/Domain/Migrations/CompaniesMigration.php diff --git a/tests/AbstractUnitTestCase.php b/tests/AbstractUnitTestCase.php index 4755dbc..e8a7619 100644 --- a/tests/AbstractUnitTestCase.php +++ b/tests/AbstractUnitTestCase.php @@ -20,6 +20,7 @@ use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\Encryption\Security; use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Encryption\Security\JWT\Builder; @@ -97,6 +98,71 @@ public function getConnection(): Connection return $this->connection; } + /** + * @param CompaniesMigration $migration + * @param array $fields + * + * @return array + */ + public function getNewCompany(CompaniesMigration $migration, array $fields = []): array + { + $companyData = $this->getNewCompanyData($fields); + $companyId = $migration->insert( + null, + $companyData['com_name'], + $companyData['com_email'], + $companyData['com_phone'], + $companyData['com_website'], + $companyData['com_address_line_1'], + $companyData['com_address_line_2'], + $companyData['com_city'], + $companyData['com_state_province'], + $companyData['com_zip_code'], + $companyData['com_country'], + $companyData['com_created_date'], + $companyData['com_created_usr_id'], + $companyData['com_updated_date'], + $companyData['com_updated_usr_id'], + ); + + $dbUser = $this->getFromDatabase( + 'co_companies', + [ + 'com_id' => $companyId, + ] + ); + + return $dbUser[0]; + } + + /** + * @param array $fields + * + * @return array + */ + public function getNewCompanyData(array $fields = []): array + { + $faker = Factory::create(); + + return [ + 'com_id' => 0, + 'com_name' => $fields['com_name'] ?? $faker->company(), + 'com_email' => $fields['com_email'] ?? $faker->safeEmail(), + 'com_phone' => $fields['com_phone'] ?? $faker->phoneNumber(), + 'com_website' => $fields['com_website'] ?? $faker->domainName(), + 'com_address_line_1' => $fields['com_address_line_1'] ?? $faker->streetAddress(), + 'com_address_line_2' => $fields['com_address_line_2'] ?? $faker->streetAddress(), + 'com_city' => $fields['com_city'] ?? $faker->city(), + 'com_state_province' => $fields['com_state_province'] ?? $faker->city(), + 'com_zip_code' => $fields['com_zip_code'] ?? $faker->postcode(), + 'com_country' => $fields['com_country'] ?? $faker->countryISOAlpha3(), + 'com_created_date' => $fields['com_created_date'] ?? $faker->date(Dates::DATE_TIME_FORMAT), + 'com_created_usr_id' => $fields['com_created_usr_id'] ?? 0, + 'com_updated_date' => $fields['com_updated_date'] ?? $faker->date(Dates::DATE_TIME_FORMAT), + 'com_updated_usr_id' => $fields['com_updated_usr_id'] ?? 0, + ]; + } + /** * @param UsersMigration $migration * @param array $fields diff --git a/tests/Fixtures/Domain/Migrations/CompaniesMigration.php b/tests/Fixtures/Domain/Migrations/CompaniesMigration.php new file mode 100644 index 0000000..220b699 --- /dev/null +++ b/tests/Fixtures/Domain/Migrations/CompaniesMigration.php @@ -0,0 +1,100 @@ + + * + * 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\Fixtures\Domain\Migrations; + +use PHPUnit\Framework\Assert; + +final class CompaniesMigration extends AbstractMigration +{ + protected string $table = 'co_companies'; + + public function insert( + ?int $id, + string $name, + string $email, + string $phone, + string $website, + string $addressLine1, + string $addressLine2, + string $city, + string $stateProvince, + string $zip_code, + string $country, + string $createdDate, + int $createdUserId, + string $updatedDate, + int $updatedUserId + ): int { + $sql = "INSERT INTO $this->table ( + com_id, + com_name, + com_email, + com_phone, + com_website, + com_address_line_1, + com_address_line_2, + com_city, + com_state_province, + com_zip_code, + com_country, + com_created_date, + com_created_usr_id, + com_updated_date, + com_updated_usr_id + ) VALUES ( + :id, + :name, + :email, + :phone, + :website, + :addressLine1, + :addressLine2, + :city, + :stateProvince, + :zip_code, + :country, + :createdDate, + :createdUserId, + :updatedDate, + :updatedUserId + )"; + + $stmt = $this->connection->prepare($sql); + $params = [ + ':id' => $id, + ':name' => $name, + ':email' => $email, + ':phone' => $phone, + ':website' => $website, + ':addressLine1' => $addressLine1, + ':addressLine2' => $addressLine2, + ':city' => $city, + ':stateProvince' => $stateProvince, + ':zip_code' => $zip_code, + ':country' => $country, + ':createdDate' => $createdDate, + ':createdUserId' => $createdUserId, + ':updatedDate' => $updatedDate, + ':updatedUserId' => $updatedUserId, + ]; + $result = $stmt->execute($params); + if (!$result) { + Assert::fail( + "Failed to insert id [#$id] into table [$this->table]" + ); + } + + return (int)$this->connection->lastInsertId(); + } +} From 68d5608648b0c10f7b6e04c7a2546760a70fbab9 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:52:18 -0600 Subject: [PATCH 256/301] [#.x] - migration for companies table --- .../20251226161244_add_companies_table.php | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 resources/db/migrations/20251226161244_add_companies_table.php diff --git a/resources/db/migrations/20251226161244_add_companies_table.php b/resources/db/migrations/20251226161244_add_companies_table.php new file mode 100644 index 0000000..107d8af --- /dev/null +++ b/resources/db/migrations/20251226161244_add_companies_table.php @@ -0,0 +1,190 @@ +table('co_companies')->drop()->save(); + } + + public function up(): void + { + $table = $this->table( + 'co_companies', + [ + 'id' => 'com_id', + 'signed' => false, + ] + ); + + $table + ->addColumn( + 'com_name', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_phone', + 'string', + [ + 'limit' => 32, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_email', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_website', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_address_line_1', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_address_line_2', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_city', + 'string', + [ + 'limit' => 128, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_state_province', + 'string', + [ + 'limit' => 64, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_zip_code', + 'string', + [ + 'limit' => 32, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_country', + 'string', + [ + 'limit' => 8, + 'null' => false, + 'default' => '', + ] + ) + ->addColumn( + 'com_created_date', + 'timestamp', + [ + 'timezone' => true, + 'default' => 'CURRENT_TIMESTAMP', + ] + ) + ->addColumn( + 'com_created_usr_id', + 'biginteger', + [ + 'null' => false, + 'default' => 0, + ] + ) + ->addColumn( + 'com_updated_date', + 'timestamp', + [ + 'timezone' => true, + 'default' => null, + ] + ) + ->addColumn( + 'com_updated_usr_id', + 'biginteger', + [ + 'null' => false, + 'default' => 0, + ] + ) + ->addIndex( + [ + 'com_name', + ], + [ + 'name' => 'i_name', + ] + ) + ->addIndex( + [ + 'com_email', + ], + [ + 'name' => 'i_email', + ] + ) + ->addIndex( + [ + 'com_city', + ], + [ + 'name' => 'i_city', + ] + ) + ->addIndex( + [ + 'com_state_province', + ], + [ + 'name' => 'i_state_province', + ] + ) + ->addIndex( + [ + 'com_country', + ], + [ + 'name' => 'i_country', + ] + ) + ->save() + ; + } +} From fcdfc44c45b9d2dd9f5dcb5f9c00b0507c7fdff2 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:52:37 -0600 Subject: [PATCH 257/301] [#.x] - adjusting dto --- src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php b/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php index 6194bd8..51dc658 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php +++ b/src/Domain/Infrastructure/DataSource/Auth/DTO/AuthInput.php @@ -14,12 +14,12 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\DTO; use Phalcon\Api\Domain\ADR\InputTypes; -use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractValueObject; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractInputValueObject; /** * @phpstan-import-type TAuthInput from InputTypes */ -final class AuthInput extends AbstractValueObject +final class AuthInput extends AbstractInputValueObject { /** * @param string|null $email From 14565bee2199289d2ef81d07924bc93671185cd7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:52:53 -0600 Subject: [PATCH 258/301] [#.x] - docblock correction --- .../DataSource/Auth/Validator/AuthLoginValidator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidator.php index 275775b..c65caab 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidator.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidator.php @@ -17,7 +17,6 @@ use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\Result; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\AuthLoginValidatorEnum; final class AuthLoginValidator extends AbstractValidator { From 717d8c4a72f2d9fddeb365440c926665341c5a44 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:53:14 -0600 Subject: [PATCH 259/301] [#.x] - added auth transformer --- .../Auth/Transformer/AuthTransformer.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Auth/Transformer/AuthTransformer.php diff --git a/src/Domain/Infrastructure/DataSource/Auth/Transformer/AuthTransformer.php b/src/Domain/Infrastructure/DataSource/Auth/Transformer/AuthTransformer.php new file mode 100644 index 0000000..dad8079 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Auth/Transformer/AuthTransformer.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\Domain\Infrastructure\DataSource\Auth\Transformer; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +/** + * Transforms the data for the payload + * + * @phpstan-type TTokens = array{ + * token: string, + * refreshToken: string + * } + * @phpstan-import-type TLoginResponsePayload from UserTypes + */ +final class AuthTransformer +{ + /** + * @param User $domainUser + * @param TTokens $tokens + * + * @return TLoginResponsePayload + */ + public function login(User $domainUser, array $tokens): array + { + return [ + 'authenticated' => true, + 'user' => [ + 'id' => $domainUser->id, + 'name' => $domainUser->fullName(), + 'email' => $domainUser->email, + ], + 'jwt' => [ + 'token' => $tokens['token'], + 'refreshToken' => $tokens['refreshToken'], + ], + ]; + } + + /** + * @return array{authenticated: false} + */ + public function logout(): array + { + return ['authenticated' => false]; + } + + /** + * @param TTokens $tokens + * + * @return TTokens + */ + public function refresh(array $tokens): array + { + return [ + 'token' => $tokens['token'], + 'refreshToken' => $tokens['refreshToken'], + ]; + } +} From 3ee1555825fdc4f419b0d206192512b21eecbed3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:53:41 -0600 Subject: [PATCH 260/301] [#.x] - handlers using the transformer now --- .../Auth/Handler/AuthLoginPostHandler.php | 31 ++++++------------- .../Auth/Handler/AuthLogoutPostHandler.php | 2 +- .../Auth/Handler/AuthRefreshPostHandler.php | 5 +-- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/src/Domain/Application/Auth/Handler/AuthLoginPostHandler.php b/src/Domain/Application/Auth/Handler/AuthLoginPostHandler.php index 0f7365b..3521de6 100644 --- a/src/Domain/Application/Auth/Handler/AuthLoginPostHandler.php +++ b/src/Domain/Application/Auth/Handler/AuthLoginPostHandler.php @@ -18,6 +18,7 @@ use Phalcon\Api\Domain\Application\Auth\Command\AuthLoginPostCommand; use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Transformer\AuthTransformer; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; use Phalcon\Api\Domain\Infrastructure\Encryption\Security; @@ -27,19 +28,21 @@ /** * @phpstan-import-type TAuthLoginInput from InputTypes */ -final class AuthLoginPostHandler implements HandlerInterface +final readonly class AuthLoginPostHandler implements HandlerInterface { /** * @param UserRepositoryInterface $repository * @param TokenManagerInterface $tokenManager + * @param AuthTransformer $transformer * @param Security $security * @param ValidatorInterface $validator */ public function __construct( - private readonly UserRepositoryInterface $repository, - private readonly TokenManagerInterface $tokenManager, - private readonly Security $security, - private readonly ValidatorInterface $validator + private UserRepositoryInterface $repository, + private TokenManagerInterface $tokenManager, + private AuthTransformer $transformer, + private Security $security, + private ValidatorInterface $validator ) { } @@ -87,22 +90,6 @@ public function __invoke(CommandInterface $command): Payload */ $tokens = $this->tokenManager->issue($domainUser); - /** - * Construct the response - */ - $results = [ - 'authenticated' => true, - 'user' => [ - 'id' => $domainUser->id, - 'name' => $domainUser->fullName(), - 'email' => $domainUser->email, - ], - 'jwt' => [ - 'token' => $tokens['token'], - 'refreshToken' => $tokens['refreshToken'], - ], - ]; - - return Payload::success($results); + return Payload::success($this->transformer->login($domainUser, $tokens)); } } diff --git a/src/Domain/Application/Auth/Handler/AuthLogoutPostHandler.php b/src/Domain/Application/Auth/Handler/AuthLogoutPostHandler.php index 26f1dc7..0ca76f3 100644 --- a/src/Domain/Application/Auth/Handler/AuthLogoutPostHandler.php +++ b/src/Domain/Application/Auth/Handler/AuthLogoutPostHandler.php @@ -49,6 +49,6 @@ public function __invoke(CommandInterface $command): Payload $this->tokenManager->revoke($domainUser); - return Payload::success(['authenticated' => false]); + return Payload::success($this->transformer->logout()); } } diff --git a/src/Domain/Application/Auth/Handler/AuthRefreshPostHandler.php b/src/Domain/Application/Auth/Handler/AuthRefreshPostHandler.php index be368c1..15f1412 100644 --- a/src/Domain/Application/Auth/Handler/AuthRefreshPostHandler.php +++ b/src/Domain/Application/Auth/Handler/AuthRefreshPostHandler.php @@ -49,9 +49,6 @@ public function __invoke(CommandInterface $command): Payload $tokens = $this->tokenManager->refresh($domainUser); - return Payload::success([ - 'token' => $tokens['token'], - 'refreshToken' => $tokens['refreshToken'], - ]); + return Payload::success($this->transformer->refresh($tokens)); } } From 1a400795e1b81d1f2821378fb7cbc261fac4b88e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:53:54 -0600 Subject: [PATCH 261/301] [#.x] - adjusting definitions to add the transformer --- .../Enums/Container/AuthDefinitionsEnum.php | 95 +++++++++---------- .../Container/AuthDefinitionsEnumTest.php | 26 +++++ 2 files changed, 71 insertions(+), 50 deletions(-) diff --git a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php index 2172ad8..afacbe2 100644 --- a/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnum.php @@ -24,6 +24,7 @@ use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Transformer\AuthTransformer; use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthLoginValidator; use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthTokenValidator; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; @@ -47,6 +48,7 @@ enum AuthDefinitionsEnum: string implements DefinitionsEnumInterface case AuthSanitizer = AuthSanitizer::class; case AuthLoginValidator = AuthLoginValidator::class; case AuthTokenValidator = AuthTokenValidator::class; + case AuthTransformer = AuthTransformer::class; /** * @return TService @@ -55,7 +57,7 @@ public function definition(): array { return match ($this) { self::AuthCommandFactory => [ - 'className' => AuthCommandFactory::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -63,11 +65,19 @@ public function definition(): array ], ], ], - self::AuthLoginPost => $this->getService(AuthLoginPostService::class), - self::AuthLogoutPost => $this->getService(AuthLogoutPostService::class), - self::AuthRefreshPost => $this->getService(AuthRefreshPostService::class), + self::AuthLoginPost, + self::AuthLogoutPost, + self::AuthRefreshPost => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => AuthFacade::class, + ], + ], + ], self::AuthFacade => [ - 'className' => AuthFacade::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -80,7 +90,7 @@ public function definition(): array ], ], self::AuthLoginHandler => [ - 'className' => AuthLoginPostHandler::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -90,6 +100,10 @@ public function definition(): array 'type' => 'service', 'name' => TokenManager::class, ], + [ + 'type' => 'service', + 'name' => AuthTransformer::class, + ], [ 'type' => 'service', 'name' => Security::class, @@ -100,10 +114,26 @@ public function definition(): array ], ], ], - self::AuthLogoutHandler => $this->getHandlerService(AuthLogoutPostHandler::class), - self::AuthRefreshHandler => $this->getHandlerService(AuthRefreshPostHandler::class), + self::AuthLogoutHandler, + self::AuthRefreshHandler => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => TokenManager::class, + ], + [ + 'type' => 'service', + 'name' => AuthTransformer::class, + ], + [ + 'type' => 'service', + 'name' => AuthTokenValidator::class, + ], + ], + ], self::AuthSanitizer => [ - 'className' => AuthSanitizer::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -112,7 +142,7 @@ public function definition(): array ], ], self::AuthLoginValidator => [ - 'className' => AuthLoginValidator::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -121,7 +151,7 @@ public function definition(): array ], ], self::AuthTokenValidator => [ - 'className' => AuthTokenValidator::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -137,6 +167,9 @@ public function definition(): array ], ], ], + self::AuthTransformer => [ + 'className' => $this->value, + ], }; } @@ -144,45 +177,7 @@ public function isShared(): bool { return match ($this) { self::AuthSanitizer => true, - default => false, + default => false, }; } - - /** - * @param class-string $className - * - * @return TService - */ - private function getHandlerService(string $className): array - { - return [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => TokenManager::class, - ], - [ - 'type' => 'service', - 'name' => AuthTokenValidator::class, - ], - ], - ]; - } - - /** - * @return TService - */ - private function getService(string $className): array - { - return [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => AuthFacade::class, - ], - ], - ]; - } } diff --git a/tests/Unit/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnumTest.php index 1d5688b..a7d025d 100644 --- a/tests/Unit/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Container/AuthDefinitionsEnumTest.php @@ -24,6 +24,7 @@ use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; use Phalcon\Api\Domain\Infrastructure\Container; use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Transformer\AuthTransformer; use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthLoginValidator; use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthTokenValidator; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; @@ -112,6 +113,10 @@ public function testDefinition(): void 'type' => 'service', 'name' => TokenManager::class, ], + [ + 'type' => 'service', + 'name' => AuthTransformer::class, + ], [ 'type' => 'service', 'name' => Security::class, @@ -132,6 +137,10 @@ public function testDefinition(): void 'type' => 'service', 'name' => TokenManager::class, ], + [ + 'type' => 'service', + 'name' => AuthTransformer::class, + ], [ 'type' => 'service', 'name' => AuthTokenValidator::class, @@ -148,6 +157,10 @@ public function testDefinition(): void 'type' => 'service', 'name' => TokenManager::class, ], + [ + 'type' => 'service', + 'name' => AuthTransformer::class, + ], [ 'type' => 'service', 'name' => AuthTokenValidator::class, @@ -200,6 +213,12 @@ public function testDefinition(): void ]; $actual = AuthDefinitionsEnum::AuthTokenValidator->definition(); $this->assertSame($expected, $actual); + + $expected = [ + 'className' => AuthTransformer::class, + ]; + $actual = AuthDefinitionsEnum::AuthTransformer->definition(); + $this->assertSame($expected, $actual); } public function testIsShared(): void @@ -236,6 +255,9 @@ public function testIsShared(): void $actual = AuthDefinitionsEnum::AuthTokenValidator->isShared(); $this->assertFalse($actual); + + $actual = AuthDefinitionsEnum::AuthTransformer->isShared(); + $this->assertFalse($actual); } public function testValues(): void @@ -283,5 +305,9 @@ public function testValues(): void $expected = AuthTokenValidator::class; $actual = AuthDefinitionsEnum::AuthTokenValidator->value; $this->assertSame($expected, $actual); + + $expected = AuthTransformer::class; + $actual = AuthDefinitionsEnum::AuthTransformer->value; + $this->assertSame($expected, $actual); } } From 8a03a931cfb5ac8baf041316ee1fbf3fa7887a2c Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:54:09 -0600 Subject: [PATCH 262/301] [#.x] - adding the transformer --- .../Auth/Handler/AbstractAuthLogoutRefreshHandler.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Domain/Application/Auth/Handler/AbstractAuthLogoutRefreshHandler.php b/src/Domain/Application/Auth/Handler/AbstractAuthLogoutRefreshHandler.php index 9b190da..8a124c3 100644 --- a/src/Domain/Application/Auth/Handler/AbstractAuthLogoutRefreshHandler.php +++ b/src/Domain/Application/Auth/Handler/AbstractAuthLogoutRefreshHandler.php @@ -15,6 +15,7 @@ use Phalcon\Api\Domain\ADR\InputTypes; use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Transformer\AuthTransformer; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; @@ -25,10 +26,12 @@ abstract class AbstractAuthLogoutRefreshHandler implements HandlerInterface { /** * @param TokenManagerInterface $tokenManager + * @param AuthTransformer $transformer * @param ValidatorInterface $validator */ public function __construct( protected readonly TokenManagerInterface $tokenManager, + protected readonly AuthTransformer $transformer, protected readonly ValidatorInterface $validator, ) { } From 931bd137b749a41221ab199c12bdf571329f6694 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:54:19 -0600 Subject: [PATCH 263/301] [#.x] - docblock --- .../Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizer.php b/src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizer.php index c3a6da2..202e532 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizer.php @@ -14,7 +14,6 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer; use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; -use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\AuthSanitizerEnum; final class AuthSanitizer extends AbstractSanitizer { From 5ec8a243d5022554e9feacfb7f6ec557529a55e2 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:54:31 -0600 Subject: [PATCH 264/301] [#.x] - scope changes --- .../DataSource/Auth/Validator/AuthTokenValidator.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidator.php b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidator.php index a8508f7..81ba9bf 100644 --- a/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidator.php +++ b/src/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidator.php @@ -22,7 +22,6 @@ use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManagerInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Common\JWTEnum; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; -use Phalcon\Api\Domain\Infrastructure\Enums\Validator\AuthTokenValidatorEnum; use Phalcon\Encryption\Security\JWT\Token\Token; use Phalcon\Filter\Validation\ValidationInterface; @@ -31,8 +30,8 @@ final class AuthTokenValidator extends AbstractValidator protected string $fields = AuthTokenValidatorEnum::class; public function __construct( - private TokenManagerInterface $tokenManager, - private UserRepositoryInterface $userRepository, + private readonly TokenManagerInterface $tokenManager, + private readonly UserRepositoryInterface $userRepository, ValidationInterface $validator, ) { parent::__construct($validator); @@ -53,9 +52,6 @@ public function validate(CommandInterface $command): Result return Result::error([HttpCodesEnum::AppTokenNotPresent->error()]); } - /** - * @todo add the docblock for $command here - */ /** @var AuthLogoutPostCommand|AuthRefreshPostCommand $command */ /** @var string $token */ $token = $command->token; From 06ab4ecf45bafe1e95deb5ef0e0c82ba9097f908 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:54:42 -0600 Subject: [PATCH 265/301] [#.x] - added transformer in the container --- .../Enums/Container/CommonDefinitionsEnum.php | 39 ++++++------------- .../Container/CommonDefinitionsEnumTest.php | 25 ------------ 2 files changed, 11 insertions(+), 53 deletions(-) diff --git a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php index 70f95ea..627dc6e 100644 --- a/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnum.php @@ -16,13 +16,13 @@ use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; use Phalcon\Api\Domain\Infrastructure\CommandBus\ContainerHandlerLocator; use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; use Phalcon\Api\Domain\Infrastructure\Encryption\JWTToken; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenCache; use Phalcon\Api\Domain\Infrastructure\Encryption\TokenManager; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Responder\JsonResponder; use Phalcon\Cache\Cache; -use Phalcon\Events\Manager as EventsManager; use Phalcon\Http\Request; use Phalcon\Http\Response; use Phalcon\Mvc\Router; @@ -34,7 +34,6 @@ enum CommonDefinitionsEnum: string implements DefinitionsEnumInterface { case CommandBus = CommandBus::class; - case EventsManager = Container::EVENTS_MANAGER; case EnvManager = EnvManager::class; case JsonResponder = JsonResponder::class; case JWTToken = JWTToken::class; @@ -44,6 +43,7 @@ enum CommonDefinitionsEnum: string implements DefinitionsEnumInterface case Request = Container::REQUEST; case Response = Container::RESPONSE; case Router = Container::ROUTER; + case Transformer = Transformer::class; /** * @return TService @@ -52,7 +52,7 @@ public function definition(): array { return match ($this) { self::CommandBus => [ - 'className' => CommandBus::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -60,28 +60,14 @@ public function definition(): array ], ], ], - self::EventsManager => [ - 'className' => EventsManager::class, - 'calls' => [ - [ - 'method' => 'enablePriorities', - 'arguments' => [ - [ - 'type' => 'parameter', - 'value' => true, - ], - ], - ], - ], - ], - self::EnvManager => [ - 'className' => EnvManager::class, - ], - self::JsonResponder => [ - 'className' => JsonResponder::class, + self::EnvManager, + self::JsonResponder, + self::Registry, + self::Transformer => [ + 'className' => $this->value, ], self::JWTToken => [ - 'className' => JWTToken::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -90,7 +76,7 @@ public function definition(): array ], ], self::JWTTokenCache => [ - 'className' => TokenCache::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -99,7 +85,7 @@ public function definition(): array ], ], self::JWTTokenManager => [ - 'className' => TokenManager::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -115,9 +101,6 @@ public function definition(): array ], ], ], - self::Registry => [ - 'className' => Registry::class, - ], self::Request => [ 'className' => Request::class, ], diff --git a/tests/Unit/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnumTest.php index 4bf4a50..165fed2 100644 --- a/tests/Unit/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Container/CommonDefinitionsEnumTest.php @@ -24,7 +24,6 @@ use Phalcon\Api\Responder\JsonResponder; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Cache\Cache; -use Phalcon\Events\Manager as EventsManager; use Phalcon\Http\Request; use Phalcon\Http\Response; use Phalcon\Mvc\Router; @@ -46,23 +45,6 @@ public function testDefinition(): void $actual = CommonDefinitionsEnum::CommandBus->definition(); $this->assertSame($expected, $actual); - $expected = [ - 'className' => EventsManager::class, - 'calls' => [ - [ - 'method' => 'enablePriorities', - 'arguments' => [ - [ - 'type' => 'parameter', - 'value' => true, - ], - ], - ], - ], - ]; - $actual = CommonDefinitionsEnum::EventsManager->definition(); - $this->assertSame($expected, $actual); - $expected = [ 'className' => EnvManager::class, ]; @@ -155,9 +137,6 @@ public function testIsShared(): void $actual = CommonDefinitionsEnum::CommandBus->isShared(); $this->assertTrue($actual); - $actual = CommonDefinitionsEnum::EventsManager->isShared(); - $this->assertTrue($actual); - $actual = CommonDefinitionsEnum::EnvManager->isShared(); $this->assertTrue($actual); @@ -192,10 +171,6 @@ public function testValues(): void $actual = CommonDefinitionsEnum::CommandBus->value; $this->assertSame($expected, $actual); - $expected = Container::EVENTS_MANAGER; - $actual = CommonDefinitionsEnum::EventsManager->value; - $this->assertSame($expected, $actual); - $expected = EnvManager::class; $actual = CommonDefinitionsEnum::EnvManager->value; $this->assertSame($expected, $actual); From 08c820da3ae080db32f411307d1c922adee4a8e2 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:54:48 -0600 Subject: [PATCH 266/301] [#.x] - added transformer in the container --- src/Domain/Infrastructure/Container.php | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Domain/Infrastructure/Container.php b/src/Domain/Infrastructure/Container.php index a12e871..947f24c 100644 --- a/src/Domain/Infrastructure/Container.php +++ b/src/Domain/Infrastructure/Container.php @@ -17,18 +17,22 @@ use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; use Phalcon\Api\Domain\Infrastructure\Enums\Container\AuthDefinitionsEnum; use Phalcon\Api\Domain\Infrastructure\Enums\Container\CommonDefinitionsEnum; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\CompanyDefinitionsEnum; use Phalcon\Api\Domain\Infrastructure\Enums\Container\DefinitionsEnumInterface; use Phalcon\Api\Domain\Infrastructure\Enums\Container\UserDefinitionsEnum; use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Domain\Infrastructure\Listeners\DbErrorListener; use Phalcon\Cache\AdapterFactory; use Phalcon\Cache\Cache; use Phalcon\DataMapper\Pdo\Connection; use Phalcon\Di\Di; use Phalcon\Di\Service; +use Phalcon\Events\Manager as EventsManager; use Phalcon\Filter\FilterFactory; use Phalcon\Logger\Adapter\Stream; use Phalcon\Logger\Logger; use Phalcon\Storage\SerializerFactory; +use Psr\Log\LoggerInterface; use function sprintf; @@ -87,6 +91,7 @@ public function __construct() $services = [ Cache::class => $this->getServiceCache(), Connection::class => $this->getServiceConnection(), + self::EVENTS_MANAGER => $this->getServiceEventsManager(), self::FILTER => $this->getServiceFilter(), Logger::class => $this->getServiceLogger(), ContainerHandlerLocator::class => $this->getServiceHandlerLocator(), @@ -99,6 +104,10 @@ public function __construct() * Auth related services */ $services = $this->registerFromEnum(AuthDefinitionsEnum::class, $services); + /** + * Company related services + */ + $services = $this->registerFromEnum(CompanyDefinitionsEnum::class, $services); /** * User related services */ @@ -195,6 +204,29 @@ function () { ); } + /** + * @return Service + */ + private function getServiceEventsManager(): Service + { + return new Service( + function () { + $evManager = new EventsManager(); + $evManager->enablePriorities(true); + + /** @var LoggerInterface $logger */ + $logger = $this->getShared(Logger::class); + $listener = new DbErrorListener($logger); + + $evManager->attach('user', $listener); + $evManager->attach('company', $listener); + + return $evManager; + }, + true + ); + } + /** * @return Service */ From 1d2518f6bb3bdb19666c1ff3e6cb50a120e0abb9 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:55:13 -0600 Subject: [PATCH 267/301] [#.x] - added transformer class --- .../DataSource/Transformer/Transformer.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Transformer/Transformer.php diff --git a/src/Domain/Infrastructure/DataSource/Transformer/Transformer.php b/src/Domain/Infrastructure/DataSource/Transformer/Transformer.php new file mode 100644 index 0000000..6fabb92 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Transformer/Transformer.php @@ -0,0 +1,71 @@ + + * + * 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\Infrastructure\DataSource\Transformer; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValueObjectInterface; + +/** + * Transforms objects to array representations so that they can be passed + * to the payload + * + * @template T of ValueObjectInterface + * @phpstan-type TItem = array + */ +final class Transformer +{ + /** + * @param int $recordId + * + * @return string[] + */ + public function delete(int $recordId): array + { + return [ + 'Record deleted successfully [#' . $recordId . '].', + ]; + } + + /** + * Transform a single object. + * + * @param T&object{id:int} $object + * + * @return array + * @phpstan-return array + */ + public function get(object $object): array + { + return [$object->id => $object->toArray()]; + } + + /** + * @param iterable $collection + * + * @return array + */ + public function getMany(iterable $collection): array + { + $result = []; + + /** + * @var int|string $key + * @var ValueObjectInterface $object + */ + foreach ($collection as $key => $object) { + $result[$key] = $object->toArray(); + } + + return $result; + } +} From feb1384ce27996915138803a334adb6890115a34 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:55:26 -0600 Subject: [PATCH 268/301] [#.x] - new vo interface --- .../Interface/ValueObjectInterface.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Interface/ValueObjectInterface.php diff --git a/src/Domain/Infrastructure/DataSource/Interface/ValueObjectInterface.php b/src/Domain/Infrastructure/DataSource/Interface/ValueObjectInterface.php new file mode 100644 index 0000000..d1e2d20 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Interface/ValueObjectInterface.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\Domain\Infrastructure\DataSource\Interface; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; + +/** + * @phpstan-import-type TCompany from CompanyTypes + * @phpstan-import-type TUser from UserTypes + */ +interface ValueObjectInterface +{ + /** + * @return TCompany|TUser + */ + public function toArray(): array; +} From 4e0d0cf86d0cd1e751d4c63b6462e496050c7303 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:56:02 -0600 Subject: [PATCH 269/301] [#.x] - updating sanitizers and validators --- .../User/Validator/UserInsertEnum.php | 43 +++++ .../User/Validator/UserUpdateEnum.php | 51 ++++++ .../User/Sanitizer/UserSanitizerEnumTest.php | 172 ++++++++++++++++++ .../User/Validator/UserInsertEnumTest.php | 80 ++++++++ .../User/Validator/UserUpdateEnumTest.php | 83 +++++++++ 5 files changed, 429 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/User/Validator/UserInsertEnum.php create mode 100644 src/Domain/Infrastructure/DataSource/User/Validator/UserUpdateEnum.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserInsertEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserUpdateEnumTest.php diff --git a/src/Domain/Infrastructure/DataSource/User/Validator/UserInsertEnum.php b/src/Domain/Infrastructure/DataSource/User/Validator/UserInsertEnum.php new file mode 100644 index 0000000..56864ce --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Validator/UserInsertEnum.php @@ -0,0 +1,43 @@ + + * + * 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\Infrastructure\DataSource\User\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValidatorEnumInterface; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum UserInsertEnum implements ValidatorEnumInterface +{ + case email; + case password; + case issuer; + case tokenPassword; + case tokenId; + + public function allowEmpty(): bool + { + return false; + } + + public function validators(): array + { + return match ($this) { + self::email => [ + PresenceOf::class, + Email::class, + ], + default => [PresenceOf::class], + }; + } +} diff --git a/src/Domain/Infrastructure/DataSource/User/Validator/UserUpdateEnum.php b/src/Domain/Infrastructure/DataSource/User/Validator/UserUpdateEnum.php new file mode 100644 index 0000000..7e08ea5 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/User/Validator/UserUpdateEnum.php @@ -0,0 +1,51 @@ + + * + * 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\Infrastructure\DataSource\User\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValidatorEnumInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum UserUpdateEnum implements ValidatorEnumInterface +{ + case id; + case email; + case password; + case issuer; + case tokenPassword; + case tokenId; + + public function allowEmpty(): bool + { + return match ($this) { + self::id => false, + default => true, + }; + } + + public function validators(): array + { + return match ($this) { + self::id => [ + PresenceOf::class, + AbsInt::class, + ], + self::email => [ + Email::class, + ], + default => [], + }; + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerEnumTest.php new file mode 100644 index 0000000..816c868 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizerEnumTest.php @@ -0,0 +1,172 @@ + + * + * 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\Infrastructure\DataSource\User\Sanitizer; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizerEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class UserSanitizerEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 17; + $actual = UserSanitizerEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = UserSanitizerEnum::id->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = UserSanitizerEnum::id->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_EMAIL; + $actual = UserSanitizerEnum::email->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::email->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = UserSanitizerEnum::status->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = UserSanitizerEnum::status->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_EMAIL; + $actual = UserSanitizerEnum::email->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::email->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = UserSanitizerEnum::password->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::password->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::namePrefix->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::namePrefix->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::nameFirst->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::nameFirst->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::nameLast->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::nameLast->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::nameMiddle->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::nameMiddle->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::nameSuffix->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::nameSuffix->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = UserSanitizerEnum::issuer->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::issuer->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = UserSanitizerEnum::tokenPassword->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::tokenPassword->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = UserSanitizerEnum::tokenId->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::tokenId->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::preferences->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::preferences->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = UserSanitizerEnum::createdUserId->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = UserSanitizerEnum::createdUserId->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::createdDate->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::createdDate->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = UserSanitizerEnum::updatedUserId->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = UserSanitizerEnum::updatedUserId->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = UserSanitizerEnum::updatedDate->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = UserSanitizerEnum::updatedDate->default(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserInsertEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserInsertEnumTest.php new file mode 100644 index 0000000..10dc2b1 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserInsertEnumTest.php @@ -0,0 +1,80 @@ + + * + * 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\Infrastructure\DataSource\User\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserInsertEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class UserInsertEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 5; + $actual = UserInsertEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + Email::class, + ]; + $actual = UserInsertEnum::email->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::email->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = UserInsertEnum::password->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::password->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = UserInsertEnum::issuer->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::issuer->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = UserInsertEnum::tokenPassword->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::tokenPassword->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = UserInsertEnum::tokenId->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserInsertEnum::tokenId->allowEmpty(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserUpdateEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserUpdateEnumTest.php new file mode 100644 index 0000000..68c42e5 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/User/Validator/UserUpdateEnumTest.php @@ -0,0 +1,83 @@ + + * + * 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\Infrastructure\DataSource\User\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Validator\UserUpdateEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class UserUpdateEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 6; + $actual = UserUpdateEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + AbsInt::class, + ]; + $actual = UserUpdateEnum::id->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = UserUpdateEnum::id->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + Email::class, + ]; + $actual = UserUpdateEnum::email->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::email->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = []; + $actual = UserUpdateEnum::password->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::password->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = []; + $actual = UserUpdateEnum::issuer->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::issuer->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = []; + $actual = UserUpdateEnum::tokenPassword->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::tokenPassword->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = []; + $actual = UserUpdateEnum::tokenId->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = UserUpdateEnum::tokenId->allowEmpty(); + $this->assertSame($expected, $actual); + } +} From 75e3c633a9071b9c16a00d4076173de9938177ce Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:56:24 -0600 Subject: [PATCH 270/301] [#.x] - removing common code --- .../User/Repository/UserRepository.php | 45 +------------------ 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/src/Domain/Infrastructure/DataSource/User/Repository/UserRepository.php b/src/Domain/Infrastructure/DataSource/User/Repository/UserRepository.php index ae62a64..d176fd8 100644 --- a/src/Domain/Infrastructure/DataSource/User/Repository/UserRepository.php +++ b/src/Domain/Infrastructure/DataSource/User/Repository/UserRepository.php @@ -14,19 +14,16 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository; use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractRepository; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\MapperInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapperInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use Phalcon\Api\Domain\Infrastructure\Enums\Common\FlagsEnum; use Phalcon\DataMapper\Pdo\Connection; -use Phalcon\DataMapper\Query\Insert; use Phalcon\DataMapper\Query\Select; -use Phalcon\DataMapper\Query\Update; /** * @phpstan-import-type TCriteria from UserTypes * @phpstan-import-type TUserRecord from UserTypes - * @phpstan-import-type TUserDbRecordOptional from UserTypes */ class UserRepository extends AbstractRepository implements UserRepositoryInterface { @@ -41,7 +38,7 @@ class UserRepository extends AbstractRepository implements UserRepositoryInterfa public function __construct( Connection $connection, - private readonly MapperInterface $mapper, + private readonly UserMapperInterface $mapper, ) { parent::__construct($connection); } @@ -107,42 +104,4 @@ public function findOneBy(array $criteria): ?User return $this->mapper->domain($result); } - - - /** - * - * @param TUserDbRecordOptional $columns - * - * @return int - */ - public function insert(array $columns): int - { - $insert = Insert::new($this->connection); - $insert - ->into($this->table) - ->columns($columns) - ->perform() - ; - - return (int)$insert->getLastInsertId(); - } - - /** - * @param int $recordId - * @param TUserDbRecordOptional $columns - * - * @return int - */ - public function update(int $recordId, array $columns): int - { - $update = Update::new($this->connection); - $update - ->table($this->table) - ->columns($columns) - ->where($this->idField . ' = ', $recordId) - ->perform() - ; - - return $recordId; - } } From f6437ef5676621b18cbcc60fda8bd1d4222da8ff Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:56:33 -0600 Subject: [PATCH 271/301] [#.x] - updating docblock --- .../Infrastructure/DataSource/User/Sanitizer/UserSanitizer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizer.php b/src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizer.php index db9c45b..58dd843 100644 --- a/src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizer.php +++ b/src/Domain/Infrastructure/DataSource/User/Sanitizer/UserSanitizer.php @@ -14,7 +14,6 @@ namespace Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer; use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; -use Phalcon\Api\Domain\Infrastructure\Enums\Sanitizer\UserSanitizerEnum; final class UserSanitizer extends AbstractSanitizer { From f94f97dcee0ae647c7d2b50fa7ebc7a7d9035846 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:56:44 -0600 Subject: [PATCH 272/301] [#.x] - scope changes --- src/Domain/Infrastructure/Encryption/TokenCache.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Domain/Infrastructure/Encryption/TokenCache.php b/src/Domain/Infrastructure/Encryption/TokenCache.php index c974469..7a53d99 100644 --- a/src/Domain/Infrastructure/Encryption/TokenCache.php +++ b/src/Domain/Infrastructure/Encryption/TokenCache.php @@ -30,10 +30,10 @@ * @phpstan-import-type TTokenIssue from TokenManagerInterface * @phpstan-import-type TValidatorErrors from InputTypes */ -final class TokenCache implements TokenCacheInterface +final readonly class TokenCache implements TokenCacheInterface { public function __construct( - private readonly Cache $cache, + private Cache $cache, ) { } From fc513f8f78786df44dcbeee873eb29e511951a26 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:56:59 -0600 Subject: [PATCH 273/301] [#.x] - adding transformer in the definitions --- .../Enums/Container/UserDefinitionsEnum.php | 95 ++++++++----------- .../Container/UserDefinitionsEnumTest.php | 15 ++- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php index 799e4aa..c6a3463 100644 --- a/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php +++ b/src/Domain/Infrastructure/Enums/Container/UserDefinitionsEnum.php @@ -25,6 +25,7 @@ use Phalcon\Api\Domain\Application\User\Service\UserPutService; use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; @@ -63,7 +64,7 @@ public function definition(): array { return match ($this) { self::UserCommandFactory => [ - 'className' => UserCommandFactory::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -71,12 +72,20 @@ public function definition(): array ], ], ], - self::UserDelete => $this->getService(UserDeleteService::class), - self::UserGet => $this->getService(UserGetService::class), - self::UserPost => $this->getService(UserPostService::class), - self::UserPut => $this->getService(UserPutService::class), + self::UserDelete, + self::UserGet, + self::UserPost, + self::UserPut => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => UserFacade::class, + ], + ], + ], self::UserFacade => [ - 'className' => UserFacade::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -88,37 +97,33 @@ public function definition(): array ], ], ], - self::UserDeleteHandler => [ - 'className' => UserDeleteHandler::class, + self::UserDeleteHandler, + self::UserGetHandler => [ + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', 'name' => UserRepository::class, ], - ], - ], - self::UserGetHandler => [ - 'className' => UserGetHandler::class, - 'arguments' => [ [ 'type' => 'service', - 'name' => UserRepository::class, + 'name' => Transformer::class, ], ], ], self::UserPostHandler => $this->getServicePutPost( - UserPostHandler::class, + $this->value, UserValidator::class ), self::UserPutHandler => $this->getServicePutPost( - UserPutHandler::class, + $this->value, UserValidatorUpdate::class ), self::UserMapper => [ - 'className' => UserMapper::class, + 'className' => $this->value, ], self::UserRepository => [ - 'className' => UserRepository::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -131,7 +136,7 @@ public function definition(): array ], ], self::UserSanitizer => [ - 'className' => UserSanitizer::class, + 'className' => $this->value, 'arguments' => [ [ 'type' => 'service', @@ -139,8 +144,16 @@ public function definition(): array ], ], ], - self::UserValidator => $this->getServiceValidator(UserValidator::class), - self::UserValidatorUpdate => $this->getServiceValidator(UserValidatorUpdate::class), + self::UserValidator, + self::UserValidatorUpdate => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Validation::class, + ], + ], + ], }; } @@ -150,28 +163,10 @@ public function isShared(): bool self::UserMapper, self::UserRepository, self::UserSanitizer => true, - default => false, + default => false, }; } - /** - * @param class-string $className - * - * @return TService - */ - private function getService(string $className): array - { - return [ - 'className' => $className, - 'arguments' => [ - [ - 'type' => 'service', - 'name' => UserFacade::class, - ], - ], - ]; - } - /** * @param class-string $className * @param class-string $validatorName @@ -201,29 +196,15 @@ private function getServicePutPost(string $className, string $validatorName): ar ], [ 'type' => 'service', - 'name' => Registry::class, + 'name' => Transformer::class, ], [ 'type' => 'service', - 'name' => Security::class, + 'name' => Registry::class, ], - ], - ]; - } - - /** - * @param class-string $className - * - * @return TService - */ - private function getServiceValidator(string $className): array - { - return [ - 'className' => $className, - 'arguments' => [ [ 'type' => 'service', - 'name' => Validation::class, + 'name' => Security::class, ], ], ]; diff --git a/tests/Unit/Domain/Infrastructure/Enums/Container/UserDefinitionsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Container/UserDefinitionsEnumTest.php index 9d90df8..028f38d 100644 --- a/tests/Unit/Domain/Infrastructure/Enums/Container/UserDefinitionsEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Container/UserDefinitionsEnumTest.php @@ -25,6 +25,7 @@ use Phalcon\Api\Domain\Application\User\Service\UserPutService; use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Sanitizer\UserSanitizer; @@ -124,6 +125,10 @@ public function testDefinition(): void 'type' => 'service', 'name' => UserRepository::class, ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], ], ]; $actual = UserDefinitionsEnum::UserDeleteHandler->definition(); @@ -138,7 +143,7 @@ public function testDefinition(): void ], [ 'type' => 'service', - 'name' => Container::EVENTS_MANAGER, + 'name' => Transformer::class, ], ], ]; @@ -164,6 +169,10 @@ public function testDefinition(): void 'type' => 'service', 'name' => Container::EVENTS_MANAGER, ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], [ 'type' => 'service', 'name' => Registry::class, @@ -196,6 +205,10 @@ public function testDefinition(): void 'type' => 'service', 'name' => Container::EVENTS_MANAGER, ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], [ 'type' => 'service', 'name' => Registry::class, From 2cdcd7b76bbdcdc520cbaeb5a164325c4836252f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:57:56 -0600 Subject: [PATCH 274/301] [#.x] - adding the transformer --- .../Application/User/Handler/UserDeleteHandler.php | 12 ++++++------ .../Application/User/Handler/UserGetHandler.php | 10 +++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Domain/Application/User/Handler/UserDeleteHandler.php b/src/Domain/Application/User/Handler/UserDeleteHandler.php index bb830fe..a1a518c 100644 --- a/src/Domain/Application/User/Handler/UserDeleteHandler.php +++ b/src/Domain/Application/User/Handler/UserDeleteHandler.php @@ -17,20 +17,24 @@ use Phalcon\Api\Domain\Application\User\Command\UserDeleteCommand; use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; final readonly class UserDeleteHandler implements HandlerInterface { /** * @param UserRepositoryInterface $repository + * @param Transformer $transformer */ public function __construct( private UserRepositoryInterface $repository, + private Transformer $transformer ) { } /** - * Get a user. + * Delete a user. * * @param CommandInterface $command * @@ -48,11 +52,7 @@ public function __invoke(CommandInterface $command): Payload $rowCount = $this->repository->deleteById($userId); if (0 !== $rowCount) { - return Payload::deleted( - [ - 'Record deleted successfully [#' . $userId . '].', - ], - ); + return Payload::deleted($this->transformer->delete($userId)); } } diff --git a/src/Domain/Application/User/Handler/UserGetHandler.php b/src/Domain/Application/User/Handler/UserGetHandler.php index 4638e99..ce8466e 100644 --- a/src/Domain/Application/User/Handler/UserGetHandler.php +++ b/src/Domain/Application/User/Handler/UserGetHandler.php @@ -17,15 +17,19 @@ use Phalcon\Api\Domain\Application\User\Command\UserGetCommand; use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; -final class UserGetHandler implements HandlerInterface +final readonly class UserGetHandler implements HandlerInterface { /** * @param UserRepositoryInterface $repository + * @param Transformer $transformer */ public function __construct( - private readonly UserRepositoryInterface $repository, + private UserRepositoryInterface $repository, + private Transformer $transformer, ) { } @@ -48,7 +52,7 @@ public function __invoke(CommandInterface $command): Payload $user = $this->repository->findById($userId); if (null !== $user) { - return Payload::success([$user->id => $user->toArray()]); + return Payload::success($this->transformer->get($user)); } } From c524e3ee310496de252a72fe9c6e3994c67b8dff Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:59:30 -0600 Subject: [PATCH 275/301] [#.x] - new listener for pdo errors --- .../User/Handler/UserPostHandler.php | 10 ++++- .../User/Handler/UserPutHandler.php | 10 ++++- .../Listeners/DbErrorListener.php | 44 +++++++++++++++++++ .../User/Handler/UserHandlerPostTest.php | 6 ++- .../User/Handler/UserHandlerPutTest.php | 7 ++- .../User/Service/UserServicePostTest.php | 25 ++++++++++- .../User/Service/UserServicePutTest.php | 6 ++- 7 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 src/Domain/Infrastructure/Listeners/DbErrorListener.php diff --git a/src/Domain/Application/User/Handler/UserPostHandler.php b/src/Domain/Application/User/Handler/UserPostHandler.php index a0e42b6..07f4512 100644 --- a/src/Domain/Application/User/Handler/UserPostHandler.php +++ b/src/Domain/Application/User/Handler/UserPostHandler.php @@ -58,8 +58,14 @@ public function __invoke(CommandInterface $command): Payload $userId = $this->repository->insert($user); } catch (PDOException $ex) { /** - * @todo send generic response and log the error + * Fire the event to the listeners */ + $this->eventsManager->fire( + 'user:pdoError', + $this, + $ex->getMessage() + ); + return $this->getErrorPayload( HttpCodesEnum::AppCannotCreateDatabaseRecord, $ex->getMessage() @@ -82,7 +88,7 @@ public function __invoke(CommandInterface $command): Payload /** * Return the user back */ - return Payload::created([$domainUser->id => $domainUser->toArray()]); + return Payload::created($this->transformer->get($domainUser)); } /** diff --git a/src/Domain/Application/User/Handler/UserPutHandler.php b/src/Domain/Application/User/Handler/UserPutHandler.php index 12ad103..5ebd73d 100644 --- a/src/Domain/Application/User/Handler/UserPutHandler.php +++ b/src/Domain/Application/User/Handler/UserPutHandler.php @@ -69,8 +69,14 @@ public function __invoke(CommandInterface $command): Payload $userId = $this->repository->update($command->id, $user); } catch (PDOException $ex) { /** - * @todo send generic response and log the error + * Fire the event to the listeners */ + $this->eventsManager->fire( + 'user:pdoError', + $this, + $ex->getMessage() + ); + return $this->getErrorPayload( HttpCodesEnum::AppCannotUpdateDatabaseRecord, $ex->getMessage() @@ -93,7 +99,7 @@ public function __invoke(CommandInterface $command): Payload /** * Return the user back */ - return Payload::updated([$domainUser->id => $domainUser->toArray()]); + return Payload::updated($this->transformer->get($domainUser)); } /** diff --git a/src/Domain/Infrastructure/Listeners/DbErrorListener.php b/src/Domain/Infrastructure/Listeners/DbErrorListener.php new file mode 100644 index 0000000..881a645 --- /dev/null +++ b/src/Domain/Infrastructure/Listeners/DbErrorListener.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\Infrastructure\Listeners; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Events\Event; +use Psr\Log\LoggerInterface; + +use function get_class; + +final readonly class DbErrorListener +{ + public function __construct( + private LoggerInterface $logger, + ) { + } + + /** + * @param Event $event + * @param HandlerInterface $handler + * + * @return void + */ + public function pdoError( + Event $event, + HandlerInterface $handler + ): void { + $class = get_class($handler); + /** @var string $data */ + $data = $event->getData(); + $this->logger->error($class . ': ' . $data); + } +} diff --git a/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php b/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php index 0a4ac1a..47eef85 100644 --- a/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php +++ b/tests/Unit/Domain/Application/User/Handler/UserHandlerPostTest.php @@ -24,6 +24,7 @@ use Phalcon\Support\Registry; use function strip_tags; +use function uniqid; final class UserHandlerPostTest extends AbstractUnitTestCase { @@ -86,6 +87,7 @@ public function testHandlerFailureNoIdReturned(): void public function testHandlerFailurePdoError(): void { + $message = uniqid('pdo-error-'); $userRepository = $this ->getMockBuilder(UserRepository::class) ->disableOriginalConstructor() @@ -98,7 +100,7 @@ public function testHandlerFailurePdoError(): void ; $userRepository ->method('insert') - ->willThrowException(new PDOException('abcde')) + ->willThrowException(new PDOException($message)) ; $this->container->setShared(UserRepository::class, $userRepository); @@ -138,7 +140,7 @@ public function testHandlerFailurePdoError(): void $errors = $actual['errors']; - $expected = [['Cannot create database record: abcde']]; + $expected = [['Cannot create database record: ' . $message]]; $actual = $errors; $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php b/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php index 015cf82..c2f645b 100644 --- a/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php +++ b/tests/Unit/Domain/Application/User/Handler/UserHandlerPutTest.php @@ -24,6 +24,8 @@ use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Support\Registry; +use function uniqid; + final class UserHandlerPutTest extends AbstractUnitTestCase { public function testHandlerFailureNoIdReturned(): void @@ -92,6 +94,7 @@ public function testHandlerFailureNoIdReturned(): void public function testHandlerFailurePdoError(): void { + $message = uniqid('pdo-error-'); /** @var UserMapper $userMapper */ $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ @@ -118,7 +121,7 @@ public function testHandlerFailurePdoError(): void $userRepository->method('findById')->willReturn($findByUser); $userRepository ->method('update') - ->willThrowException(new PDOException('abcde')) + ->willThrowException(new PDOException($message)) ; $this->container->setShared(UserRepository::class, $userRepository); @@ -152,7 +155,7 @@ public function testHandlerFailurePdoError(): void $errors = $actual['errors']; - $expected = [['Cannot update database record: abcde']]; + $expected = [['Cannot update database record: ' . $message]]; $actual = $errors; $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php b/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php index d025312..bbb6961 100644 --- a/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServicePostTest.php @@ -16,15 +16,18 @@ use DateTimeImmutable; use PayloadInterop\DomainStatus; use PDOException; +use Phalcon\Api\Domain\Application\User\Handler\UserPostHandler; use Phalcon\Api\Domain\Application\User\Service\UserPostService; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepository; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Support\Registry; use function array_key_first; use function str_starts_with; use function strip_tags; +use function uniqid; final class UserServicePostTest extends AbstractUnitTestCase { @@ -84,6 +87,7 @@ public function testServiceFailureNoIdReturned(): void public function testServiceFailurePdoError(): void { + $message = uniqid('pdo-error-'); $userRepository = $this ->getMockBuilder(UserRepository::class) ->disableOriginalConstructor() @@ -96,7 +100,7 @@ public function testServiceFailurePdoError(): void ; $userRepository ->method('insert') - ->willThrowException(new PDOException('abcde')) + ->willThrowException(new PDOException($message)) ; $this->container->setShared(UserRepository::class, $userRepository); @@ -107,6 +111,8 @@ public function testServiceFailurePdoError(): void $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); + /** @var EnvManager $env */ + $env = $this->container->get(EnvManager::class); $userData = $this->getNewUserData(); $userData['usr_id'] = 1; @@ -134,9 +140,24 @@ public function testServiceFailurePdoError(): void $errors = $actual['errors']; - $expected = [['Cannot create database record: abcde']]; + $expected = [['Cannot create database record: ' . $message]]; $actual = $errors; $this->assertSame($expected, $actual); + /** + * Also check the logger if the message was logged + */ + /** @var string $logName */ + $logName = $env->get('LOG_FILENAME', 'rest-api'); + /** @var string $logPath */ + $logPath = $env->get('LOG_PATH', 'storage/logs/'); + $logFile = $env->appPath($logPath) . '/' . $logName . '.log'; + + $message = sprintf( + '[error] %s: %s', + UserPostHandler::class, + $message + ); + $this->assertFileContentsContains($logFile, $message); } public function testServiceFailureValidation(): void diff --git a/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php b/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php index 6efc574..4c8d4dd 100644 --- a/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServicePutTest.php @@ -26,6 +26,7 @@ use function date; use function str_starts_with; use function strip_tags; +use function uniqid; final class UserServicePutTest extends AbstractUnitTestCase { @@ -93,6 +94,7 @@ public function testServiceFailureNoIdReturned(): void public function testServiceFailurePdoError(): void { + $message = uniqid('pdo-error-'); /** @var UserMapper $userMapper */ $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ @@ -117,7 +119,7 @@ public function testServiceFailurePdoError(): void $userRepository->method('findById')->willReturn($findByUser); $userRepository ->method('update') - ->willThrowException(new PDOException('abcde')) + ->willThrowException(new PDOException($message)) ; $this->container->setShared(UserRepository::class, $userRepository); @@ -151,7 +153,7 @@ public function testServiceFailurePdoError(): void $errors = $actual['errors']; - $expected = [['Cannot update database record: abcde']]; + $expected = [['Cannot update database record: ' . $message]]; $actual = $errors; $this->assertSame($expected, $actual); } From 9e7eab315a65b0000276fbfbfeb792daf503eb5f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 11:59:52 -0600 Subject: [PATCH 276/301] [#.x] - adjusting test --- .../Domain/Application/User/Service/UserServiceDispatchTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php b/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php index 6e615af..607af0e 100644 --- a/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php +++ b/tests/Unit/Domain/Application/User/Service/UserServiceDispatchTest.php @@ -23,6 +23,7 @@ use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; use Phalcon\Cache\Cache; use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use function json_decode; use function ob_get_clean; @@ -33,6 +34,7 @@ #[BackupGlobals(true)] final class UserServiceDispatchTest extends AbstractUnitTestCase { + #[RunInSeparateProcess] public function testDispatchGet(): void { /** @var EnvManager $env */ From bf5e2180c2ecbc625ca7516d1130fe111d8562a3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:00:16 -0600 Subject: [PATCH 277/301] [#.x] - updating handler with transformer --- .../User/Handler/AbstractUserPutPostHandler.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Domain/Application/User/Handler/AbstractUserPutPostHandler.php b/src/Domain/Application/User/Handler/AbstractUserPutPostHandler.php index aa89c39..b8a532a 100644 --- a/src/Domain/Application/User/Handler/AbstractUserPutPostHandler.php +++ b/src/Domain/Application/User/Handler/AbstractUserPutPostHandler.php @@ -15,12 +15,15 @@ use Phalcon\Api\Domain\ADR\Payload; use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; -use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\MapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapperInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\Repository\UserRepositoryInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\User\UserTypes; use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; use Phalcon\Api\Domain\Infrastructure\Encryption\Security; use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Events\ManagerInterface as EventsManagerInterface; use Phalcon\Support\Registry; use function array_filter; @@ -33,14 +36,19 @@ abstract class AbstractUserPutPostHandler implements HandlerInterface { /** * @param ValidatorInterface $validator - * @param MapperInterface $mapper + * @param UserMapperInterface $mapper * @param UserRepositoryInterface $repository + * @param EventsManagerInterface $eventsManager + * @param Transformer $transformer + * @param Registry $registry * @param Security $security */ public function __construct( protected readonly ValidatorInterface $validator, - protected readonly MapperInterface $mapper, + protected readonly UserMapperInterface $mapper, protected readonly UserRepositoryInterface $repository, + protected readonly EventsManagerInterface $eventsManager, + protected readonly Transformer $transformer, protected readonly Registry $registry, private readonly Security $security, ) { From ec85fcfaedeba2532ed39536fe4ea9f793f472c7 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:00:31 -0600 Subject: [PATCH 278/301] [#.x] - new routes for company --- .../Infrastructure/Enums/Http/RoutesEnum.php | 32 +++++++++++-- .../Enums/Http/RoutesEnumTest.php | 47 ++++++++++++++++++- .../Providers/RouterProviderTest.php | 20 ++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php index 48660c4..ea17f0b 100644 --- a/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php +++ b/src/Domain/Infrastructure/Enums/Http/RoutesEnum.php @@ -16,6 +16,11 @@ use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; use Phalcon\Api\Domain\Application\Auth\Service\AuthLogoutPostService; use Phalcon\Api\Domain\Application\Auth\Service\AuthRefreshPostService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyDeleteService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetManyService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPostService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPutService; use Phalcon\Api\Domain\Application\User\Service\UserDeleteService; use Phalcon\Api\Domain\Application\User\Service\UserGetService; use Phalcon\Api\Domain\Application\User\Service\UserPostService; @@ -52,6 +57,12 @@ enum RoutesEnum: int case authLogoutPost = 12; case authRefreshPost = 13; + case companyDelete = 31; + case companyGet = 32; + case companyGetMany = 33; + case companyPost = 34; + case companyPut = 35; + case userDelete = 21; case userGet = 22; case userPost = 23; @@ -74,9 +85,14 @@ public function method(): string self::authLoginPost, self::authLogoutPost, self::authRefreshPost, + self::companyPost, self::userPost => self::POST, + self::companyDelete, self::userDelete => self::DELETE, + self::companyGet, + self::companyGetMany, self::userGet => self::GET, + self::companyPut, self::userPut => self::PUT, }; } @@ -106,6 +122,11 @@ public function prefix(): string self::authLoginPost, self::authLogoutPost, self::authRefreshPost => 'auth', + self::companyDelete, + self::companyGet, + self::companyGetMany, + self::companyPost, + self::companyPut => 'company', self::userDelete, self::userGet, self::userPost, @@ -121,6 +142,11 @@ public function service(): string self::authLoginPost => AuthLoginPostService::class, self::authLogoutPost => AuthLogoutPostService::class, self::authRefreshPost => AuthRefreshPostService::class, + self::companyDelete => CompanyDeleteService::class, + self::companyGet => CompanyGetService::class, + self::companyGetMany => CompanyGetManyService::class, + self::companyPost => CompanyPostService::class, + self::companyPut => CompanyPutService::class, self::userDelete => UserDeleteService::class, self::userGet => UserGetService::class, self::userPost => UserPostService::class, @@ -137,10 +163,8 @@ public function suffix(): string self::authLoginPost => '/login', self::authLogoutPost => '/logout', self::authRefreshPost => '/refresh', - self::userDelete, - self::userGet, - self::userPost, - self::userPut => '', + self::companyGetMany => '/all', + default => '', }; } } diff --git a/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php index 84d1788..6263075 100644 --- a/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php +++ b/tests/Unit/Domain/Infrastructure/Enums/Http/RoutesEnumTest.php @@ -16,6 +16,11 @@ use Phalcon\Api\Domain\Application\Auth\Service\AuthLoginPostService; use Phalcon\Api\Domain\Application\Auth\Service\AuthLogoutPostService; use Phalcon\Api\Domain\Application\Auth\Service\AuthRefreshPostService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyDeleteService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetManyService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPostService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPutService; use Phalcon\Api\Domain\Application\User\Service\UserDeleteService; use Phalcon\Api\Domain\Application\User\Service\UserGetService; use Phalcon\Api\Domain\Application\User\Service\UserPostService; @@ -60,6 +65,46 @@ public static function getExamples(): array RoutesEnum::POST, AuthRefreshPostService::class, ], + [ + RoutesEnum::companyDelete, + '/company', + '', + '/company', + RoutesEnum::DELETE, + CompanyDeleteService::class, + ], + [ + RoutesEnum::companyGet, + '/company', + '', + '/company', + RoutesEnum::GET, + CompanyGetService::class, + ], + [ + RoutesEnum::companyGetMany, + '/company', + '/all', + '/company/all', + RoutesEnum::GET, + CompanyGetManyService::class, + ], + [ + RoutesEnum::companyPost, + '/company', + '', + '/company', + RoutesEnum::POST, + CompanyPostService::class, + ], + [ + RoutesEnum::companyPut, + '/company', + '', + '/company', + RoutesEnum::PUT, + CompanyPutService::class, + ], [ RoutesEnum::userDelete, '/user', @@ -97,7 +142,7 @@ public static function getExamples(): array public function testCheckCount(): void { - $expected = 7; + $expected = 12; $actual = RoutesEnum::cases(); $this->assertCount($expected, $actual); } diff --git a/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php b/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php index d0a993b..8b1dba1 100644 --- a/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php +++ b/tests/Unit/Domain/Infrastructure/Providers/RouterProviderTest.php @@ -44,6 +44,26 @@ public function testCheckRegistration(): void 'method' => 'POST', 'pattern' => '/auth/refresh', ], + [ + 'method' => 'DELETE', + 'pattern' => '/company', + ], + [ + 'method' => 'GET', + 'pattern' => '/company', + ], + [ + 'method' => 'GET', + 'pattern' => '/company/all', + ], + [ + 'method' => 'POST', + 'pattern' => '/company', + ], + [ + 'method' => 'PUT', + 'pattern' => '/company', + ], [ 'method' => 'DELETE', 'pattern' => '/user', From b66a29f4adc9014ed47ae3f15f488f4dd34695c2 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:00:41 -0600 Subject: [PATCH 279/301] [#.x] - new shape for company --- src/Domain/ADR/InputTypes.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Domain/ADR/InputTypes.php b/src/Domain/ADR/InputTypes.php index 43a1471..1f3c666 100644 --- a/src/Domain/ADR/InputTypes.php +++ b/src/Domain/ADR/InputTypes.php @@ -29,6 +29,26 @@ * token?: string * } * + * @phpstan-type TCompanyInput array{ + * id?: int, + * name?: string, + * phone?: string, + * email?: string, + * website?: string, + * addressLine1?: string, + * addressLine2?: string, + * city?: string, + * stateProvince?: string, + * zipCode?: string, + * country?: string, + * createdDate?: string, + * createdUserId?: int, + * updatedDate?: string, + * updatedUserId?: int, + * page?: int, + * perPage?: int + * } + * * @phpstan-type TUserInput array{ * id?: int, * status?: int, From 22dfe064ec37571deda2d19cabc3ad4ec334b192 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:00:56 -0600 Subject: [PATCH 280/301] [#.x] - company di definitions --- .../Container/CompanyDefinitionsEnum.php | 218 +++++++++ .../Container/CompanyDefinitionsEnumTest.php | 431 ++++++++++++++++++ 2 files changed, 649 insertions(+) create mode 100644 src/Domain/Infrastructure/Enums/Container/CompanyDefinitionsEnum.php create mode 100644 tests/Unit/Domain/Infrastructure/Enums/Container/CompanyDefinitionsEnumTest.php diff --git a/src/Domain/Infrastructure/Enums/Container/CompanyDefinitionsEnum.php b/src/Domain/Infrastructure/Enums/Container/CompanyDefinitionsEnum.php new file mode 100644 index 0000000..0f7f973 --- /dev/null +++ b/src/Domain/Infrastructure/Enums/Container/CompanyDefinitionsEnum.php @@ -0,0 +1,218 @@ + + * + * 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\Infrastructure\Enums\Container; + +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Application\Company\Facade\CompanyFacade; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyDeleteHandler; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyGetHandler; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyGetManyHandler; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyPostHandler; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyPutHandler; +use Phalcon\Api\Domain\Application\Company\Service\CompanyDeleteService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetManyService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPostService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPutService; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Sanitizer\CompanySanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Validator\CompanyValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Validator\CompanyValidatorUpdate; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\Filter\Validation; +use Phalcon\Support\Registry; + +/** + * @phpstan-import-type TService from Container + */ +enum CompanyDefinitionsEnum: string implements DefinitionsEnumInterface +{ + case CompanyCommandFactory = CompanyCommandFactory::class; + case CompanyDelete = CompanyDeleteService::class; + case CompanyGet = CompanyGetService::class; + case CompanyGetMany = CompanyGetManyService::class; + case CompanyPost = CompanyPostService::class; + case CompanyPut = CompanyPutService::class; + case CompanyFacade = CompanyFacade::class; + case CompanyDeleteHandler = CompanyDeleteHandler::class; + case CompanyGetHandler = CompanyGetHandler::class; + case CompanyGetManyHandler = CompanyGetManyHandler::class; + case CompanyPostHandler = CompanyPostHandler::class; + case CompanyPutHandler = CompanyPutHandler::class; + case CompanyMapper = CompanyMapper::class; + case CompanyRepository = CompanyRepository::class; + case CompanySanitizer = CompanySanitizer::class; + case CompanyValidator = CompanyValidator::class; + case CompanyValidatorUpdate = CompanyValidatorUpdate::class; + + /** + * @return TService + */ + public function definition(): array + { + return match ($this) { + self::CompanyCommandFactory => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanySanitizer::class, + ], + ], + ], + self::CompanyDelete, + self::CompanyGet, + self::CompanyGetMany, + self::CompanyPost, + self::CompanyPut => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyFacade::class, + ], + ], + ], + self::CompanyFacade => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CommandBus::class, + ], + [ + 'type' => 'service', + 'name' => CompanyCommandFactory::class, + ], + ], + ], + self::CompanyDeleteHandler, + self::CompanyGetHandler, + self::CompanyGetManyHandler => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyRepository::class, + ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], + ], + ], + self::CompanyPostHandler => $this->getServicePutPost( + $this->value, + CompanyValidator::class + ), + self::CompanyPutHandler => $this->getServicePutPost( + $this->value, + CompanyValidatorUpdate::class + ), + self::CompanyMapper => [ + 'className' => $this->value, + ], + self::CompanyRepository => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Connection::class, + ], + [ + 'type' => 'service', + 'name' => CompanyMapper::class, + ], + ], + ], + self::CompanySanitizer => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::FILTER, + ], + ], + ], + self::CompanyValidator, + self::CompanyValidatorUpdate => [ + 'className' => $this->value, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Validation::class, + ], + ], + ], + }; + } + + public function isShared(): bool + { + return match ($this) { + self::CompanyMapper, + self::CompanyRepository, + self::CompanySanitizer => true, + default => false, + }; + } + + /** + * @param class-string $className + * @param class-string $validatorName + * + * @return TService + */ + private function getServicePutPost(string $className, string $validatorName): array + { + return [ + 'className' => $className, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => $validatorName, + ], + [ + 'type' => 'service', + 'name' => CompanyMapper::class, + ], + [ + 'type' => 'service', + 'name' => CompanyRepository::class, + ], + [ + 'type' => 'service', + 'name' => Container::EVENTS_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], + [ + 'type' => 'service', + 'name' => Registry::class, + ], + [ + 'type' => 'service', + 'name' => Security::class, + ], + ], + ]; + } +} diff --git a/tests/Unit/Domain/Infrastructure/Enums/Container/CompanyDefinitionsEnumTest.php b/tests/Unit/Domain/Infrastructure/Enums/Container/CompanyDefinitionsEnumTest.php new file mode 100644 index 0000000..90d908b --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/Enums/Container/CompanyDefinitionsEnumTest.php @@ -0,0 +1,431 @@ + + * + * 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\Infrastructure\Enums\Container; + +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Application\Company\Facade\CompanyFacade; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyDeleteHandler; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyGetHandler; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyGetManyHandler; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyPostHandler; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyPutHandler; +use Phalcon\Api\Domain\Application\Company\Service\CompanyDeleteService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetManyService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPostService; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPutService; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBus; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Sanitizer\CompanySanitizer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Validator\CompanyValidator; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Validator\CompanyValidatorUpdate; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; +use Phalcon\Api\Domain\Infrastructure\Encryption\Security; +use Phalcon\Api\Domain\Infrastructure\Enums\Container\CompanyDefinitionsEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\Filter\Validation; +use Phalcon\Support\Registry; + +final class CompanyDefinitionsEnumTest extends AbstractUnitTestCase +{ + public function testDefinition(): void + { + $expected = [ + 'className' => CompanyCommandFactory::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanySanitizer::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyCommandFactory->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyDeleteService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyFacade::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyDelete->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyGetService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyFacade::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyGet->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyGetManyService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyFacade::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyGetMany->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyPostService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyFacade::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyPost->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyPutService::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyFacade::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyPut->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyFacade::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CommandBus::class, + ], + [ + 'type' => 'service', + 'name' => CompanyCommandFactory::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyFacade->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyDeleteHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyRepository::class, + ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyDeleteHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyGetHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyRepository::class, + ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyGetHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyGetManyHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyRepository::class, + ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyGetManyHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyPostHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyValidator::class, + ], + [ + 'type' => 'service', + 'name' => CompanyMapper::class, + ], + [ + 'type' => 'service', + 'name' => CompanyRepository::class, + ], + [ + 'type' => 'service', + 'name' => Container::EVENTS_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], + [ + 'type' => 'service', + 'name' => Registry::class, + ], + [ + 'type' => 'service', + 'name' => Security::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyPostHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyPutHandler::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => CompanyValidatorUpdate::class, + ], + [ + 'type' => 'service', + 'name' => CompanyMapper::class, + ], + [ + 'type' => 'service', + 'name' => CompanyRepository::class, + ], + [ + 'type' => 'service', + 'name' => Container::EVENTS_MANAGER, + ], + [ + 'type' => 'service', + 'name' => Transformer::class, + ], + [ + 'type' => 'service', + 'name' => Registry::class, + ], + [ + 'type' => 'service', + 'name' => Security::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyPutHandler->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyMapper::class, + ]; + $actual = CompanyDefinitionsEnum::CompanyMapper->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyRepository::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Connection::class, + ], + [ + 'type' => 'service', + 'name' => CompanyMapper::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyRepository->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanySanitizer::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Container::FILTER, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanySanitizer->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyValidator::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Validation::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyValidator->definition(); + $this->assertSame($expected, $actual); + + $expected = [ + 'className' => CompanyValidatorUpdate::class, + 'arguments' => [ + [ + 'type' => 'service', + 'name' => Validation::class, + ], + ], + ]; + $actual = CompanyDefinitionsEnum::CompanyValidatorUpdate->definition(); + $this->assertSame($expected, $actual); + } + + public function testIsShared(): void + { + $actual = CompanyDefinitionsEnum::CompanyCommandFactory->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyDelete->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyGet->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyGetMany->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyPost->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyPut->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyFacade->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyDeleteHandler->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyGetHandler->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyPostHandler->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyPutHandler->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyMapper->isShared(); + $this->assertTrue($actual); + + $actual = CompanyDefinitionsEnum::CompanyRepository->isShared(); + $this->assertTrue($actual); + + $actual = CompanyDefinitionsEnum::CompanySanitizer->isShared(); + $this->assertTrue($actual); + + $actual = CompanyDefinitionsEnum::CompanyValidator->isShared(); + $this->assertFalse($actual); + + $actual = CompanyDefinitionsEnum::CompanyValidatorUpdate->isShared(); + $this->assertFalse($actual); + } + + public function testValue(): void + { + $expected = CompanyCommandFactory::class; + $actual = CompanyDefinitionsEnum::CompanyCommandFactory->value; + $this->assertSame($expected, $actual); + + $expected = CompanyDeleteService::class; + $actual = CompanyDefinitionsEnum::CompanyDelete->value; + $this->assertSame($expected, $actual); + + $expected = CompanyGetService::class; + $actual = CompanyDefinitionsEnum::CompanyGet->value; + $this->assertSame($expected, $actual); + + $expected = CompanyGetManyService::class; + $actual = CompanyDefinitionsEnum::CompanyGetMany->value; + $this->assertSame($expected, $actual); + + $expected = CompanyPostService::class; + $actual = CompanyDefinitionsEnum::CompanyPost->value; + $this->assertSame($expected, $actual); + + $expected = CompanyPutService::class; + $actual = CompanyDefinitionsEnum::CompanyPut->value; + $this->assertSame($expected, $actual); + + $expected = CompanyFacade::class; + $actual = CompanyDefinitionsEnum::CompanyFacade->value; + $this->assertSame($expected, $actual); + + $expected = CompanyDeleteHandler::class; + $actual = CompanyDefinitionsEnum::CompanyDeleteHandler->value; + $this->assertSame($expected, $actual); + + $expected = CompanyGetHandler::class; + $actual = CompanyDefinitionsEnum::CompanyGetHandler->value; + $this->assertSame($expected, $actual); + + $expected = CompanyPostHandler::class; + $actual = CompanyDefinitionsEnum::CompanyPostHandler->value; + $this->assertSame($expected, $actual); + + $expected = CompanyPutHandler::class; + $actual = CompanyDefinitionsEnum::CompanyPutHandler->value; + $this->assertSame($expected, $actual); + + $expected = CompanyMapper::class; + $actual = CompanyDefinitionsEnum::CompanyMapper->value; + $this->assertSame($expected, $actual); + + $expected = CompanyRepository::class; + $actual = CompanyDefinitionsEnum::CompanyRepository->value; + $this->assertSame($expected, $actual); + + $expected = CompanySanitizer::class; + $actual = CompanyDefinitionsEnum::CompanySanitizer->value; + $this->assertSame($expected, $actual); + + $expected = CompanyValidator::class; + $actual = CompanyDefinitionsEnum::CompanyValidator->value; + $this->assertSame($expected, $actual); + + $expected = CompanyValidatorUpdate::class; + $actual = CompanyDefinitionsEnum::CompanyValidatorUpdate->value; + $this->assertSame($expected, $actual); + } +} From 6b87a4cb04b11d97099761467a322b255965f25b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:01:12 -0600 Subject: [PATCH 281/301] [#.x] - new auth enum tests --- .../Auth/Sanitizer/AuthSanitizerEnumTest.php | 52 +++++++++++++++++++ .../Validator/AuthLoginValidatorEnumTest.php | 50 ++++++++++++++++++ .../Validator/AuthTokenValidatorEnumTest.php | 38 ++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorEnumTest.php diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerEnumTest.php new file mode 100644 index 0000000..c3da9e5 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Sanitizer/AuthSanitizerEnumTest.php @@ -0,0 +1,52 @@ + + * + * 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\Infrastructure\DataSource\Auth\Sanitizer; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Sanitizer\AuthSanitizerEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class AuthSanitizerEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 3; + $actual = AuthSanitizerEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = Filter::FILTER_EMAIL; + $actual = AuthSanitizerEnum::email->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = AuthSanitizerEnum::email->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = AuthSanitizerEnum::password->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = AuthSanitizerEnum::password->default(); + $this->assertSame($expected, $actual); + + $expected = ''; + $actual = AuthSanitizerEnum::token->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = AuthSanitizerEnum::token->default(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorEnumTest.php new file mode 100644 index 0000000..28ec6a2 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthLoginValidatorEnumTest.php @@ -0,0 +1,50 @@ + + * + * 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\Infrastructure\DataSource\Auth\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthLoginValidatorEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class AuthLoginValidatorEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 2; + $actual = AuthLoginValidatorEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + Email::class, + ]; + $actual = AuthLoginValidatorEnum::email->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = AuthLoginValidatorEnum::email->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = AuthLoginValidatorEnum::password->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = AuthLoginValidatorEnum::password->allowEmpty(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorEnumTest.php new file mode 100644 index 0000000..969b3a5 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Auth/Validator/AuthTokenValidatorEnumTest.php @@ -0,0 +1,38 @@ + + * + * 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\Infrastructure\DataSource\Auth\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Auth\Validator\AuthTokenValidatorEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class AuthTokenValidatorEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 1; + $actual = AuthTokenValidatorEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = AuthTokenValidatorEnum::token->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = AuthTokenValidatorEnum::token->allowEmpty(); + $this->assertSame($expected, $actual); + } +} From 679eff27da27177d412625f29382b16ca1c8e3b9 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:01:25 -0600 Subject: [PATCH 282/301] [#.x] - new company collection --- .../DataSource/AbstractCollection.php | 68 +++++++++++++++++++ .../Company/DTO/CompanyCollection.php | 23 +++++++ 2 files changed, 91 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/AbstractCollection.php create mode 100644 src/Domain/Infrastructure/DataSource/Company/DTO/CompanyCollection.php diff --git a/src/Domain/Infrastructure/DataSource/AbstractCollection.php b/src/Domain/Infrastructure/DataSource/AbstractCollection.php new file mode 100644 index 0000000..ebf0b2f --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/AbstractCollection.php @@ -0,0 +1,68 @@ + + * + * 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\Infrastructure\DataSource; + +use Countable; +use Generator; +use IteratorAggregate; + +use function count; + +/** + * Collection for domain objects + * + * @template T of object + * @implements IteratorAggregate + */ +abstract class AbstractCollection implements IteratorAggregate, Countable +{ + /** @var array */ + private array $items = []; + + /** + * Add an item to the collection. + * + * @param T&object{id:int} $item + */ + public function add(object $item): void + { + $this->items[$item->id] = $item; + } + + /** + * @return int + */ + public function count(): int + { + return count($this->items); + } + + /** + * @return Generator + */ + public function getIterator(): Generator + { + foreach ($this->items as $key => $item) { + yield $key => $item; + } + } + + /** + * @return T[] + */ + public function toArray(): array + { + return $this->items; + } +} diff --git a/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyCollection.php b/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyCollection.php new file mode 100644 index 0000000..a85a821 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyCollection.php @@ -0,0 +1,23 @@ + + * + * 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\Infrastructure\DataSource\Company\DTO; + +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractCollection; + +/** + * @extends AbstractCollection + */ +final class CompanyCollection extends AbstractCollection +{ +} From b25cc3c820ec86938416875bcbf2dfd78edc429b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:02:41 -0600 Subject: [PATCH 283/301] [#.x] - new company dtos --- .../DataSource/Company/DTO/Company.php | 38 ++++++ .../DataSource/Company/DTO/CompanyInput.php | 99 +++++++++++++++ .../Company/DTO/CompanyInputTest.php | 114 ++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Company/DTO/Company.php create mode 100644 src/Domain/Infrastructure/DataSource/Company/DTO/CompanyInput.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/DTO/CompanyInputTest.php diff --git a/src/Domain/Infrastructure/DataSource/Company/DTO/Company.php b/src/Domain/Infrastructure/DataSource/Company/DTO/Company.php new file mode 100644 index 0000000..560ca08 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/DTO/Company.php @@ -0,0 +1,38 @@ + + * + * 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\Infrastructure\DataSource\Company\DTO; + +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractValueObject; + +final class Company extends AbstractValueObject +{ + public function __construct( + public readonly int $id, + public readonly ?string $name, + public readonly ?string $phone, + public readonly ?string $email, + public readonly ?string $website, + public readonly ?string $addressLine1, + public readonly ?string $addressLine2, + public readonly ?string $city, + public readonly ?string $stateProvince, + public readonly ?string $zipCode, + public readonly ?string $country, + public readonly ?string $createdDate, + public readonly ?int $createdUserId, + public readonly ?string $updatedDate, + public readonly ?int $updatedUserId, + ) { + } +} diff --git a/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyInput.php b/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyInput.php new file mode 100644 index 0000000..71f0f5c --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyInput.php @@ -0,0 +1,99 @@ + + * + * 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\Infrastructure\DataSource\Company\DTO; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractInputValueObject; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; + +use function abs; + +/** + * Value object for input + * + * @phpstan-import-type TCompanyInput from InputTypes + * @phpstan-import-type TCompany from CompanyTypes + */ +final class CompanyInput extends AbstractInputValueObject +{ + /** + * @param int $id + * @param string|null $name + * @param string|null $phone + * @param string|null $email + * @param string|null $website + * @param string|null $addressLine1 + * @param string|null $addressLine2 + * @param string|null $city + * @param string|null $stateProvince + * @param string|null $zipCode + * @param string|null $country + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + * @param int $page + * @param int $perPage + */ + public function __construct( + public readonly int $id, + public readonly ?string $name, + public readonly ?string $phone, + public readonly ?string $email, + public readonly ?string $website, + public readonly ?string $addressLine1, + public readonly ?string $addressLine2, + public readonly ?string $city, + public readonly ?string $stateProvince, + public readonly ?string $zipCode, + public readonly ?string $country, + public readonly ?string $createdDate, + public readonly ?int $createdUserId, + public readonly ?string $updatedDate, + public readonly ?int $updatedUserId, + public readonly int $page, + public readonly int $perPage, + ) { + } + + /** + * Build the concrete DTO from a sanitized array. + * + * @param TCompanyInput $sanitized + * + * @return static + */ + protected static function fromArray(array $sanitized): static + { + return new self( + abs(isset($sanitized['id']) ? (int)$sanitized['id'] : 0), + isset($sanitized['name']) ? (string)$sanitized['name'] : null, + isset($sanitized['phone']) ? (string)$sanitized['phone'] : null, + isset($sanitized['email']) ? (string)$sanitized['email'] : null, + isset($sanitized['website']) ? (string)$sanitized['website'] : null, + isset($sanitized['addressLine1']) ? (string)$sanitized['addressLine1'] : null, + isset($sanitized['addressLine2']) ? (string)$sanitized['addressLine2'] : null, + isset($sanitized['city']) ? (string)$sanitized['city'] : null, + isset($sanitized['stateProvince']) ? (string)$sanitized['stateProvince'] : null, + isset($sanitized['zipCode']) ? (string)$sanitized['zipCode'] : null, + isset($sanitized['country']) ? (string)$sanitized['country'] : null, + isset($sanitized['createdDate']) ? (string)$sanitized['createdDate'] : null, + abs(isset($sanitized['createdUserId']) ? (int)$sanitized['createdUserId'] : 0), + isset($sanitized['updatedDate']) ? (string)$sanitized['updatedDate'] : null, + abs(isset($sanitized['updatedUserId']) ? (int)$sanitized['updatedUserId'] : 0), + abs(isset($sanitized['page']) ? (int)$sanitized['page'] : 1), + abs(isset($sanitized['perPage']) ? (int)$sanitized['perPage'] : 10) + ); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/DTO/CompanyInputTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/DTO/CompanyInputTest.php new file mode 100644 index 0000000..7ced16a --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/DTO/CompanyInputTest.php @@ -0,0 +1,114 @@ + + * + * 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\Infrastructure\DataSource\Company\DTO; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Sanitizer\CompanySanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class CompanyInputTest extends AbstractUnitTestCase +{ + public function testToArray(): void + { + /** @var CompanySanitizer $sanitizer */ + $sanitizer = $this->container->get(CompanySanitizer::class); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'name' => $faker->company(), + 'email' => $faker->safeEmail(), + 'phone' => $faker->phoneNumber(), + 'website' => $faker->domainName(), + 'addressLine1' => $faker->streetAddress(), + 'addressLine2' => $faker->streetAddress(), + 'city' => $faker->city(), + 'stateProvince' => $faker->city(), + 'zipCode' => $faker->postcode(), + 'country' => $faker->countryISOAlpha3(), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), + ]; + + $sanitized = $sanitizer->sanitize($input); + $companyInput = CompanyInput::new($sanitizer, $input); + + $expected = $sanitized['id']; + $actual = $companyInput->id; + $this->assertSame($expected, $actual); + + $expected = $sanitized['name']; + $actual = $companyInput->name; + $this->assertSame($expected, $actual); + + $expected = $sanitized['email']; + $actual = $companyInput->email; + $this->assertSame($expected, $actual); + + $expected = $sanitized['phone']; + $actual = $companyInput->phone; + $this->assertSame($expected, $actual); + + $expected = $sanitized['website']; + $actual = $companyInput->website; + $this->assertSame($expected, $actual); + + $expected = $sanitized['addressLine1']; + $actual = $companyInput->addressLine1; + $this->assertSame($expected, $actual); + + $expected = $sanitized['addressLine2']; + $actual = $companyInput->addressLine2; + $this->assertSame($expected, $actual); + + $expected = $sanitized['city']; + $actual = $companyInput->city; + $this->assertSame($expected, $actual); + + $expected = $sanitized['stateProvince']; + $actual = $companyInput->stateProvince; + $this->assertSame($expected, $actual); + + $expected = $sanitized['zipCode']; + $actual = $companyInput->zipCode; + $this->assertSame($expected, $actual); + + $expected = $sanitized['country']; + $actual = $companyInput->country; + $this->assertSame($expected, $actual); + + $expected = $sanitized['createdDate']; + $actual = $companyInput->createdDate; + $this->assertSame($expected, $actual); + + $expected = $sanitized['createdUserId']; + $actual = $companyInput->createdUserId; + $this->assertSame($expected, $actual); + + $expected = $sanitized['updatedDate']; + $actual = $companyInput->updatedDate; + $this->assertSame($expected, $actual); + + $expected = $sanitized['updatedUserId']; + $actual = $companyInput->updatedUserId; + $this->assertSame($expected, $actual); + + $expected = get_object_vars($companyInput); + $actual = $companyInput->toArray(); + $this->assertSame($expected, $actual); + } +} From 460b2d8db1a3d22be2af0215692d291d211f7a0e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:03:15 -0600 Subject: [PATCH 284/301] [#.x] - new sanitizers, mapper and validators --- .../Company/Mapper/CompanyMapper.php | 99 +++++++++++ .../Company/Mapper/CompanyMapperInterface.php | 52 ++++++ .../Company/Sanitizer/CompanySanitizer.php | 21 +++ .../Sanitizer/CompanySanitizerEnum.php | 63 +++++++ .../Company/Validator/CompanyInsertEnum.php | 40 +++++ .../Sanitizer/CompanySanitizerEnumTest.php | 164 ++++++++++++++++++ .../Sanitizer/CompanySanitizerTest.php | 100 +++++++++++ .../Validator/CompanyInsertEnumTest.php | 50 ++++++ 8 files changed, 589 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapper.php create mode 100644 src/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapperInterface.php create mode 100644 src/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizer.php create mode 100644 src/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerEnum.php create mode 100644 src/Domain/Infrastructure/DataSource/Company/Validator/CompanyInsertEnum.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerEnumTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerTest.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyInsertEnumTest.php diff --git a/src/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapper.php b/src/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapper.php new file mode 100644 index 0000000..ba57b38 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapper.php @@ -0,0 +1,99 @@ + + * + * 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\Infrastructure\DataSource\Company\Mapper; + +use Phalcon\Api\Domain\Application\Company\Command\CompanyPutCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCollection; + +/** + * @phpstan-import-type TCompany from CompanyTypes + * @phpstan-import-type TCompanyDbRecord from CompanyTypes + * @phpstan-import-type TCompanyDomainToDbRecord from CompanyTypes + */ +final class CompanyMapper implements CompanyMapperInterface +{ + /** + * Map Domain Company -> DB row (usr_*) + * + * @return TCompanyDomainToDbRecord + */ + public function db(CommandInterface $company): array + { + /** @var CompanyPutCommand $company */ + return [ + 'com_id' => $company->id, + 'com_name' => $company->name, + 'com_phone' => $company->phone, + 'com_email' => $company->email, + 'com_website' => $company->website, + 'com_address_line_1' => $company->addressLine1, + 'com_address_line_2' => $company->addressLine2, + 'com_city' => $company->city, + 'com_state_province' => $company->stateProvince, + 'com_zip_code' => $company->zipCode, + 'com_country' => $company->country, + 'com_created_date' => $company->createdDate, + 'com_created_usr_id' => $company->createdUserId, + 'com_updated_date' => $company->updatedDate, + 'com_updated_usr_id' => $company->updatedUserId, + ]; + } + + /** + * Map DB row (usr_*) -> Domain Company + * + * @param TCompanyDbRecord|array{} $row + */ + public function domain(array $row): Company + { + return new Company( + (int)($row['com_id'] ?? 0), + $row['com_name'] ?? null, + $row['com_phone'] ?? null, + $row['com_email'] ?? null, + $row['com_website'] ?? null, + $row['com_address_line_1'] ?? null, + $row['com_address_line_2'] ?? null, + $row['com_city'] ?? null, + $row['com_state_province'] ?? null, + $row['com_zip_code'] ?? null, + $row['com_country'] ?? null, + $row['com_created_date'] ?? null, + isset($row['com_created_usr_id']) ? (int)$row['com_created_usr_id'] : null, + $row['com_updated_date'] ?? null, + isset($row['com_updated_usr_id']) ? (int)$row['com_updated_usr_id'] : null, + ); + } + + /** + * Map DB resultset -> Domain CompanyCollection + * + * @param TCompanyDbRecord[]|array{} $rows + */ + public function domainRows(array $rows): CompanyCollection + { + /** + * Return the DTO collection back + */ + $collection = new CompanyCollection(); + foreach ($rows as $row) { + $collection->add($this->domain($row)); + } + + return $collection; + } +} diff --git a/src/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapperInterface.php b/src/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapperInterface.php new file mode 100644 index 0000000..9d5130f --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapperInterface.php @@ -0,0 +1,52 @@ + + * + * 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\Infrastructure\DataSource\Company\Mapper; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCollection; + +/** + * Contract for mapping between domain DTO/objects and persistence arrays. + * + * @phpstan-import-type TCompany from CompanyTypes + * @phpstan-import-type TCompanyDbRecord from CompanyTypes + * @phpstan-import-type TCompanyDomainToDbRecord from CompanyTypes + */ +interface CompanyMapperInterface +{ + /** + * Map Domain User -> DB row (usr_*) + * + * @param CommandInterface $company + * + * @return TCompanyDomainToDbRecord + */ + public function db(CommandInterface $company): array; + + /** + * Map DB row (usr_*) -> Domain User + * + * @param TCompanyDbRecord|array{} $row + */ + public function domain(array $row): Company; + + /** + * Map DB resultset -> Domain CompanyCollection + * + * @param TCompanyDbRecord[]|array{} $rows + */ + public function domainRows(array $rows): CompanyCollection; +} diff --git a/src/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizer.php b/src/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizer.php new file mode 100644 index 0000000..1489a50 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizer.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\Infrastructure\DataSource\Company\Sanitizer; + +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractSanitizer; + +final class CompanySanitizer extends AbstractSanitizer +{ + protected string $enum = CompanySanitizerEnum::class; +} diff --git a/src/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerEnum.php b/src/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerEnum.php new file mode 100644 index 0000000..366258b --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerEnum.php @@ -0,0 +1,63 @@ + + * + * 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\Infrastructure\DataSource\Company\Sanitizer; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerEnumInterface; +use Phalcon\Filter\Filter; + +enum CompanySanitizerEnum implements SanitizerEnumInterface +{ + case id; + case name; + case phone; + case email; + case website; + case addressLine1; + case addressLine2; + case city; + case stateProvince; + case zipCode; + case country; + case createdDate; + case createdUserId; + case updatedDate; + case updatedUserId; + case page; + case perPage; + + public function default(): mixed + { + return match ($this) { + self::id, + self::createdUserId, + self::updatedUserId => 0, + self::page => 1, + self::perPage => 10, + default => null + }; + } + + public function sanitizer(): string + { + return match ($this) { + self::id, + self::createdUserId, + self::updatedUserId, + self::page, + self::perPage => Filter::FILTER_ABSINT, + self::email => Filter::FILTER_EMAIL, + default => Filter::FILTER_STRIPTAGS + }; + } +} diff --git a/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyInsertEnum.php b/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyInsertEnum.php new file mode 100644 index 0000000..9a13ae8 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyInsertEnum.php @@ -0,0 +1,40 @@ + + * + * 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\Infrastructure\DataSource\Company\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValidatorEnumInterface; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum CompanyInsertEnum implements ValidatorEnumInterface +{ + case name; + case email; + + public function allowEmpty(): bool + { + return false; + } + + public function validators(): array + { + return match ($this) { + self::email => [ + PresenceOf::class, + Email::class, + ], + default => [PresenceOf::class], + }; + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerEnumTest.php new file mode 100644 index 0000000..e642f3b --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerEnumTest.php @@ -0,0 +1,164 @@ + + * + * 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\Infrastructure\DataSource\Company\Sanitizer; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Sanitizer\CompanySanitizerEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class CompanySanitizerEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 17; + $actual = CompanySanitizerEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = CompanySanitizerEnum::id->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = CompanySanitizerEnum::id->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::name->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::name->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::phone->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::phone->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_EMAIL; + $actual = CompanySanitizerEnum::email->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::email->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::website->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::website->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::addressLine1->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::addressLine1->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::addressLine2->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::addressLine2->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::city->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::city->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::stateProvince->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::stateProvince->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::zipCode->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::zipCode->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::country->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::country->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::createdDate->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::createdDate->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = CompanySanitizerEnum::createdUserId->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = CompanySanitizerEnum::createdUserId->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_STRIPTAGS; + $actual = CompanySanitizerEnum::updatedDate->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = null; + $actual = CompanySanitizerEnum::updatedDate->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = CompanySanitizerEnum::updatedUserId->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = CompanySanitizerEnum::updatedUserId->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = CompanySanitizerEnum::page->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 1; + $actual = CompanySanitizerEnum::page->default(); + $this->assertSame($expected, $actual); + + $expected = Filter::FILTER_ABSINT; + $actual = CompanySanitizerEnum::perPage->sanitizer(); + $this->assertSame($expected, $actual); + + $expected = 10; + $actual = CompanySanitizerEnum::perPage->default(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerTest.php new file mode 100644 index 0000000..d55dd5a --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/Sanitizer/CompanySanitizerTest.php @@ -0,0 +1,100 @@ + + * + * 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\Infrastructure\DataSource\Company\Sanitizer; + +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Sanitizer\CompanySanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class CompanySanitizerTest extends AbstractUnitTestCase +{ + public function testEmpty(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new CompanySanitizer($filter); + + $expected = [ + 'id' => 0, + 'name' => null, + 'phone' => null, + 'email' => null, + 'website' => null, + 'addressLine1' => null, + 'addressLine2' => null, + 'city' => null, + 'stateProvince' => null, + 'zipCode' => null, + 'country' => null, + 'createdDate' => null, + 'createdUserId' => 0, + 'updatedDate' => null, + 'updatedUserId' => 0, + 'page' => 1, + 'perPage' => 10, + ]; + $actual = $sanitizer->sanitize([]); + $this->assertSame($expected, $actual); + } + + public function testObject(): void + { + /** @var Filter $filter */ + $filter = $this->container->getShared(Container::FILTER); + $sanitizer = new CompanySanitizer($filter); + + $companyData = [ + 'id' => '123', + 'name' => 'some ', + 'phone' => 'some ', + 'email' => 'John.Doe (newsletter) +spam@example.COM', + 'website' => 'some ', + 'addressLine1' => 'some ', + 'addressLine2' => 'some ', + 'city' => 'some ', + 'stateProvince' => 'some ', + 'zipCode' => 'some ', + 'country' => 'some ', + 'createdDate' => 'some ', + 'createdUserId' => '123', + 'updatedDate' => null, + 'updatedUserId' => '123', + 'page' => '1', + 'perPage' => '10', + ]; + + $expected = [ + 'id' => 123, + 'name' => 'some ', + 'phone' => 'some ', + 'email' => 'John.Doenewsletter+spam@example.COM', + 'website' => 'some ', + 'addressLine1' => 'some ', + 'addressLine2' => 'some ', + 'city' => 'some ', + 'stateProvince' => 'some ', + 'zipCode' => 'some ', + 'country' => 'some ', + 'createdDate' => 'some ', + 'createdUserId' => 123, + 'updatedDate' => null, + 'updatedUserId' => 123, + 'page' => 1, + 'perPage' => 10, + ]; + $actual = $sanitizer->sanitize($companyData); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyInsertEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyInsertEnumTest.php new file mode 100644 index 0000000..185e4cf --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyInsertEnumTest.php @@ -0,0 +1,50 @@ + + * + * 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\Infrastructure\DataSource\Company\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Validator\CompanyInsertEnum; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class CompanyInsertEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 2; + $actual = CompanyInsertEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + Email::class, + ]; + $actual = CompanyInsertEnum::email->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = CompanyInsertEnum::email->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + PresenceOf::class, + ]; + $actual = CompanyInsertEnum::name->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = CompanyInsertEnum::name->allowEmpty(); + $this->assertSame($expected, $actual); + } +} From 2d6603bad787f459ed13f54b381a7710e461c08f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:03:29 -0600 Subject: [PATCH 285/301] [#.x] - new company validators --- .../Validator/CompanyValidatorUpdate.php | 21 +++++ .../Validator/CompanyValidatorUpdateTest.php | 82 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorUpdate.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorUpdateTest.php diff --git a/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorUpdate.php b/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorUpdate.php new file mode 100644 index 0000000..247b072 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorUpdate.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\Infrastructure\DataSource\Company\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; + +final class CompanyValidatorUpdate extends AbstractValidator +{ + protected string $fields = CompanyUpdateEnum::class; +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorUpdateTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorUpdateTest.php new file mode 100644 index 0000000..fb8cd87 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorUpdateTest.php @@ -0,0 +1,82 @@ + + * + * 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\Infrastructure\DataSource\Company\Validator; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Validator\CompanyValidatorUpdate; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class CompanyValidatorUpdateTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + /** @var CompanyValidatorUpdate $validator */ + $validator = $this->container->get(CompanyValidatorUpdate::class); + + $input = []; + $companyInput = $factory->update($input); + + $result = $validator->validate($companyInput); + $actual = $result->getErrors(); + + $expected = [ + ['Field id is not a valid absolute integer and greater than 0'], + ]; + + $this->assertSame($expected, $actual); + + $input = [ + 'id' => 1, + 'email' => 'not-email', + ]; + $companyInput = $factory->update($input); + + /** @var CompanyValidatorUpdate $validator */ + $validator = $this->container->get(CompanyValidatorUpdate::class); + + $result = $validator->validate($companyInput); + $actual = $result->getErrors(); + + $expected = [ + ['Field email must be an email address'], + ]; + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + /** @var CompanyValidatorUpdate $validator */ + $validator = $this->container->get(CompanyValidatorUpdate::class); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 100), + 'name' => $faker->company(), + 'email' => $faker->safeEmail(), + ]; + + $companyInput = $factory->update($input); + + $result = $validator->validate($companyInput); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} From cee5d53ff34fd3ffaff4a91f9a373a1ef7452eee Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:03:35 -0600 Subject: [PATCH 286/301] [#.x] - new company validators --- .../Company/Validator/CompanyValidator.php | 21 ++++++ .../Validator/CompanyValidatorTest.php | 66 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidator.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorTest.php diff --git a/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidator.php b/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidator.php new file mode 100644 index 0000000..0d8117c --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidator.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\Infrastructure\DataSource\Company\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbstractValidator; + +final class CompanyValidator extends AbstractValidator +{ + protected string $fields = CompanyInsertEnum::class; +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorTest.php new file mode 100644 index 0000000..5650a51 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyValidatorTest.php @@ -0,0 +1,66 @@ + + * + * 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\Infrastructure\DataSource\Company\Validator; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Validator\CompanyValidator; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class CompanyValidatorTest extends AbstractUnitTestCase +{ + public function testError(): void + { + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + /** @var CompanyValidator $validator */ + $validator = $this->container->get(CompanyValidator::class); + + $input = []; + $companyInput = $factory->insert($input); + + $result = $validator->validate($companyInput); + $actual = $result->getErrors(); + + $expected = [ + ['Field name is required'], + ['Field email is required'], + ['Field email must be an email address'], + ]; + + $this->assertSame($expected, $actual); + } + + public function testSuccess(): void + { + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + /** @var CompanyValidator $validator */ + $validator = $this->container->get(CompanyValidator::class); + $faker = FakerFactory::create(); + + $input = [ + 'name' => $faker->company(), + 'email' => $faker->safeEmail(), + ]; + + $companyInput = $factory->insert($input); + + $result = $validator->validate($companyInput); + $actual = $result->getErrors(); + + $expected = []; + $this->assertSame($expected, $actual); + } +} From de6ea2728ebd69de1c525d88f168532c49da5e09 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:03:46 -0600 Subject: [PATCH 287/301] [#.x] - new company types for stan --- .../DataSource/Company/CompanyTypes.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Company/CompanyTypes.php diff --git a/src/Domain/Infrastructure/DataSource/Company/CompanyTypes.php b/src/Domain/Infrastructure/DataSource/Company/CompanyTypes.php new file mode 100644 index 0000000..2a1f22d --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/CompanyTypes.php @@ -0,0 +1,96 @@ + + * + * 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\Infrastructure\DataSource\Company; + +/** + * @phpstan-type TCompanyDbRecord array{ + * com_id: int, + * com_name: string, + * com_phone: string, + * com_email: string, + * com_website: string, + * com_address_line_1: string, + * com_address_line_2: string, + * com_city: string, + * com_state_province: string, + * com_zip_code: string, + * com_country: string, + * com_created_date: string, + * com_created_usr_id: int, + * com_updated_date: string, + * com_updated_usr_id: int + * } + * + * @phpstan-type TCompanyDomainToDbRecord array{ + * com_id: int, + * com_name: ?string, + * com_phone: ?string, + * com_email: ?string, + * com_website: ?string, + * com_address_line_1: ?string, + * com_address_line_2: ?string, + * com_city: ?string, + * com_state_province: ?string, + * com_zip_code: ?string, + * com_country: ?string, + * com_created_date: ?string, + * com_created_usr_id: ?int, + * com_updated_date: ?string, + * com_updated_usr_id: ?int + * } + * + * @phpstan-type TCompanyDbRecordOptional array{ + * com_id?: int, + * com_name?: ?string, + * com_phone?: ?string, + * com_email?: ?string, + * com_website?: ?string, + * com_address_line_1?: ?string, + * com_address_line_2?: ?string, + * com_city?: ?string, + * com_state_province?: ?string, + * com_zip_code?: ?string, + * com_country?: ?string, + * com_created_date?: ?string, + * com_created_usr_id?: ?int, + * com_updated_date?: ?string, + * com_updated_usr_id?: ?int + * } + * + * @phpstan-type TCompany array{ + * id: int, + * name: ?string, + * phone: ?string, + * email: ?string, + * website: ?string, + * addressLine1: ?string, + * addressLine2: ?string, + * city: ?string, + * stateProvince: ?string, + * zipCode: ?string, + * country: ?string, + * createdDate: ?string, + * createdUserId: ?int, + * updatedDate: ?string, + * updatedUserId: ?int + * } + * + * @phpstan-type TCompanyRecord array{}|TCompanyDbRecord + * @phpstan-type TCompanyRecords array{}|TCompanyDbRecord[] + * + * @phpstan-type TCriteria array + */ +final class CompanyTypes +{ +} From 294cae9d4c03fa7df1754bc5a2e78543913b3663 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:04:04 -0600 Subject: [PATCH 288/301] [#.x] - new company command factory --- .../Company/Command/CompanyCommandFactory.php | 172 +++++++++++ .../CompanyCommandFactoryInterface.php | 57 ++++ .../Command/CompanyCommandFactoryTest.php | 277 ++++++++++++++++++ 3 files changed, 506 insertions(+) create mode 100644 src/Domain/Application/Company/Command/CompanyCommandFactory.php create mode 100644 src/Domain/Application/Company/Command/CompanyCommandFactoryInterface.php create mode 100644 tests/Unit/Domain/Application/Company/Command/CompanyCommandFactoryTest.php diff --git a/src/Domain/Application/Company/Command/CompanyCommandFactory.php b/src/Domain/Application/Company/Command/CompanyCommandFactory.php new file mode 100644 index 0000000..b8defa2 --- /dev/null +++ b/src/Domain/Application/Company/Command/CompanyCommandFactory.php @@ -0,0 +1,172 @@ + + * + * 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\Application\Company\Command; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyInput; +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\SanitizerInterface; + +/** + * @phpstan-import-type TCompanyInput from InputTypes + */ +final class CompanyCommandFactory implements CompanyCommandFactoryInterface +{ + /** + * @param SanitizerInterface $sanitizer + */ + public function __construct( + private readonly SanitizerInterface $sanitizer + ) { + } + + /** + * @param TCompanyInput $input + * + * @return CompanyDeleteCommand + */ + public function delete(array $input): CompanyDeleteCommand + { + /** + * Sanitize the input + */ + $dto = CompanyInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new CompanyDeleteCommand($dto->id); + } + + /** + * @param TCompanyInput $input + * + * @return CompanyGetCommand + */ + public function get(array $input): CompanyGetCommand + { + /** + * Sanitize the input + */ + $dto = CompanyInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new CompanyGetCommand($dto->id); + } + + /** + * @param TCompanyInput $input + * + * @return CompanyGetManyCommand + */ + public function getMany(array $input): CompanyGetManyCommand + { + /** + * Sanitize the input + */ + $dto = CompanyInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new CompanyGetManyCommand( + $dto->id, + $dto->name, + $dto->phone, + $dto->email, + $dto->website, + $dto->addressLine1, + $dto->addressLine2, + $dto->city, + $dto->stateProvince, + $dto->zipCode, + $dto->country, + $dto->createdDate, + $dto->createdUserId, + $dto->updatedDate, + $dto->updatedUserId, + $dto->page, + $dto->perPage, + ); + } + + /** + * @param TCompanyInput $input + * + * @return CompanyPostCommand + */ + public function insert(array $input): CompanyPostCommand + { + /** + * Sanitize the input + */ + $dto = CompanyInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new CompanyPostCommand( + null, + $dto->name, + $dto->phone, + $dto->email, + $dto->website, + $dto->addressLine1, + $dto->addressLine2, + $dto->city, + $dto->stateProvince, + $dto->zipCode, + $dto->country, + $dto->createdDate, + $dto->createdUserId, + $dto->updatedDate, + $dto->updatedUserId, + ); + } + + /** + * @param TCompanyInput $input + * + * @return CompanyPutCommand + */ + public function update(array $input): CompanyPutCommand + { + /** + * Sanitize the input + */ + $dto = CompanyInput::new($this->sanitizer, $input); + + /** + * Return the DTO back + */ + return new CompanyPutCommand( + $dto->id, + $dto->name, + $dto->phone, + $dto->email, + $dto->website, + $dto->addressLine1, + $dto->addressLine2, + $dto->city, + $dto->stateProvince, + $dto->zipCode, + $dto->country, + $dto->createdDate, + $dto->createdUserId, + $dto->updatedDate, + $dto->updatedUserId, + ); + } +} diff --git a/src/Domain/Application/Company/Command/CompanyCommandFactoryInterface.php b/src/Domain/Application/Company/Command/CompanyCommandFactoryInterface.php new file mode 100644 index 0000000..985a0f7 --- /dev/null +++ b/src/Domain/Application/Company/Command/CompanyCommandFactoryInterface.php @@ -0,0 +1,57 @@ + + * + * 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\Application\Company\Command; + +use Phalcon\Api\Domain\ADR\InputTypes; + +/** + * @phpstan-import-type TCompanyInput from InputTypes + */ +interface CompanyCommandFactoryInterface +{ + /** + * @param TCompanyInput $input + * + * @return CompanyDeleteCommand + */ + public function delete(array $input): CompanyDeleteCommand; + + /** + * @param TCompanyInput $input + * + * @return CompanyGetCommand + */ + public function get(array $input): CompanyGetCommand; + + /** + * @param TCompanyInput $input + * + * @return CompanyGetManyCommand + */ + public function getMany(array $input): CompanyGetManyCommand; + + /** + * @param TCompanyInput $input + * + * @return CompanyPostCommand + */ + public function insert(array $input): CompanyPostCommand; + + /** + * @param TCompanyInput $input + * + * @return CompanyPutCommand + */ + public function update(array $input): CompanyPutCommand; +} diff --git a/tests/Unit/Domain/Application/Company/Command/CompanyCommandFactoryTest.php b/tests/Unit/Domain/Application/Company/Command/CompanyCommandFactoryTest.php new file mode 100644 index 0000000..2997bcb --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Command/CompanyCommandFactoryTest.php @@ -0,0 +1,277 @@ + + * + * 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\Application\Company\Command; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Application\Company\Command\CompanyDeleteCommand; +use Phalcon\Api\Domain\Application\Company\Command\CompanyGetCommand; +use Phalcon\Api\Domain\Application\Company\Command\CompanyPostCommand; +use Phalcon\Api\Domain\Application\Company\Command\CompanyPutCommand; +use Phalcon\Api\Domain\Infrastructure\Container; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Sanitizer\CompanySanitizer; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Filter; + +final class CompanyCommandFactoryTest extends AbstractUnitTestCase +{ + public function testDelete(): void + { + $sanitizer = $this->container->get(CompanySanitizer::class); + $factory = new CompanyCommandFactory($sanitizer); + $faker = FakerFactory::create(); + + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'name' => $faker->company(), + 'email' => " Foo.Bar+tag@Example.COM ", + 'phone' => $faker->phoneNumber(), + 'website' => $faker->domainName(), + 'addressLine1' => $faker->streetAddress(), + 'addressLine2' => $faker->streetAddress(), + 'city' => $faker->city(), + 'stateProvince' => $faker->city(), + 'zipCode' => $faker->postcode(), + 'country' => $faker->countryISOAlpha3(), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), + ]; + + /** @var CompanyDeleteCommand $command */ + $command = $factory->delete($input); + + $this->assertInstanceOf(CompanyDeleteCommand::class, $command); + + $expected = $input['id']; + $actual = $command->id; + $this->assertSame($expected, $actual); + } + + public function testGet(): void + { + $sanitizer = $this->container->get(CompanySanitizer::class); + $factory = new CompanyCommandFactory($sanitizer); + $faker = FakerFactory::create(); + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'name' => $faker->company(), + 'email' => " Foo.Bar+tag@Example.COM ", + 'phone' => $faker->phoneNumber(), + 'website' => $faker->domainName(), + 'addressLine1' => $faker->streetAddress(), + 'addressLine2' => $faker->streetAddress(), + 'city' => $faker->city(), + 'stateProvince' => $faker->city(), + 'zipCode' => $faker->postcode(), + 'country' => $faker->countryISOAlpha3(), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), + ]; + + $command = $factory->get($input); + + $this->assertInstanceOf(CompanyGetCommand::class, $command); + $expected = $input['id']; + $actual = $command->id; + $this->assertSame($expected, $actual); + } + + public function testInsert(): void + { + $sanitizer = $this->container->get(CompanySanitizer::class); + /** @var Filter $filter */ + $filter = $this->container->get(Container::FILTER); + $factory = new CompanyCommandFactory($sanitizer); + $faker = FakerFactory::create(); + + $input = [ + 'id' => (string)$faker->numberBetween(1, 1000), + 'name' => $faker->company(), + 'email' => " Foo.Bar+tag@Example.COM ", + 'phone' => $faker->phoneNumber(), + 'website' => $faker->domainName(), + 'addressLine1' => $faker->streetAddress(), + 'addressLine2' => $faker->streetAddress(), + 'city' => $faker->city(), + 'stateProvince' => $faker->city(), + 'zipCode' => $faker->postcode(), + 'country' => $faker->countryISOAlpha3(), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), + ]; + + /** @var CompanyPostCommand $command */ + $command = $factory->insert($input); + + $this->assertInstanceOf(CompanyPostCommand::class, $command); + + $expected = null; + $actual = $command->id; + $this->assertSame($expected, $actual); + + $expected = $input['name']; + $actual = $command->name; + $this->assertSame($expected, $actual); + + $expected = $filter->email($input['email']); + $actual = $command->email; + $this->assertSame($expected, $actual); + + $expected = $input['phone']; + $actual = $command->phone; + $this->assertSame($expected, $actual); + + $expected = $input['website']; + $actual = $command->website; + $this->assertSame($expected, $actual); + + $expected = $input['addressLine1']; + $actual = $command->addressLine1; + $this->assertSame($expected, $actual); + + $expected = $input['addressLine2']; + $actual = $command->addressLine2; + $this->assertSame($expected, $actual); + + $expected = $input['city']; + $actual = $command->city; + $this->assertSame($expected, $actual); + + $expected = $input['stateProvince']; + $actual = $command->stateProvince; + $this->assertSame($expected, $actual); + + $expected = $input['zipCode']; + $actual = $command->zipCode; + $this->assertSame($expected, $actual); + + $expected = $input['country']; + $actual = $command->country; + $this->assertSame($expected, $actual); + + $expected = $input['createdDate']; + $actual = $command->createdDate; + $this->assertSame($expected, $actual); + + $expected = $filter->absint($input['createdUserId']); + $actual = $command->createdUserId; + $this->assertSame($expected, $actual); + + $expected = $input['updatedDate']; + $actual = $command->updatedDate; + $this->assertSame($expected, $actual); + + $expected = $filter->absint($input['updatedUserId']); + $actual = $command->updatedUserId; + $this->assertSame($expected, $actual); + } + + public function testUpdate(): void + { + $sanitizer = $this->container->get(CompanySanitizer::class); + /** @var Filter $filter */ + $filter = $this->container->get(Container::FILTER); + $factory = new CompanyCommandFactory($sanitizer); + $faker = FakerFactory::create(); + + $input = [ + 'id' => (string)$faker->numberBetween(1, 1000), + 'name' => $faker->company(), + 'email' => " Foo.Bar+tag@Example.COM ", + 'phone' => $faker->phoneNumber(), + 'website' => $faker->domainName(), + 'addressLine1' => $faker->streetAddress(), + 'addressLine2' => $faker->streetAddress(), + 'city' => $faker->city(), + 'stateProvince' => $faker->city(), + 'zipCode' => $faker->postcode(), + 'country' => $faker->countryISOAlpha3(), + 'createdDate' => $faker->date(), + 'createdUserId' => (string)$faker->numberBetween(1, 1000), + 'updatedDate' => $faker->date(), + 'updatedUserId' => (string)$faker->numberBetween(1, 1000), + ]; + + /** @var CompanyPutCommand $command */ + $command = $factory->update($input); + + $this->assertInstanceOf(CompanyPutCommand::class, $command); + + $expected = $filter->absint($input['id']); + $actual = $command->id; + $this->assertSame($expected, $actual); + + $expected = $input['name']; + $actual = $command->name; + $this->assertSame($expected, $actual); + + $expected = $filter->email($input['email']); + $actual = $command->email; + $this->assertSame($expected, $actual); + + $expected = $input['phone']; + $actual = $command->phone; + $this->assertSame($expected, $actual); + + $expected = $input['website']; + $actual = $command->website; + $this->assertSame($expected, $actual); + + $expected = $input['addressLine1']; + $actual = $command->addressLine1; + $this->assertSame($expected, $actual); + + $expected = $input['addressLine2']; + $actual = $command->addressLine2; + $this->assertSame($expected, $actual); + + $expected = $input['city']; + $actual = $command->city; + $this->assertSame($expected, $actual); + + $expected = $input['stateProvince']; + $actual = $command->stateProvince; + $this->assertSame($expected, $actual); + + $expected = $input['zipCode']; + $actual = $command->zipCode; + $this->assertSame($expected, $actual); + + $expected = $input['country']; + $actual = $command->country; + $this->assertSame($expected, $actual); + + $expected = $input['createdDate']; + $actual = $command->createdDate; + $this->assertSame($expected, $actual); + + $expected = $filter->absint($input['createdUserId']); + $actual = $command->createdUserId; + $this->assertSame($expected, $actual); + + $expected = $input['updatedDate']; + $actual = $command->updatedDate; + $this->assertSame($expected, $actual); + + $expected = $filter->absint($input['updatedUserId']); + $actual = $command->updatedUserId; + $this->assertSame($expected, $actual); + } +} From f27fd7ee5adef0bc1bbb1c9a1c324dff67347242 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:04:32 -0600 Subject: [PATCH 289/301] [#.x] - new company command dtos --- .../Company/Command/CompanyDeleteCommand.php | 27 +++++++++ .../Company/Command/CompanyGetCommand.php | 27 +++++++++ .../Company/Command/CompanyGetManyCommand.php | 59 +++++++++++++++++++ .../Company/Command/CompanyPostCommand.php | 55 +++++++++++++++++ .../Company/Command/CompanyPutCommand.php | 55 +++++++++++++++++ 5 files changed, 223 insertions(+) create mode 100644 src/Domain/Application/Company/Command/CompanyDeleteCommand.php create mode 100644 src/Domain/Application/Company/Command/CompanyGetCommand.php create mode 100644 src/Domain/Application/Company/Command/CompanyGetManyCommand.php create mode 100644 src/Domain/Application/Company/Command/CompanyPostCommand.php create mode 100644 src/Domain/Application/Company/Command/CompanyPutCommand.php diff --git a/src/Domain/Application/Company/Command/CompanyDeleteCommand.php b/src/Domain/Application/Company/Command/CompanyDeleteCommand.php new file mode 100644 index 0000000..31ba47d --- /dev/null +++ b/src/Domain/Application/Company/Command/CompanyDeleteCommand.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\Application\Company\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class CompanyDeleteCommand implements CommandInterface +{ + /** + * @param int $id + */ + public function __construct( + public int $id, + ) { + } +} diff --git a/src/Domain/Application/Company/Command/CompanyGetCommand.php b/src/Domain/Application/Company/Command/CompanyGetCommand.php new file mode 100644 index 0000000..dc93731 --- /dev/null +++ b/src/Domain/Application/Company/Command/CompanyGetCommand.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\Application\Company\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class CompanyGetCommand implements CommandInterface +{ + /** + * @param int $id + */ + public function __construct( + public int $id, + ) { + } +} diff --git a/src/Domain/Application/Company/Command/CompanyGetManyCommand.php b/src/Domain/Application/Company/Command/CompanyGetManyCommand.php new file mode 100644 index 0000000..776e1c1 --- /dev/null +++ b/src/Domain/Application/Company/Command/CompanyGetManyCommand.php @@ -0,0 +1,59 @@ + + * + * 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\Application\Company\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class CompanyGetManyCommand implements CommandInterface +{ + /** + * @param int|null $id + * @param string|null $name + * @param string|null $phone + * @param string|null $email + * @param string|null $website + * @param string|null $addressLine1 + * @param string|null $addressLine2 + * @param string|null $city + * @param string|null $stateProvince + * @param string|null $zipCode + * @param string|null $country + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + * @param int $page + * @param int $perPage + */ + public function __construct( + public ?int $id, + public ?string $name, + public ?string $phone, + public ?string $email, + public ?string $website, + public ?string $addressLine1, + public ?string $addressLine2, + public ?string $city, + public ?string $stateProvince, + public ?string $zipCode, + public ?string $country, + public ?string $createdDate, + public ?int $createdUserId, + public ?string $updatedDate, + public ?int $updatedUserId, + public int $page, + public int $perPage, + ) { + } +} diff --git a/src/Domain/Application/Company/Command/CompanyPostCommand.php b/src/Domain/Application/Company/Command/CompanyPostCommand.php new file mode 100644 index 0000000..3f1324c --- /dev/null +++ b/src/Domain/Application/Company/Command/CompanyPostCommand.php @@ -0,0 +1,55 @@ + + * + * 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\Application\Company\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class CompanyPostCommand implements CommandInterface +{ + /** + * @param int|null $id + * @param string|null $name + * @param string|null $phone + * @param string|null $email + * @param string|null $website + * @param string|null $addressLine1 + * @param string|null $addressLine2 + * @param string|null $city + * @param string|null $stateProvince + * @param string|null $zipCode + * @param string|null $country + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + */ + public function __construct( + public ?int $id, + public ?string $name, + public ?string $phone, + public ?string $email, + public ?string $website, + public ?string $addressLine1, + public ?string $addressLine2, + public ?string $city, + public ?string $stateProvince, + public ?string $zipCode, + public ?string $country, + public ?string $createdDate, + public ?int $createdUserId, + public ?string $updatedDate, + public ?int $updatedUserId, + ) { + } +} diff --git a/src/Domain/Application/Company/Command/CompanyPutCommand.php b/src/Domain/Application/Company/Command/CompanyPutCommand.php new file mode 100644 index 0000000..d5c1586 --- /dev/null +++ b/src/Domain/Application/Company/Command/CompanyPutCommand.php @@ -0,0 +1,55 @@ + + * + * 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\Application\Company\Command; + +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; + +final readonly class CompanyPutCommand implements CommandInterface +{ + /** + * @param int $id + * @param string|null $name + * @param string|null $phone + * @param string|null $email + * @param string|null $website + * @param string|null $addressLine1 + * @param string|null $addressLine2 + * @param string|null $city + * @param string|null $stateProvince + * @param string|null $zipCode + * @param string|null $country + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + */ + public function __construct( + public int $id, + public ?string $name, + public ?string $phone, + public ?string $email, + public ?string $website, + public ?string $addressLine1, + public ?string $addressLine2, + public ?string $city, + public ?string $stateProvince, + public ?string $zipCode, + public ?string $country, + public ?string $createdDate, + public ?int $createdUserId, + public ?string $updatedDate, + public ?int $updatedUserId, + ) { + } +} From 678c662e3c1fbf86132d7446c3db96ae3a31fe5b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:04:44 -0600 Subject: [PATCH 290/301] [#.x] - new company command repository --- .../Company/Repository/CompanyRepository.php | 118 +++++++++++ .../Repository/CompanyRepositoryInterface.php | 76 +++++++ .../Repository/CompanyRepositoryTest.php | 194 ++++++++++++++++++ 3 files changed, 388 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepository.php create mode 100644 src/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepositoryInterface.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepositoryTest.php diff --git a/src/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepository.php b/src/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepository.php new file mode 100644 index 0000000..ef6c11a --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepository.php @@ -0,0 +1,118 @@ + + * + * 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\Infrastructure\DataSource\Company\Repository; + +use Phalcon\Api\Domain\Infrastructure\DataSource\AbstractRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCollection; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCriteria; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapperInterface; +use Phalcon\DataMapper\Pdo\Connection; +use Phalcon\DataMapper\Query\Select; + +/** + * @phpstan-import-type TCriteria from CompanyTypes + * @phpstan-import-type TCompanyRecord from CompanyTypes + * @phpstan-import-type TCompanyRecords from CompanyTypes + * @phpstan-import-type TCompanyDbRecordOptional from CompanyTypes + */ +class CompanyRepository extends AbstractRepository implements CompanyRepositoryInterface +{ + /** + * @var string + */ + protected string $idField = 'com_id'; + /** + * @var string + */ + protected string $table = 'co_companies'; + + public function __construct( + Connection $connection, + private readonly CompanyMapperInterface $mapper, + ) { + parent::__construct($connection); + } + + /** + * @param CompanyCriteria $criteria + * + * @return CompanyCollection + */ + public function find(CompanyCriteria $criteria): CompanyCollection + { + $select = Select::new($this->connection); + + $select->from($this->table); + + $columns = $criteria->toArray(); + if (true !== empty($columns)) { + $select->whereEquals($columns); + } + + /** @var TCompanyRecords $results */ + $results = $select + ->page($criteria->page) + ->perPage($criteria->perPage) + ->fetchAll() + ; + + return $this->mapper->domainRows($results); + } + + /** + * @param int $recordId + * + * @return Company|null + */ + public function findById(int $recordId): ?Company + { + if ($recordId > 0) { + return $this->findOneBy( + [ + $this->idField => $recordId, + ] + ); + } + + return null; + } + + + /** + * @param TCriteria $criteria + * + * @return Company|null + */ + public function findOneBy(array $criteria): ?Company + { + $select = Select::new($this->connection); + + $select->from($this->table); + + if (true !== empty($criteria)) { + $select->whereEquals($criteria); + } + + /** @var TCompanyRecord $result */ + $result = $select->fetchOne(); + + if (empty($result)) { + return null; + } + + return $this->mapper->domain($result); + } +} diff --git a/src/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepositoryInterface.php b/src/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepositoryInterface.php new file mode 100644 index 0000000..0007f1f --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepositoryInterface.php @@ -0,0 +1,76 @@ + + * + * 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\Infrastructure\DataSource\Company\Repository; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCollection; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCriteria; + +/** + * @phpstan-import-type TCriteria from CompanyTypes + * @phpstan-import-type TCompanyDbRecordOptional from CompanyTypes + */ +interface CompanyRepositoryInterface +{ + /** + * @param TCriteria $criteria + * + * @return int + */ + public function deleteBy(array $criteria): int; + + /** + * @param int $recordId + * + * @return int + */ + public function deleteById(int $recordId): int; + + /** + * @param CompanyCriteria $criteria + * + * @return CompanyCollection + */ + public function find(CompanyCriteria $criteria): CompanyCollection; + + /** + * @param int $recordId + * + * @return Company|null + */ + public function findById(int $recordId): ?Company; + + /** + * @param TCriteria $criteria + * + * @return Company|null + */ + public function findOneBy(array $criteria): ?Company; + + /** + * @param TCompanyDbRecordOptional $columns + * + * @return int + */ + public function insert(array $columns): int; + + /** + * @param int $recordId + * @param TCompanyDbRecordOptional $columns + * + * @return int + */ + public function update(int $recordId, array $columns): int; +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepositoryTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepositoryTest.php new file mode 100644 index 0000000..cc9bf09 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/Repository/CompanyRepositoryTest.php @@ -0,0 +1,194 @@ + + * + * 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\Infrastructure\DataSource\Company\Repository; + +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCollection; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCriteria; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepository; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; + +use function uniqid; + +final class CompanyRepositoryTest extends AbstractUnitTestCase +{ + public function testFind(): void + { + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + /** @var CompanyRepository $repository */ + $repository = $this->container->get(CompanyRepository::class); + + $companyName = uniqid('name-'); + $migration = new CompaniesMigration($this->getConnection()); + + $company1 = $this->getNewCompany($migration, ['com_name' => $companyName]); + $company2 = $this->getNewCompany($migration, ['com_name' => $companyName]); + $company3 = $this->getNewCompany($migration, ['com_name' => $companyName]); + $company4 = $this->getNewCompany($migration); + $company5 = $this->getNewCompany($migration); + + $command = $factory->getMany( + [ + 'page' => 1, + 'perPage' => 10, + ] + ); + $criteria = CompanyCriteria::fromDto($command); + + $companies = $repository->find($criteria); + + $class = CompanyCollection::class; + $this->assertInstanceOf($class, $companies); + + $expected = 5; + $actual = $companies->count(); + $this->assertSame($expected, $actual); + + $companies = $companies->toArray(); + + $expected = $company1['com_id']; + $actual = $companies[$company1['com_id']]->id; + $this->assertSame($expected, $actual); + + $expected = $company2['com_id']; + $actual = $companies[$company2['com_id']]->id; + $this->assertSame($expected, $actual); + + $expected = $company3['com_id']; + $actual = $companies[$company3['com_id']]->id; + $this->assertSame($expected, $actual); + + $expected = $company4['com_id']; + $actual = $companies[$company4['com_id']]->id; + $this->assertSame($expected, $actual); + + $expected = $company5['com_id']; + $actual = $companies[$company5['com_id']]->id; + $this->assertSame($expected, $actual); + + $command = $factory->getMany( + [ + 'name' => $companyName, + 'page' => 1, + 'perPage' => 10, + ] + ); + $criteria = CompanyCriteria::fromDto($command); + $companies = $repository->find($criteria); + + $class = CompanyCollection::class; + $this->assertInstanceOf($class, $companies); + + $expected = 3; + $actual = $companies->count(); + $this->assertSame($expected, $actual); + + $companyIds = []; + foreach ($companies as $company) { + $companyIds[] = $company->id; + } + + $expected = [ + $company1['com_id'], + $company2['com_id'], + $company3['com_id'], + ]; + $actual = $companyIds; + $this->assertSame($expected, $actual); + } + + public function testFindById(): void + { + /** @var CompanyRepository $repository */ + $repository = $this->container->get(CompanyRepository::class); + + $migration = new CompaniesMigration($this->getConnection()); + + $repositoryCompany = $repository->findById(0); + $this->assertEmpty($repositoryCompany); + + $migrationCompany = $this->getNewCompany($migration); + $companyId = $migrationCompany['com_id']; + + $repositoryCompany = $repository->findById($companyId); + + $this->runAssertions($migrationCompany, $repositoryCompany); + } + + private function runAssertions(array $dbCompany, Company $company): void + { + $expected = $dbCompany['com_id']; + $actual = $company->id; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_name']; + $actual = $company->name; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_email']; + $actual = $company->email; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_phone']; + $actual = $company->phone; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_website']; + $actual = $company->website; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_address_line_1']; + $actual = $company->addressLine1; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_address_line_2']; + $actual = $company->addressLine2; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_city']; + $actual = $company->city; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_state_province']; + $actual = $company->stateProvince; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_zip_code']; + $actual = $company->zipCode; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_country']; + $actual = $company->country; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_created_date']; + $actual = $company->createdDate; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_created_usr_id']; + $actual = $company->createdUserId; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_updated_date']; + $actual = $company->updatedDate; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_updated_usr_id']; + $actual = $company->updatedUserId; + $this->assertSame($expected, $actual); + } +} From f6b113f021ef6ff79b6025618156b0ff4f8c3283 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:05:19 -0600 Subject: [PATCH 291/301] [#.x] - new company services --- .../Company/Handler/CompanyPostHandler.php | 125 +++++ .../Company/Handler/CompanyPutHandler.php | 135 +++++ .../Company/Service/CompanyPostService.php | 33 ++ .../Company/Service/CompanyPutService.php | 33 ++ .../Service/CompanyServiceDeleteTest.php | 105 ++++ .../Service/CompanyServiceGetManyTest.php | 79 +++ .../Company/Service/CompanyServiceGetTest.php | 158 ++++++ .../Service/CompanyServicePostTest.php | 392 +++++++++++++++ .../Company/Service/CompanyServicePutTest.php | 476 ++++++++++++++++++ 9 files changed, 1536 insertions(+) create mode 100644 src/Domain/Application/Company/Handler/CompanyPostHandler.php create mode 100644 src/Domain/Application/Company/Handler/CompanyPutHandler.php create mode 100644 src/Domain/Application/Company/Service/CompanyPostService.php create mode 100644 src/Domain/Application/Company/Service/CompanyPutService.php create mode 100644 tests/Unit/Domain/Application/Company/Service/CompanyServiceDeleteTest.php create mode 100644 tests/Unit/Domain/Application/Company/Service/CompanyServiceGetManyTest.php create mode 100644 tests/Unit/Domain/Application/Company/Service/CompanyServiceGetTest.php create mode 100644 tests/Unit/Domain/Application/Company/Service/CompanyServicePostTest.php create mode 100644 tests/Unit/Domain/Application/Company/Service/CompanyServicePutTest.php diff --git a/src/Domain/Application/Company/Handler/CompanyPostHandler.php b/src/Domain/Application/Company/Handler/CompanyPostHandler.php new file mode 100644 index 0000000..64df3a5 --- /dev/null +++ b/src/Domain/Application/Company/Handler/CompanyPostHandler.php @@ -0,0 +1,125 @@ + + * + * 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\Application\Company\Handler; + +use PDOException; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; + +/** + * @phpstan-import-type TCompanyDomainToDbRecord from CompanyTypes + * @phpstan-import-type TCompanyDbRecordOptional from CompanyTypes + */ +final class CompanyPostHandler extends AbstractCompanyPutPostHandler +{ + /** + * Create a company. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + $validation = $this->validator->validate($command); + if (!$validation->isValid()) { + return Payload::invalid($validation->getErrors()); + } + + /** + * Array for inserting + */ + $company = $this->mapper->db($command); + + /** + * Pre-insert checks and manipulations + */ + $company = $this->preInsert($company); + + /** + * Insert the record + */ + try { + $companyId = $this->repository->insert($company); + } catch (PDOException $ex) { + /** + * Fire the event to the listeners + */ + $this->eventsManager->fire( + 'company:pdoError', + $this, + $ex->getMessage() + ); + + return $this->getErrorPayload( + HttpCodesEnum::AppCannotCreateDatabaseRecord, + $ex->getMessage() + ); + } + + if ($companyId < 1) { + return $this->getErrorPayload( + HttpCodesEnum::AppCannotCreateDatabaseRecord, + 'No id returned' + ); + } + + /** + * Get the company from the database + */ + /** @var Company $domainCompany */ + $domainCompany = $this->repository->findById($companyId); + + /** + * Return the company back + */ + return Payload::created($this->transformer->get($domainCompany)); + } + + /** + * @param TCompanyDomainToDbRecord $input + * + * @return TCompanyDbRecordOptional + */ + private function preInsert(array $input): array + { + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + /** @var User $sessionUser */ + $sessionUser = $this->registry->get('user'); + + /** + * Created and updated company IDs come from the company in the session + */ + $input['com_created_usr_id'] = $sessionUser->id; + $input['com_updated_usr_id'] = $sessionUser->id; + + /** + * Set the created/updated dates if need be + */ + if (true === empty($input['com_created_date'])) { + $input['com_created_date'] = $now; + } + if (true === empty($input['com_updated_date'])) { + $input['com_updated_date'] = $now; + } + + /** @var TCompanyDbRecordOptional $input */ + return $this->cleanupFields($input); + } +} diff --git a/src/Domain/Application/Company/Handler/CompanyPutHandler.php b/src/Domain/Application/Company/Handler/CompanyPutHandler.php new file mode 100644 index 0000000..172e5d2 --- /dev/null +++ b/src/Domain/Application/Company/Handler/CompanyPutHandler.php @@ -0,0 +1,135 @@ + + * + * 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\Application\Company\Handler; + +use PDOException; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\Company\Command\CompanyPutCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\DTO\User; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; + +/** + * @phpstan-import-type TCompanyDomainToDbRecord from CompanyTypes + * @phpstan-import-type TCompanyDbRecordOptional from CompanyTypes + */ +final class CompanyPutHandler extends AbstractCompanyPutPostHandler +{ + /** + * Delete a company. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + $validation = $this->validator->validate($command); + if (!$validation->isValid()) { + return Payload::invalid($validation->getErrors()); + } + + /** + * Check if the company exists, If not, return an error + */ + /** @var CompanyPutCommand $command */ + $domainCompany = $this->repository->findById($command->id); + + if (null === $domainCompany) { + return Payload::notFound(); + } + + /** + * Array for updating + */ + $company = $this->mapper->db($command); + + /** + * Pre-update checks and manipulations + */ + $company = $this->preUpdate($company); + + /** + * Update the record + */ + try { + $companyId = $this->repository->update($command->id, $company); + } catch (PDOException $ex) { + /** + * Fire the event to the listeners + */ + $this->eventsManager->fire( + 'company:pdoError', + $this, + $ex->getMessage() + ); + + return $this->getErrorPayload( + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + $ex->getMessage() + ); + } + + if ($companyId < 1) { + return $this->getErrorPayload( + HttpCodesEnum::AppCannotUpdateDatabaseRecord, + 'No id returned' + ); + } + + /** + * Get the company from the database + */ + /** @var Company $domainCompany */ + $domainCompany = $this->repository->findById($companyId); + + /** + * Return the company back + */ + return Payload::updated($this->transformer->get($domainCompany)); + } + + /** + * @param TCompanyDomainToDbRecord $input + * + * @return TCompanyDbRecordOptional + */ + private function preUpdate(array $input): array + { + $now = Dates::toUTC(format: Dates::DATE_TIME_FORMAT); + /** @var User $sessionUser */ + $sessionUser = $this->registry->get('user'); + + $input['com_updated_usr_id'] = $sessionUser->id; + + /** + * Set updated date to now if it has not been set + */ + if (true === empty($input['com_updated_date'])) { + $input['com_updated_date'] = $now; + } + + /** + * Remove createdDate and createdCompanyId - cannot be changed. This + * needs to be here because we don't want to touch those fields. + */ + unset($input['com_created_date'], $input['com_created_usr_id']); + + + return $this->cleanupFields($input); + } +} diff --git a/src/Domain/Application/Company/Service/CompanyPostService.php b/src/Domain/Application/Company/Service/CompanyPostService.php new file mode 100644 index 0000000..5138e77 --- /dev/null +++ b/src/Domain/Application/Company/Service/CompanyPostService.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\Application\Company\Service; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; + +/** + * @phpstan-import-type TCompanyInput from InputTypes + */ +final class CompanyPostService extends AbstractCompanyService +{ + /** + * @param TCompanyInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + return $this->facade->insert($input); + } +} diff --git a/src/Domain/Application/Company/Service/CompanyPutService.php b/src/Domain/Application/Company/Service/CompanyPutService.php new file mode 100644 index 0000000..94ea7ae --- /dev/null +++ b/src/Domain/Application/Company/Service/CompanyPutService.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\Application\Company\Service; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; + +/** + * @phpstan-import-type TCompanyInput from InputTypes + */ +final class CompanyPutService extends AbstractCompanyService +{ + /** + * @param TCompanyInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + return $this->facade->update($input); + } +} diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServiceDeleteTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServiceDeleteTest.php new file mode 100644 index 0000000..21a6f35 --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServiceDeleteTest.php @@ -0,0 +1,105 @@ + + * + * 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\Application\Company\Service; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Company\Service\CompanyDeleteService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; + +final class CompanyServiceDeleteTest extends AbstractUnitTestCase +{ + public function testServiceWithCompanyId(): void + { + /** @var CompanyDeleteService $service */ + $service = $this->container->get(CompanyDeleteService::class); + + /** + * We need to ask for a user to be deleted with an ID that does not + * exist in the database. To ensure that, we will create a user, + * delete it and then try to delete the same user with that ID + */ + $migration = new CompaniesMigration($this->getConnection()); + $dbCompany = $this->getNewCompany($migration); + $companyId = $dbCompany['com_id']; + + $payload = $service->__invoke( + [ + 'id' => $companyId, + ] + ); + + $expected = DomainStatus::DELETED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $expected = [ + 'Record deleted successfully [#' . $companyId . '].', + ]; + $actual = $data; + $this->assertSame($expected, $actual); + + /** + * Now deleting it again, will result in a 404 + */ + $payload = $service->__invoke( + [ + 'id' => $companyId, + ] + ); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceZeroCompanyId(): void + { + /** @var CompanyDeleteService $service */ + $service = $this->container->get(CompanyDeleteService::class); + + $payload = $service->__invoke( + [ + 'id' => 0, + ] + ); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServiceGetManyTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServiceGetManyTest.php new file mode 100644 index 0000000..c447e3a --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServiceGetManyTest.php @@ -0,0 +1,79 @@ + + * + * 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\Application\Company\Service; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetManyService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; + +use function count; + +final class CompanyServiceGetManyTest extends AbstractUnitTestCase +{ + public function testServiceWithCompanyId(): void + { + /** @var CompanyGetManyService $service */ + $service = $this->container->get(CompanyGetManyService::class); + + $companyMigration = new CompaniesMigration($this->getConnection()); + + $company1 = $this->getNewCompany($companyMigration); + $company2 = $this->getNewCompany($companyMigration); + $company3 = $this->getNewCompany($companyMigration); + $company4 = $this->getNewCompany($companyMigration); + $company5 = $this->getNewCompany($companyMigration); + + $payload = $service->__invoke([]); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $expected = 5; + $actual = count($data); + $this->assertSame($expected, $actual); + + $this->assertArrayHasKey($company1['com_id'], $data); + $this->assertArrayHasKey($company2['com_id'], $data); + $this->assertArrayHasKey($company3['com_id'], $data); + $this->assertArrayHasKey($company4['com_id'], $data); + $this->assertArrayHasKey($company5['com_id'], $data); + + $expected = $company1['com_name']; + $actual = $data[$company1['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company2['com_name']; + $actual = $data[$company2['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company3['com_name']; + $actual = $data[$company3['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company4['com_name']; + $actual = $data[$company4['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company5['com_name']; + $actual = $data[$company5['com_id']]['name']; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServiceGetTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServiceGetTest.php new file mode 100644 index 0000000..614e913 --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServiceGetTest.php @@ -0,0 +1,158 @@ + + * + * 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\Application\Company\Service; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Company\Service\CompanyGetService; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; + +use function array_key_first; + +final class CompanyServiceGetTest extends AbstractUnitTestCase +{ + public function testServiceEmptyCompanyId(): void + { + /** @var CompanyGetService $service */ + $service = $this->container->get(CompanyGetService::class); + + $payload = $service->__invoke([]); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceWithCompanyId(): void + { + /** @var CompanyGetService $service */ + $service = $this->container->get(CompanyGetService::class); + + $companyMigration = new CompaniesMigration($this->getConnection()); + + $dbCompany = $this->getNewCompany($companyMigration); + $companyId = $dbCompany['com_id']; + + $payload = $service->__invoke( + [ + 'id' => $companyId, + ] + ); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $company = $actual['data']; + $key = array_key_first($company); + $company = $company[$key]; + + $expected = $dbCompany['com_id']; + $actual = $company['id']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_name']; + $actual = $company['name']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_email']; + $actual = $company['email']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_phone']; + $actual = $company['phone']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_website']; + $actual = $company['website']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_address_line_1']; + $actual = $company['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_address_line_2']; + $actual = $company['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_city']; + $actual = $company['city']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_state_province']; + $actual = $company['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_zip_code']; + $actual = $company['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_country']; + $actual = $company['country']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_created_date']; + $actual = $company['createdDate']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_created_usr_id']; + $actual = $company['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_updated_date']; + $actual = $company['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_updated_usr_id']; + $actual = $company['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testServiceWrongCompanyId(): void + { + /** @var CompanyGetService $service */ + $service = $this->container->get(CompanyGetService::class); + + $payload = $service->__invoke( + [ + 'id' => 999999, + ] + ); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServicePostTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServicePostTest.php new file mode 100644 index 0000000..a2b91ac --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServicePostTest.php @@ -0,0 +1,392 @@ + + * + * 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\Application\Company\Service; + +use DateTimeImmutable; +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPostService; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapperInterface; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Support\Registry; + +use function array_key_first; +use function uniqid; + +final class CompanyServicePostTest extends AbstractUnitTestCase +{ + public function testServiceFailureNoIdReturned(): void + { + $companyRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'insert', + ] + ) + ->getMock() + ; + $companyRepository->method('insert')->willReturn(0); + + $this->container->setShared(CompanyRepository::class, $companyRepository); + + /** @var CompanyPostService $service */ + $service = $this->container->get(CompanyPostService::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + /** + * $companyData is a db record. We need a domain object here + */ + $domainCompany = $companyMapper->domain($companyData); + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainCompany); + + $domainData = $domainCompany->toArray(); + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot create database record: No id returned']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailurePdoError(): void + { + $message = uniqid('pdo-error-'); + $companyRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'insert', + ] + ) + ->getMock() + ; + $companyRepository + ->method('insert') + ->willThrowException(new PDOException($message)) + ; + + $this->container->setShared(CompanyRepository::class, $companyRepository); + + /** @var CompanyPostService $service */ + $service = $this->container->get(CompanyPostService::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + /** + * $companyData is a db record. We need a domain object here + */ + $domainCompany = $companyMapper->domain($companyData); + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainCompany); + + $domainData = $domainCompany->toArray(); + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot create database record: ' . $message]]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailureValidation(): void + { + /** @var CompanyPostService $service */ + $service = $this->container->get(CompanyPostService::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + + $companyData = $this->getNewCompanyData(); + + unset( + $companyData['com_name'], + $companyData['com_email'], + ); + + /** + * $companyData is a db record. We need a domain object here + */ + $domainCompany = $companyMapper->domain($companyData); + $domainData = $domainCompany->toArray(); + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + ['Field name is required'], + ['Field email is required'], + ['Field email must be an email address'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccess(): void + { + /** @var CompanyPostService $service */ + $service = $this->container->get(CompanyPostService::class); + /** @var UserMapperInterface $companyMapper */ + $companyMapper = $this->container->get(UserMapper::class); + /** @var CompanyMapperInterface $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + + $companyData = $this->getNewUserData(); + $companyData = $this->getNewCompanyData(); + + /** + * $companyData is a db record. We need a domain object here + */ + $domainUser = $companyMapper->domain($companyData); + + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainCompany = $companyMapper->domain($companyData); + $domainData = $domainCompany->toArray(); + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $companyId = array_key_first($data); + + $this->assertGreaterThan(0, $companyId); + + $data = $data[$companyId]; + + $expected = $domainData['name']; + $actual = $data['name']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $expected = $domainData['phone']; + $actual = $data['phone']; + $this->assertSame($expected, $actual); + + $expected = $domainData['website']; + $actual = $data['website']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine1']; + $actual = $data['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine2']; + $actual = $data['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $domainData['city']; + $actual = $data['city']; + $this->assertSame($expected, $actual); + + $expected = $domainData['stateProvince']; + $actual = $data['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $domainData['zipCode']; + $actual = $data['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $domainData['country']; + $actual = $data['country']; + $this->assertSame($expected, $actual); + + $expected = $domainData['createdDate']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + $expected = $domainUser->id; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['updatedDate']; + $actual = $data['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $domainUser->id; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccessEmptyDates(): void + { + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); + /** @var CompanyPostService $service */ + $service = $this->container->get(CompanyPostService::class); + /** @var CompanyMapperInterface $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + + $companyData = $this->getNewCompanyData(); + + /** + * $companyData is a db record. We need a domain object here + */ + $domainUser = $companyMapper->domain($companyData); + + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainUser); + + unset( + $companyData['com_created_date'], + $companyData['com_updated_date'], + ); + + /** + * $companyData is a db record. We need a domain object here + */ + $domainCompany = $companyMapper->domain($companyData); + $domainData = $domainCompany->toArray(); + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $companyId = array_key_first($data); + + $this->assertGreaterThan(0, $companyId); + + $data = $data[$companyId]; + + $expected = $domainData['name']; + $actual = $data['name']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $expected = $domainData['phone']; + $actual = $data['phone']; + $this->assertSame($expected, $actual); + + $expected = $domainData['website']; + $actual = $data['website']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine1']; + $actual = $data['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine2']; + $actual = $data['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $domainData['city']; + $actual = $data['city']; + $this->assertSame($expected, $actual); + + $expected = $domainData['stateProvince']; + $actual = $data['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $domainData['zipCode']; + $actual = $data['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $domainData['country']; + $actual = $data['country']; + $this->assertSame($expected, $actual); + + $actual = $data['createdDate']; + $this->assertStringContainsString($today, $actual); + + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $actual = $data['updatedDate']; + $this->assertStringContainsString($today, $actual); + + $expected = 0; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServicePutTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServicePutTest.php new file mode 100644 index 0000000..1ad5ea2 --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServicePutTest.php @@ -0,0 +1,476 @@ + + * + * 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\Application\Company\Service; + +use DateTimeImmutable; +use Faker\Factory; +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\Application\Company\Service\CompanyPutService; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; +use Phalcon\Support\Registry; + +use function uniqid; + +final class CompanyServicePutTest extends AbstractUnitTestCase +{ + public function testServiceFailureNoIdReturned(): void + { + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + $companyData = $this->getNewCompanyData(); + + $companyData['com_id'] = 1; + + $findByCompany = $companyMapper->domain($companyData); + + $userRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'update', + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('update')->willReturn(0); + $userRepository->method('findById')->willReturn($findByCompany); + + $this->container->setShared(CompanyRepository::class, $userRepository); + + /** + * Add a user in the repository for the session + */ + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + $sessionUser = $userMapper->domain(['id' => 1]); + $registry->set('user', $sessionUser); + + /** @var CompanyPutService $service */ + $service = $this->container->get(CompanyPutService::class); + + /** + * Update user + */ + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + $updateCompany = $companyMapper->domain($companyData); + $updateCompany = $updateCompany->toArray(); + + $payload = $service->__invoke($updateCompany); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot update database record: No id returned']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailurePdoError(): void + { + $message = uniqid('pdo-error-'); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + $companyData = $this->getNewCompanyData(); + + $companyData['com_id'] = 1; + + $findByCompany = $companyMapper->domain($companyData); + + $userRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'update', + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('findById')->willReturn($findByCompany); + $userRepository + ->method('update') + ->willThrowException(new PDOException($message)) + ; + + $this->container->setShared(CompanyRepository::class, $userRepository); + + /** + * Add a user in the repository for the session + */ + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + $sessionUser = $userMapper->domain(['id' => 1]); + $registry->set('user', $sessionUser); + + /** @var CompanyPutService $service */ + $service = $this->container->get(CompanyPutService::class); + + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + /** + * $companyData is a db record. We need a domain object here + */ + $updateCompany = $companyMapper->domain($companyData); + $updateCompany = $updateCompany->toArray(); + + $payload = $service->__invoke($updateCompany); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot update database record: ' . $message]]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailureRecordNotFound(): void + { + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + $userRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('findById')->willReturn(null); + + $this->container->setShared(CompanyRepository::class, $userRepository); + + /** @var CompanyPutService $service */ + $service = $this->container->get(CompanyPutService::class); + + /** + * Update user + */ + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + $updateCompany = $companyMapper->domain($companyData); + $updateCompany = $updateCompany->toArray(); + + $payload = $service->__invoke($updateCompany); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailureValidation(): void + { + /** @var CompanyPutService $service */ + $service = $this->container->get(CompanyPutService::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + $companyData = $this->getNewCompanyData(); + + $companyData['com_email'] = 'wrong-email'; + unset($companyData['com_id']); + + /** + * $companyData is a db record. We need a domain object here + */ + $updateCompany = $companyMapper->domain($companyData); + $updateCompany = $updateCompany->toArray(); + + $payload = $service->__invoke($updateCompany); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + ['Field id is not a valid absolute integer and greater than 0'], + ['Field email must be an email address'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccess(): void + { + $faker = Factory::create(); + /** @var CompanyPutService $service */ + $service = $this->container->get(CompanyPutService::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + + $migration = new CompaniesMigration($this->getConnection()); + $dbCompany = $this->getNewCompany($migration); + $companyId = $dbCompany['com_id']; + $userData = $this->getNewUserData(); + $userId = $faker->numberBetween(1, 10); + $userData['usr_id'] = $userId; + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainCompany = $companyMapper->domain($dbCompany); + $domainData = $domainCompany->toArray(); + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey($companyId, $data); + + $data = $data[$companyId]; + + $expected = $domainData['id']; + $actual = $data['id']; + $this->assertSame($expected, $actual); + + $expected = $domainData['name']; + $actual = $data['name']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $expected = $domainData['phone']; + $actual = $data['phone']; + $this->assertSame($expected, $actual); + + $expected = $domainData['website']; + $actual = $data['website']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine1']; + $actual = $data['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine2']; + $actual = $data['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $domainData['city']; + $actual = $data['city']; + $this->assertSame($expected, $actual); + + $expected = $domainData['stateProvince']; + $actual = $data['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $domainData['zipCode']; + $actual = $data['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $domainData['country']; + $actual = $data['country']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = $domainData['createdDate']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['updatedDate']; + $actual = $data['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $userId; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccessEmptyDates(): void + { + $faker = Factory::create(); + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); + /** @var CompanyPutService $service */ + $service = $this->container->get(CompanyPutService::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + + $migration = new CompaniesMigration($this->getConnection()); + $dbCompany = $this->getNewCompany($migration); + + $companyId = $dbCompany['com_id']; + $userData = $this->getNewUserData(); + $userId = $faker->numberBetween(1, 10); + $userData['usr_id'] = $userId; + + unset( + $dbCompany['com_updated_date'], + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainCompany = $companyMapper->domain($dbCompany); + $domainData = $domainCompany->toArray(); + + $payload = $service->__invoke($domainData); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey($companyId, $data); + + $data = $data[$companyId]; + + $expected = $domainData['id']; + $actual = $data['id']; + $this->assertSame($expected, $actual); + + $expected = $domainData['name']; + $actual = $data['name']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $expected = $domainData['phone']; + $actual = $data['phone']; + $this->assertSame($expected, $actual); + + $expected = $domainData['website']; + $actual = $data['website']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine1']; + $actual = $data['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine2']; + $actual = $data['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $domainData['city']; + $actual = $data['city']; + $this->assertSame($expected, $actual); + + $expected = $domainData['stateProvince']; + $actual = $data['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $domainData['zipCode']; + $actual = $data['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $domainData['country']; + $actual = $data['country']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = $dbCompany['com_created_date']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $actual = $data['updatedDate']; + $this->assertStringContainsString($today, $actual); + + $expected = $userId; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } +} From f60ee634aba91783587b67bcab8ab7e8283a4921 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:05:32 -0600 Subject: [PATCH 292/301] [#.x] - new company facade --- .../Company/Facade/CompanyFacade.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/Domain/Application/Company/Facade/CompanyFacade.php diff --git a/src/Domain/Application/Company/Facade/CompanyFacade.php b/src/Domain/Application/Company/Facade/CompanyFacade.php new file mode 100644 index 0000000..4cd2ae0 --- /dev/null +++ b/src/Domain/Application/Company/Facade/CompanyFacade.php @@ -0,0 +1,106 @@ + + * + * 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\Application\Company\Facade; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactoryInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandBusInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; + +/** + * Orchestration for workflow + * + * - Sanitization + * - DTO creation + * - Validation + * - Pre-operation checks (when necessary) + * - Repository operation + * + * @phpstan-import-type TCompanyInput from InputTypes + * @phpstan-import-type TCompanyDomainToDbRecord from CompanyTypes + * @phpstan-import-type TCompanyDbRecordOptional from CompanyTypes + */ +final readonly class CompanyFacade +{ + /** + * @param CommandBusInterface $bus + * @param CompanyCommandFactoryInterface $factory + */ + public function __construct( + private CommandBusInterface $bus, + private CompanyCommandFactoryInterface $factory, + ) { + } + + /** + * Delete a company. + * + * @param TCompanyInput $input + * + * @return Payload + */ + public function delete(array $input): Payload + { + return $this->bus->dispatch($this->factory->delete($input)); + } + + /** + * Get a company. + * + * @param TCompanyInput $input + * + * @return Payload + */ + public function get(array $input): Payload + { + return $this->bus->dispatch($this->factory->get($input)); + } + + /** + * Get many records + * + * @param TCompanyInput $input + * + * @return Payload + */ + public function getMany(array $input): Payload + { + return $this->bus->dispatch($this->factory->getMany($input)); + } + + /** + * Create a company. + * + * @param TCompanyInput $input + * + * @return Payload + */ + public function insert(array $input): Payload + { + return $this->bus->dispatch($this->factory->insert($input)); + } + + /** + * Create a company. + * + * @param TCompanyInput $input + * + * @return Payload + */ + public function update(array $input): Payload + { + return $this->bus->dispatch($this->factory->update($input)); + } +} From df19f2f0d1852573ad108e05d27de2bd135dae3f Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:06:11 -0600 Subject: [PATCH 293/301] [#.x] - company services and mapper test --- .../Service/AbstractCompanyService.php | 28 +++ .../Company/Service/CompanyDeleteService.php | 33 ++++ .../Company/Service/CompanyGetManyService.php | 33 ++++ .../Company/Service/CompanyGetService.php | 33 ++++ .../Company/Mapper/CompanyMapperTest.php | 178 ++++++++++++++++++ 5 files changed, 305 insertions(+) create mode 100644 src/Domain/Application/Company/Service/AbstractCompanyService.php create mode 100644 src/Domain/Application/Company/Service/CompanyDeleteService.php create mode 100644 src/Domain/Application/Company/Service/CompanyGetManyService.php create mode 100644 src/Domain/Application/Company/Service/CompanyGetService.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapperTest.php diff --git a/src/Domain/Application/Company/Service/AbstractCompanyService.php b/src/Domain/Application/Company/Service/AbstractCompanyService.php new file mode 100644 index 0000000..a329769 --- /dev/null +++ b/src/Domain/Application/Company/Service/AbstractCompanyService.php @@ -0,0 +1,28 @@ + + * + * 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\Application\Company\Service; + +use Phalcon\Api\Domain\ADR\DomainInterface; +use Phalcon\Api\Domain\Application\Company\Facade\CompanyFacade; + +abstract class AbstractCompanyService implements DomainInterface +{ + /** + * @param CompanyFacade $facade + */ + public function __construct( + protected readonly CompanyFacade $facade, + ) { + } +} diff --git a/src/Domain/Application/Company/Service/CompanyDeleteService.php b/src/Domain/Application/Company/Service/CompanyDeleteService.php new file mode 100644 index 0000000..ecae262 --- /dev/null +++ b/src/Domain/Application/Company/Service/CompanyDeleteService.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\Application\Company\Service; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; + +/** + * @phpstan-import-type TCompanyInput from InputTypes + */ +final class CompanyDeleteService extends AbstractCompanyService +{ + /** + * @param TCompanyInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + return $this->facade->delete($input); + } +} diff --git a/src/Domain/Application/Company/Service/CompanyGetManyService.php b/src/Domain/Application/Company/Service/CompanyGetManyService.php new file mode 100644 index 0000000..6479cf9 --- /dev/null +++ b/src/Domain/Application/Company/Service/CompanyGetManyService.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\Application\Company\Service; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; + +/** + * @phpstan-import-type TCompanyInput from InputTypes + */ +final class CompanyGetManyService extends AbstractCompanyService +{ + /** + * @param TCompanyInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + return $this->facade->getMany($input); + } +} diff --git a/src/Domain/Application/Company/Service/CompanyGetService.php b/src/Domain/Application/Company/Service/CompanyGetService.php new file mode 100644 index 0000000..29353dc --- /dev/null +++ b/src/Domain/Application/Company/Service/CompanyGetService.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\Application\Company\Service; + +use Phalcon\Api\Domain\ADR\InputTypes; +use Phalcon\Api\Domain\ADR\Payload; + +/** + * @phpstan-import-type TCompanyInput from InputTypes + */ +final class CompanyGetService extends AbstractCompanyService +{ + /** + * @param TCompanyInput $input + * + * @return Payload + */ + public function __invoke(array $input): Payload + { + return $this->facade->get($input); + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapperTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapperTest.php new file mode 100644 index 0000000..e606070 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/Mapper/CompanyMapperTest.php @@ -0,0 +1,178 @@ + + * + * 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\Infrastructure\DataSource\Company\Mapper; + +use Faker\Factory as FakerFactory; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +final class CompanyMapperTest extends AbstractUnitTestCase +{ + public function testDb(): void + { + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + $faker = FakerFactory::create(); + + $createdDate = $faker->date(Dates::DATE_TIME_FORMAT); + $updatedDate = $faker->date(Dates::DATE_TIME_FORMAT); + + $input = [ + 'id' => $faker->numberBetween(1, 1000), + 'name' => $faker->company(), + 'phone' => $faker->safeEmail(), + 'email' => $faker->phoneNumber(), + 'website' => $faker->domainName(), + 'addressLine1' => $faker->streetAddress(), + 'addressLine2' => $faker->streetAddress(), + 'city' => $faker->city(), + 'stateProvince' => $faker->city(), + 'zipCode' => $faker->postcode(), + 'country' => $faker->countryISOAlpha3(), + 'createdDate' => $createdDate, + 'createdUserId' => $faker->numberBetween(1, 1000), + 'updatedDate' => $updatedDate, + 'updatedUserId' => $faker->numberBetween(1, 1000), + ]; + + $company = $factory->update($input); + + /** @var CompanyMapper $mapper */ + $mapper = $this->container->get(CompanyMapper::class); + $row = $mapper->db($company); + + $expected = [ + 'com_id' => $company->id, + 'com_name' => $company->name, + 'com_phone' => $company->phone, + 'com_email' => $company->email, + 'com_website' => $company->website, + 'com_address_line_1' => $company->addressLine1, + 'com_address_line_2' => $company->addressLine2, + 'com_city' => $company->city, + 'com_state_province' => $company->stateProvince, + 'com_zip_code' => $company->zipCode, + 'com_country' => $company->country, + 'com_created_date' => $company->createdDate, + 'com_created_usr_id' => $company->createdUserId, + 'com_updated_date' => $company->updatedDate, + 'com_updated_usr_id' => $company->updatedUserId, + ]; + + $actual = $row; + $this->assertSame($expected, $actual); + } + + public function testDomain(): void + { + $faker = FakerFactory::create(); + $mapper = $this->container->get(CompanyMapper::class); + + // Empty row: defaults should be applied + $emptyCompany = $mapper->domain([]); + + $expected = 0; + $actual = $emptyCompany->id; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->name; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->phone; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->email; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->website; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->addressLine1; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->addressLine2; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->city; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->stateProvince; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->zipCode; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->country; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->createdDate; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->createdUserId; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->updatedDate; + $this->assertSame($expected, $actual); + + $expected = null; + $actual = $emptyCompany->updatedUserId; + $this->assertSame($expected, $actual); + + // Row with present created/updated user ids as strings should be cast to int + $row = [ + 'com_id' => (string)$faker->numberBetween(1, 1000), + 'com_name' => $faker->company(), + 'com_email' => $faker->safeEmail(), + 'com_created_usr_id' => (string)$faker->numberBetween(1, 1000), + 'com_updated_usr_id' => (string)$faker->numberBetween(1, 1000), + ]; + + $company = $mapper->domain($row); + + $expected = (int)$row['com_id']; + $actual = $company->id; + $this->assertSame($expected, $actual); + + $expected = $row['com_name']; + $actual = $company->name; + $this->assertSame($expected, $actual); + + $expected = $row['com_email']; + $actual = $company->email; + $this->assertSame($expected, $actual); + + $expected = (int)$row['com_created_usr_id']; + $actual = $company->createdUserId; + $this->assertSame($expected, $actual); + + $expected = (int)$row['com_updated_usr_id']; + $actual = $company->updatedUserId; + $this->assertSame($expected, $actual); + } +} From da85f2b13680dca73fe7cd47f2b97331886cf970 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:06:31 -0600 Subject: [PATCH 294/301] [#.x] - company dto test --- .../DataSource/Company/DTO/CompanyTest.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/DTO/CompanyTest.php diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/DTO/CompanyTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/DTO/CompanyTest.php new file mode 100644 index 0000000..4c38aa0 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/DTO/CompanyTest.php @@ -0,0 +1,67 @@ + + * + * 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\Infrastructure\DataSource\Company\DTO; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Tests\AbstractUnitTestCase; + +use function rand; + +final class CompanyTest extends AbstractUnitTestCase +{ + public function testObject(): void + { + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = rand(1, 100); + + $company = new Company( + $companyData['com_id'], + $companyData['com_name'], + $companyData['com_phone'], + $companyData['com_email'], + $companyData['com_website'], + $companyData['com_address_line_1'], + $companyData['com_address_line_2'], + $companyData['com_city'], + $companyData['com_state_province'], + $companyData['com_zip_code'], + $companyData['com_country'], + $companyData['com_created_date'], + $companyData['com_created_usr_id'], + $companyData['com_updated_date'], + $companyData['com_updated_usr_id'], + ); + + + $expected = [ + 'id' => $companyData['com_id'], + 'name' => $companyData['com_name'], + 'phone' => $companyData['com_phone'], + 'email' => $companyData['com_email'], + 'website' => $companyData['com_website'], + 'addressLine1' => $companyData['com_address_line_1'], + 'addressLine2' => $companyData['com_address_line_2'], + 'city' => $companyData['com_city'], + 'stateProvince' => $companyData['com_state_province'], + 'zipCode' => $companyData['com_zip_code'], + 'country' => $companyData['com_country'], + 'createdDate' => $companyData['com_created_date'], + 'createdUserId' => $companyData['com_created_usr_id'], + 'updatedDate' => $companyData['com_updated_date'], + 'updatedUserId' => $companyData['com_updated_usr_id'], + ]; + $actual = $company->toArray(); + $this->assertSame($expected, $actual); + } +} From 8b089eebe11064232aa5c1f7c0aaed2bf6d66da3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:06:44 -0600 Subject: [PATCH 295/301] [#.x] - company validator tests --- .../Company/Validator/CompanyUpdateEnum.php | 46 +++++++++++++++++ .../Validator/CompanyUpdateEnumTest.php | 51 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Company/Validator/CompanyUpdateEnum.php create mode 100644 tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyUpdateEnumTest.php diff --git a/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyUpdateEnum.php b/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyUpdateEnum.php new file mode 100644 index 0000000..0152f60 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/Validator/CompanyUpdateEnum.php @@ -0,0 +1,46 @@ + + * + * 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\Infrastructure\DataSource\Company\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Interface\ValidatorEnumInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +enum CompanyUpdateEnum implements ValidatorEnumInterface +{ + case id; + case email; + + public function allowEmpty(): bool + { + return match ($this) { + self::id => false, + default => true, + }; + } + + public function validators(): array + { + return match ($this) { + self::id => [ + PresenceOf::class, + AbsInt::class, + ], + self::email => [ + Email::class, + ], + }; + } +} diff --git a/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyUpdateEnumTest.php b/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyUpdateEnumTest.php new file mode 100644 index 0000000..4625328 --- /dev/null +++ b/tests/Unit/Domain/Infrastructure/DataSource/Company/Validator/CompanyUpdateEnumTest.php @@ -0,0 +1,51 @@ + + * + * 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\Infrastructure\DataSource\Company\Validator; + +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Validator\CompanyUpdateEnum; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\AbsInt; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Filter\Validation\Validator\Email; +use Phalcon\Filter\Validation\Validator\PresenceOf; + +final class CompanyUpdateEnumTest extends AbstractUnitTestCase +{ + public function testEnum(): void + { + $expected = 2; + $actual = CompanyUpdateEnum::cases(); + $this->assertCount($expected, $actual); + + $expected = [ + PresenceOf::class, + AbsInt::class, + ]; + $actual = CompanyUpdateEnum::id->validators(); + $this->assertSame($expected, $actual); + + $expected = false; + $actual = CompanyUpdateEnum::id->allowEmpty(); + $this->assertSame($expected, $actual); + + $expected = [ + Email::class, + ]; + $actual = CompanyUpdateEnum::email->validators(); + $this->assertSame($expected, $actual); + + $expected = true; + $actual = CompanyUpdateEnum::email->allowEmpty(); + $this->assertSame($expected, $actual); + } +} From a5a163422dd7e03dc4d781f142e6adc7e6e0ad67 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:07:06 -0600 Subject: [PATCH 296/301] [#.x] - added company handlers and tests --- .../Handler/AbstractCompanyPutPostHandler.php | 81 +++ .../Company/Handler/CompanyDeleteHandler.php | 64 +++ .../Company/Handler/CompanyGetHandler.php | 64 +++ .../Company/Handler/CompanyGetManyHandler.php | 51 ++ .../Handler/CompanyHandlerDeleteTest.php | 100 ++++ .../Handler/CompanyHandlerGetManyTest.php | 82 +++ .../Company/Handler/CompanyHandlerGetTest.php | 160 ++++++ .../Handler/CompanyHandlerPostTest.php | 398 ++++++++++++++ .../Company/Handler/CompanyHandlerPutTest.php | 493 ++++++++++++++++++ 9 files changed, 1493 insertions(+) create mode 100644 src/Domain/Application/Company/Handler/AbstractCompanyPutPostHandler.php create mode 100644 src/Domain/Application/Company/Handler/CompanyDeleteHandler.php create mode 100644 src/Domain/Application/Company/Handler/CompanyGetHandler.php create mode 100644 src/Domain/Application/Company/Handler/CompanyGetManyHandler.php create mode 100644 tests/Unit/Domain/Application/Company/Handler/CompanyHandlerDeleteTest.php create mode 100644 tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetManyTest.php create mode 100644 tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetTest.php create mode 100644 tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPostTest.php create mode 100644 tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPutTest.php diff --git a/src/Domain/Application/Company/Handler/AbstractCompanyPutPostHandler.php b/src/Domain/Application/Company/Handler/AbstractCompanyPutPostHandler.php new file mode 100644 index 0000000..656f096 --- /dev/null +++ b/src/Domain/Application/Company/Handler/AbstractCompanyPutPostHandler.php @@ -0,0 +1,81 @@ + + * + * 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\Application\Company\Handler; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; +use Phalcon\Api\Domain\Infrastructure\DataSource\Validation\ValidatorInterface; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\HttpCodesEnum; +use Phalcon\Events\ManagerInterface as EventsManagerInterface; +use Phalcon\Support\Registry; + +use function array_filter; + +/** + * @phpstan-import-type TCompanyDomainToDbRecord from CompanyTypes + * @phpstan-import-type TCompanyDbRecordOptional from CompanyTypes + */ +abstract class AbstractCompanyPutPostHandler implements HandlerInterface +{ + /** + * @param ValidatorInterface $validator + * @param CompanyMapperInterface $mapper + * @param CompanyRepositoryInterface $repository + * @param EventsManagerInterface $eventsManager + * @param Transformer $transformer + * @param Registry $registry + */ + public function __construct( + protected readonly ValidatorInterface $validator, + protected readonly CompanyMapperInterface $mapper, + protected readonly CompanyRepositoryInterface $repository, + protected readonly EventsManagerInterface $eventsManager, + protected readonly Transformer $transformer, + protected readonly Registry $registry, + ) { + } + + /** + * @param TCompanyDbRecordOptional $row + * + * @return TCompanyDbRecordOptional + */ + protected function cleanupFields(array $row): array + { + unset($row['com_id']); + + return array_filter( + $row, + static fn($v) => $v !== null && $v !== '' + ); + } + + /** + * @param HttpCodesEnum $item + * @param string $message + * + * @return Payload + */ + protected function getErrorPayload( + HttpCodesEnum $item, + string $message + ): Payload { + return Payload::error([[$item->text() . $message]]); + } +} diff --git a/src/Domain/Application/Company/Handler/CompanyDeleteHandler.php b/src/Domain/Application/Company/Handler/CompanyDeleteHandler.php new file mode 100644 index 0000000..ce03a94 --- /dev/null +++ b/src/Domain/Application/Company/Handler/CompanyDeleteHandler.php @@ -0,0 +1,64 @@ + + * + * 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\Application\Company\Handler; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\Company\Command\CompanyDeleteCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; + +final readonly class CompanyDeleteHandler implements HandlerInterface +{ + /** + * @param CompanyRepositoryInterface $repository + * @param Transformer $transformer + */ + public function __construct( + private CompanyRepositoryInterface $repository, + private Transformer $transformer + ) { + } + + /** + * Delete a company. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + /** @var CompanyDeleteCommand $command */ + $companyId = $command->id; + + /** + * Success + */ + if ($companyId > 0) { + $rowCount = $this->repository->deleteById($companyId); + + if (0 !== $rowCount) { + return Payload::deleted($this->transformer->delete($companyId)); + } + } + + /** + * 404 + */ + return Payload::notFound(); + } +} diff --git a/src/Domain/Application/Company/Handler/CompanyGetHandler.php b/src/Domain/Application/Company/Handler/CompanyGetHandler.php new file mode 100644 index 0000000..6ab5371 --- /dev/null +++ b/src/Domain/Application/Company/Handler/CompanyGetHandler.php @@ -0,0 +1,64 @@ + + * + * 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\Application\Company\Handler; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Application\Company\Command\CompanyGetCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; + +final readonly class CompanyGetHandler implements HandlerInterface +{ + /** + * @param CompanyRepositoryInterface $repository + * @param Transformer $transformer + */ + public function __construct( + private CompanyRepositoryInterface $repository, + private Transformer $transformer + ) { + } + + /** + * Get a company. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + /** @var CompanyGetCommand $command */ + $companyId = $command->id; + + /** + * Success + */ + if ($companyId > 0) { + $company = $this->repository->findById($companyId); + + if (null !== $company) { + return Payload::success($this->transformer->get($company)); + } + } + + /** + * 404 + */ + return Payload::notFound(); + } +} diff --git a/src/Domain/Application/Company/Handler/CompanyGetManyHandler.php b/src/Domain/Application/Company/Handler/CompanyGetManyHandler.php new file mode 100644 index 0000000..6fe6ab6 --- /dev/null +++ b/src/Domain/Application/Company/Handler/CompanyGetManyHandler.php @@ -0,0 +1,51 @@ + + * + * 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\Application\Company\Handler; + +use Phalcon\Api\Domain\ADR\Payload; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\CommandBus\HandlerInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\Company; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\DTO\CompanyCriteria; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepositoryInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Transformer\Transformer; + +final readonly class CompanyGetManyHandler implements HandlerInterface +{ + /** + * @param CompanyRepositoryInterface $repository + * @param Transformer $transformer + */ + public function __construct( + private CompanyRepositoryInterface $repository, + private Transformer $transformer, + ) { + } + + /** + * Get a company. + * + * @param CommandInterface $command + * + * @return Payload + */ + public function __invoke(CommandInterface $command): Payload + { + $criteria = CompanyCriteria::fromDto($command); + + $results = $this->repository->find($criteria); + + return Payload::success($this->transformer->getMany($results)); + } +} diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerDeleteTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerDeleteTest.php new file mode 100644 index 0000000..e1dcdf3 --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerDeleteTest.php @@ -0,0 +1,100 @@ + + * + * 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\Application\Company\Handler; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyDeleteHandler; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; + +final class CompanyHandlerDeleteTest extends AbstractUnitTestCase +{ + public function testHandlerWithCompanyId(): void + { + /** @var CompanyDeleteHandler $handler */ + $handler = $this->container->get(CompanyDeleteHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + /** + * We need to ask for a user to be deleted with an ID that does not + * exist in the database. To ensure that, we will create a user, + * delete it and then try to delete the same user with that ID + */ + $migration = new CompaniesMigration($this->getConnection()); + $dbCompany = $this->getNewCompany($migration); + $companyId = $dbCompany['com_id']; + + $command = $factory->delete(['id' => $companyId]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::DELETED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $expected = [ + 'Record deleted successfully [#' . $companyId . '].', + ]; + $actual = $data; + $this->assertSame($expected, $actual); + + /** + * Now deleting it again, will result in a 404 + */ + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerZeroCompanyId(): void + { + /** @var CompanyDeleteHandler $handler */ + $handler = $this->container->get(CompanyDeleteHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $command = $factory->delete(['id' => 0]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetManyTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetManyTest.php new file mode 100644 index 0000000..c55792f --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetManyTest.php @@ -0,0 +1,82 @@ + + * + * 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\Application\Company\Handler; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyGetManyHandler; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; + +final class CompanyHandlerGetManyTest extends AbstractUnitTestCase +{ + public function testHandlerSuccess(): void + { + /** @var CompanyGetManyHandler $handler */ + $handler = $this->container->get(CompanyGetManyHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $companyMigration = new CompaniesMigration($this->getConnection()); + + $company1 = $this->getNewCompany($companyMigration); + $company2 = $this->getNewCompany($companyMigration); + $company3 = $this->getNewCompany($companyMigration); + $company4 = $this->getNewCompany($companyMigration); + $company5 = $this->getNewCompany($companyMigration); + + $command = $factory->getMany([]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $expected = 5; + $actual = count($data); + $this->assertSame($expected, $actual); + + $this->assertSame('', $data); + $this->assertArrayHasKey($company1['com_id'], $data); + $this->assertArrayHasKey($company2['com_id'], $data); + $this->assertArrayHasKey($company3['com_id'], $data); + $this->assertArrayHasKey($company4['com_id'], $data); + $this->assertArrayHasKey($company5['com_id'], $data); + + $expected = $company1['com_name']; + $actual = $data[$company1['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company2['com_name']; + $actual = $data[$company2['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company3['com_name']; + $actual = $data[$company3['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company4['com_name']; + $actual = $data[$company4['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company5['com_name']; + $actual = $data[$company5['com_id']]['name']; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetTest.php new file mode 100644 index 0000000..e2c75e7 --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetTest.php @@ -0,0 +1,160 @@ + + * + * 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\Application\Company\Handler; + +use PayloadInterop\DomainStatus; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyGetHandler; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; + +use function array_key_first; + +final class CompanyHandlerGetTest extends AbstractUnitTestCase +{ + public function testHandlerEmptyCompanyId(): void + { + /** @var CompanyGetHandler $handler */ + $handler = $this->container->get(CompanyGetHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $command = $factory->get([]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerWithCompanyId(): void + { + /** @var CompanyGetHandler $handler */ + $handler = $this->container->get(CompanyGetHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $companyMigration = new CompaniesMigration($this->getConnection()); + + $dbCompany = $this->getNewCompany($companyMigration); + $companyId = $dbCompany['com_id']; + + $command = $factory->get(['id' => $companyId]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $company = $actual['data']; + $key = array_key_first($company); + $company = $company[$key]; + + $expected = $dbCompany['com_id']; + $actual = $company['id']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_name']; + $actual = $company['name']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_email']; + $actual = $company['email']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_phone']; + $actual = $company['phone']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_website']; + $actual = $company['website']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_address_line_1']; + $actual = $company['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_address_line_2']; + $actual = $company['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_city']; + $actual = $company['city']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_state_province']; + $actual = $company['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_zip_code']; + $actual = $company['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_country']; + $actual = $company['country']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_created_date']; + $actual = $company['createdDate']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_created_usr_id']; + $actual = $company['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_updated_date']; + $actual = $company['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $dbCompany['com_updated_usr_id']; + $actual = $company['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testHandlerWrongCompanyId(): void + { + /** @var CompanyGetHandler $handler */ + $handler = $this->container->get(CompanyGetHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $command = $factory->get(['id' => 999999]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPostTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPostTest.php new file mode 100644 index 0000000..0baa471 --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPostTest.php @@ -0,0 +1,398 @@ + + * + * 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\Application\Company\Handler; + +use DateTimeImmutable; +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyPostHandler; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapperInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepository; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Support\Registry; + +use function array_key_first; +use function uniqid; + +final class CompanyHandlerPostTest extends AbstractUnitTestCase +{ + public function testHandlerFailureNoIdReturned(): void + { + $companyRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'insert', + ] + ) + ->getMock() + ; + $companyRepository->method('insert')->willReturn(0); + + $this->container->setShared(CompanyRepository::class, $companyRepository); + + /** @var CompanyPostHandler $handler */ + $handler = $this->container->get(CompanyPostHandler::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + /** + * $companyData is a db record. We need a domain object here + */ + $domainCompany = $companyMapper->domain($companyData); + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainCompany); + + $domainData = $domainCompany->toArray(); + $command = $factory->insert($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot create database record: No id returned']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerFailurePdoError(): void + { + $message = uniqid('pdo-error-'); + $companyRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'insert', + ] + ) + ->getMock() + ; + $companyRepository + ->method('insert') + ->willThrowException(new PDOException($message)) + ; + + $this->container->setShared(CompanyRepository::class, $companyRepository); + + /** @var CompanyPostHandler $handler */ + $handler = $this->container->get(CompanyPostHandler::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + /** + * $companyData is a db record. We need a domain object here + */ + $domainCompany = $companyMapper->domain($companyData); + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainCompany); + + $domainData = $domainCompany->toArray(); + $command = $factory->insert($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot create database record: ' . $message]]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerFailureValidation(): void + { + /** @var CompanyPostHandler $handler */ + $handler = $this->container->get(CompanyPostHandler::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $companyData = $this->getNewCompanyData(); + + unset( + $companyData['com_name'], + $companyData['com_email'], + ); + + /** + * $companyData is a db record. We need a domain object here + */ + $domainCompany = $companyMapper->domain($companyData); + $domainData = $domainCompany->toArray(); + $command = $factory->insert($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + ['Field name is required'], + ['Field email is required'], + ['Field email must be an email address'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testHandlerSuccess(): void + { + /** @var CompanyPostHandler $handler */ + $handler = $this->container->get(CompanyPostHandler::class); + /** @var CompanyMapperInterface $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $companyData = $this->getNewCompanyData(); + + /** + * $companyData is a db record. We need a domain object here + */ + $domainUser = $companyMapper->domain($companyData); + + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainData = $domainUser->toArray(); + $command = $factory->insert($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $companyId = array_key_first($data); + + $this->assertGreaterThan(0, $companyId); + + $data = $data[$companyId]; + + $expected = $domainData['name']; + $actual = $data['name']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $expected = $domainData['phone']; + $actual = $data['phone']; + $this->assertSame($expected, $actual); + + $expected = $domainData['website']; + $actual = $data['website']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine1']; + $actual = $data['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine2']; + $actual = $data['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $domainData['city']; + $actual = $data['city']; + $this->assertSame($expected, $actual); + + $expected = $domainData['stateProvince']; + $actual = $data['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $domainData['zipCode']; + $actual = $data['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $domainData['country']; + $actual = $data['country']; + $this->assertSame($expected, $actual); + + $expected = $domainData['createdDate']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + $expected = $domainUser->id; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['updatedDate']; + $actual = $data['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $domainUser->id; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testHandlerSuccessEmptyDates(): void + { + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); + /** @var CompanyPostHandler $handler */ + $handler = $this->container->get(CompanyPostHandler::class); + /** @var CompanyMapperInterface $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $companyData = $this->getNewCompanyData(); + + /** + * $companyData is a db record. We need a domain object here + */ + $domainUser = $companyMapper->domain($companyData); + + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainUser); + + unset( + $companyData['com_created_date'], + $companyData['com_updated_date'], + ); + + /** + * $companyData is a db record. We need a domain object here + */ + $domainCompany = $companyMapper->domain($companyData); + $domainData = $domainCompany->toArray(); + $command = $factory->insert($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::CREATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $companyId = array_key_first($data); + + $this->assertGreaterThan(0, $companyId); + + $data = $data[$companyId]; + + $expected = $domainData['name']; + $actual = $data['name']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $expected = $domainData['phone']; + $actual = $data['phone']; + $this->assertSame($expected, $actual); + + $expected = $domainData['website']; + $actual = $data['website']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine1']; + $actual = $data['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine2']; + $actual = $data['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $domainData['city']; + $actual = $data['city']; + $this->assertSame($expected, $actual); + + $expected = $domainData['stateProvince']; + $actual = $data['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $domainData['zipCode']; + $actual = $data['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $domainData['country']; + $actual = $data['country']; + $this->assertSame($expected, $actual); + + $actual = $data['createdDate']; + $this->assertStringContainsString($today, $actual); + + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $actual = $data['updatedDate']; + $this->assertStringContainsString($today, $actual); + + $expected = 0; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPutTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPutTest.php new file mode 100644 index 0000000..7bf2334 --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPutTest.php @@ -0,0 +1,493 @@ + + * + * 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\Application\Company\Handler; + +use DateTimeImmutable; +use Faker\Factory; +use PayloadInterop\DomainStatus; +use PDOException; +use Phalcon\Api\Domain\Application\Company\Command\CompanyCommandFactory; +use Phalcon\Api\Domain\Application\Company\Handler\CompanyPutHandler; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; +use Phalcon\Support\Registry; + +use function uniqid; + +final class CompanyHandlerPutTest extends AbstractUnitTestCase +{ + public function testServiceFailureNoIdReturned(): void + { + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + $companyData = $this->getNewCompanyData(); + + $companyData['com_id'] = 1; + + $findByCompany = $companyMapper->domain($companyData); + + $userRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'update', + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('update')->willReturn(0); + $userRepository->method('findById')->willReturn($findByCompany); + + $this->container->setShared(CompanyRepository::class, $userRepository); + + /** + * Add a user in the repository for the session + */ + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + $sessionUser = $userMapper->domain(['id' => 1]); + $registry->set('user', $sessionUser); + + /** @var CompanyPutHandler $handler */ + $handler = $this->container->get(CompanyPutHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + /** + * Update company + */ + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + $updateCompany = $companyMapper->domain($companyData); + $updateCompany = $updateCompany->toArray(); + + $command = $factory->update($updateCompany); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot update database record: No id returned']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailurePdoError(): void + { + $message = uniqid('pdo-error-'); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + $companyData = $this->getNewCompanyData(); + + $companyData['com_id'] = 1; + + $findByCompany = $companyMapper->domain($companyData); + + $userRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'update', + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('findById')->willReturn($findByCompany); + $userRepository + ->method('update') + ->willThrowException(new PDOException($message)) + ; + + $this->container->setShared(CompanyRepository::class, $userRepository); + + /** + * Add a user in the repository for the session + */ + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + $sessionUser = $userMapper->domain(['id' => 1]); + $registry->set('user', $sessionUser); + + /** @var CompanyPutHandler $handler */ + $handler = $this->container->get(CompanyPutHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + /** + * $companyData is a db record. We need a domain object here + */ + $updateCompany = $companyMapper->domain($companyData); + $updateCompany = $updateCompany->toArray(); + + $command = $factory->update($updateCompany); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::ERROR; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Cannot update database record: ' . $message]]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailureRecordNotFound(): void + { + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + $userRepository = $this + ->getMockBuilder(CompanyRepository::class) + ->disableOriginalConstructor() + ->onlyMethods( + [ + 'findById', + ] + ) + ->getMock() + ; + $userRepository->method('findById')->willReturn(null); + + $this->container->setShared(CompanyRepository::class, $userRepository); + + /** @var CompanyPutHandler $handler */ + $handler = $this->container->get(CompanyPutHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + + /** + * Update user + */ + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; + + $updateCompany = $companyMapper->domain($companyData); + $updateCompany = $updateCompany->toArray(); + + $command = $factory->update($updateCompany); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::NOT_FOUND; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [['Record(s) not found']]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceFailureValidation(): void + { + /** @var CompanyPutHandler $handler */ + $handler = $this->container->get(CompanyPutHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + $companyData = $this->getNewCompanyData(); + + $companyData['com_email'] = 'wrong-email'; + unset($companyData['com_id']); + + /** + * $companyData is a db record. We need a domain object here + */ + $updateCompany = $companyMapper->domain($companyData); + $updateCompany = $updateCompany->toArray(); + + $command = $factory->update($updateCompany); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::INVALID; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('errors', $actual); + + $errors = $actual['errors']; + + $expected = [ + ['Field id is not a valid absolute integer and greater than 0'], + ['Field email must be an email address'], + ]; + $actual = $errors; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccess(): void + { + $faker = Factory::create(); + /** @var CompanyPutHandler $handler */ + $handler = $this->container->get(CompanyPutHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + + $migration = new CompaniesMigration($this->getConnection()); + $dbCompany = $this->getNewCompany($migration); + $companyId = $dbCompany['com_id']; + $userData = $this->getNewUserData(); + $userId = $faker->numberBetween(1, 10); + $userData['usr_id'] = $userId; + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_com_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainCompany = $companyMapper->domain($dbCompany); + $domainData = $domainCompany->toArray(); + $command = $factory->update($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey($companyId, $data); + + $data = $data[$companyId]; + + $expected = $domainData['id']; + $actual = $data['id']; + $this->assertSame($expected, $actual); + + $expected = $domainData['name']; + $actual = $data['name']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $expected = $domainData['phone']; + $actual = $data['phone']; + $this->assertSame($expected, $actual); + + $expected = $domainData['website']; + $actual = $data['website']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine1']; + $actual = $data['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine2']; + $actual = $data['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $domainData['city']; + $actual = $data['city']; + $this->assertSame($expected, $actual); + + $expected = $domainData['stateProvince']; + $actual = $data['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $domainData['zipCode']; + $actual = $data['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $domainData['country']; + $actual = $data['country']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = $domainData['createdDate']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $expected = $domainData['updatedDate']; + $actual = $data['updatedDate']; + $this->assertSame($expected, $actual); + + $expected = $userId; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } + + public function testServiceSuccessEmptyDates(): void + { + $faker = Factory::create(); + $now = new DateTimeImmutable(); + $today = $now->format('Y-m-d'); + /** @var CompanyPutHandler $handler */ + $handler = $this->container->get(CompanyPutHandler::class); + /** @var CompanyCommandFactory $factory */ + $factory = $this->container->get(CompanyCommandFactory::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + /** @var Registry $registry */ + $registry = $this->container->get(Registry::class); + + $migration = new CompaniesMigration($this->getConnection()); + $dbCompany = $this->getNewCompany($migration); + + $companyId = $dbCompany['com_id']; + $userData = $this->getNewUserData(); + $userId = $faker->numberBetween(1, 10); + $userData['usr_id'] = $userId; + + unset( + $dbCompany['com_updated_date'], + ); + + /** + * $userData is a db record. We need a domain object here + */ + $domainUser = $userMapper->domain($userData); + /** + * Store the domain user in the registry - to be used for *_usr_id + * field updates + */ + $registry->set('user', $domainUser); + + $domainCompany = $companyMapper->domain($dbCompany); + $domainData = $domainCompany->toArray(); + $command = $factory->update($domainData); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::UPDATED; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $this->assertArrayHasKey($companyId, $data); + + $data = $data[$companyId]; + + $expected = $domainData['id']; + $actual = $data['id']; + $this->assertSame($expected, $actual); + + $expected = $domainData['name']; + $actual = $data['name']; + $this->assertSame($expected, $actual); + + $expected = $domainData['email']; + $actual = $data['email']; + $this->assertSame($expected, $actual); + + $expected = $domainData['phone']; + $actual = $data['phone']; + $this->assertSame($expected, $actual); + + $expected = $domainData['website']; + $actual = $data['website']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine1']; + $actual = $data['addressLine1']; + $this->assertSame($expected, $actual); + + $expected = $domainData['addressLine2']; + $actual = $data['addressLine2']; + $this->assertSame($expected, $actual); + + $expected = $domainData['city']; + $actual = $data['city']; + $this->assertSame($expected, $actual); + + $expected = $domainData['stateProvince']; + $actual = $data['stateProvince']; + $this->assertSame($expected, $actual); + + $expected = $domainData['zipCode']; + $actual = $data['zipCode']; + $this->assertSame($expected, $actual); + + $expected = $domainData['country']; + $actual = $data['country']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = $dbCompany['com_created_date']; + $actual = $data['createdDate']; + $this->assertSame($expected, $actual); + + /** + * These have to be the same on an update + */ + $expected = 0; + $actual = $data['createdUserId']; + $this->assertSame($expected, $actual); + + $actual = $data['updatedDate']; + $this->assertStringContainsString($today, $actual); + + $expected = $userId; + $actual = $data['updatedUserId']; + $this->assertSame($expected, $actual); + } +} From c4ea841727c2e91f24d58de1e67555b9569eda4b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:07:20 -0600 Subject: [PATCH 297/301] [#.x] - added company criteria dto --- .../Company/DTO/CompanyCriteria.php | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/Domain/Infrastructure/DataSource/Company/DTO/CompanyCriteria.php diff --git a/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyCriteria.php b/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyCriteria.php new file mode 100644 index 0000000..0aa1cc3 --- /dev/null +++ b/src/Domain/Infrastructure/DataSource/Company/DTO/CompanyCriteria.php @@ -0,0 +1,133 @@ + + * + * 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\Infrastructure\DataSource\Company\DTO; + +use Phalcon\Api\Domain\Application\Company\Command\CompanyGetManyCommand; +use Phalcon\Api\Domain\Infrastructure\CommandBus\CommandInterface; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\CompanyTypes; + +/** + * Value object for database criteria + * + * @phpstan-import-type TCriteria from CompanyTypes + * @phpstan-import-type TCompanyDbRecordOptional from CompanyTypes + */ +final readonly class CompanyCriteria +{ + /** + * @param int|null $id + * @param string|null $name + * @param string|null $phone + * @param string|null $email + * @param string|null $website + * @param string|null $addressLine1 + * @param string|null $addressLine2 + * @param string|null $city + * @param string|null $stateProvince + * @param string|null $zipCode + * @param string|null $country + * @param string|null $createdDate + * @param int|null $createdUserId + * @param string|null $updatedDate + * @param int|null $updatedUserId + * @param int $page + * @param int $perPage + */ + private function __construct( + public ?int $id, + public ?string $name, + public ?string $phone, + public ?string $email, + public ?string $website, + public ?string $addressLine1, + public ?string $addressLine2, + public ?string $city, + public ?string $stateProvince, + public ?string $zipCode, + public ?string $country, + public ?string $createdDate, + public ?int $createdUserId, + public ?string $updatedDate, + public ?int $updatedUserId, + public int $page, + public int $perPage, + ) { + } + + /** + * @param CommandInterface $command + * + * @return self + */ + public static function fromDto(CommandInterface $command): self + { + /** @var CompanyGetManyCommand $command */ + return new self( + $command->id, + $command->name, + $command->phone, + $command->email, + $command->website, + $command->addressLine1, + $command->addressLine2, + $command->city, + $command->stateProvince, + $command->zipCode, + $command->country, + $command->createdDate, + $command->createdUserId, + $command->updatedDate, + $command->updatedUserId, + self::checkLimit($command->page, 1), + self::checkLimit($command->perPage, 10) + ); + } + + /** + * @return TCompanyDbRecordOptional + */ + public function toArray(): array + { + $criteria = [ + 'com_id' => $this->id, + 'com_name' => $this->name, + 'com_phone' => $this->phone, + 'com_email' => $this->email, + 'com_website' => $this->website, + 'com_address_line_1' => $this->addressLine1, + 'com_address_line_2' => $this->addressLine2, + 'com_city' => $this->city, + 'com_state_province' => $this->stateProvince, + 'com_zip_code' => $this->zipCode, + 'com_country' => $this->country, + 'com_created_date' => $this->createdDate, + 'com_created_usr_id' => $this->createdUserId, + 'com_updated_date' => $this->updatedDate, + 'com_updated_usr_id' => $this->updatedUserId, + ]; + + return array_filter($criteria); + } + + /** + * @param int $value + * @param int $defaultValue + * + * @return int + */ + private static function checkLimit(int $value, int $defaultValue): int + { + return $value > 0 ? $value : $defaultValue; + } +} From aca8ecc61fff77de6973f5f6470ed36c326e216e Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:07:42 -0600 Subject: [PATCH 298/301] [#.x] - added company dispatch get test --- .../Service/CompanyServiceDispatchTest.php | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/Unit/Domain/Application/Company/Service/CompanyServiceDispatchTest.php diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServiceDispatchTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServiceDispatchTest.php new file mode 100644 index 0000000..30e0d8a --- /dev/null +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServiceDispatchTest.php @@ -0,0 +1,117 @@ + + * + * 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\Application\Company\Service; + +use DateTimeImmutable; +use Phalcon\Api\Domain\Infrastructure\Constants\Cache as CacheConstants; +use Phalcon\Api\Domain\Infrastructure\Constants\Dates; +use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; +use Phalcon\Api\Domain\Infrastructure\Enums\Http\RoutesEnum; +use Phalcon\Api\Domain\Infrastructure\Env\EnvManager; +use Phalcon\Api\Tests\AbstractUnitTestCase; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; +use Phalcon\Api\Tests\Fixtures\Domain\Migrations\UsersMigration; +use Phalcon\Cache\Cache; +use PHPUnit\Framework\Attributes\BackupGlobals; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; + +use function json_decode; +use function ob_get_clean; +use function ob_start; +use function restore_error_handler; +use function time; + +#[BackupGlobals(true)] +final class CompanyServiceDispatchTest extends AbstractUnitTestCase +{ + #[RunInSeparateProcess] + public function testDispatchGet(): void + { + /** @var EnvManager $env */ + $env = $this->container->getShared(EnvManager::class); + /** @var Cache $cache */ + $cache = $this->container->getShared(Cache::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); + /** @var CompanyMapper $companyMapper */ + $companyMapper = $this->container->get(CompanyMapper::class); + + $companyMigration = new CompaniesMigration($this->getConnection()); + $userMigration = new UsersMigration($this->getConnection()); + + $dbUser = $this->getNewUser($userMigration); + $dbCompany = $this->getNewCompany($companyMigration); + $companyId = $dbCompany['com_id']; + $token = $this->getUserToken($dbUser); + $domainUser = $userMapper->domain($dbUser); + $domainCompany = $companyMapper->domain($dbCompany); + + /** + * Store the token in the cache + */ + $cacheKey = CacheConstants::getCacheTokenKey($domainUser, $token); + /** @var int $expiration */ + $expiration = $env->get( + 'TOKEN_EXPIRATION', + CacheConstants::CACHE_TOKEN_EXPIRY, + 'int' + ); + $expirationDate = (new DateTimeImmutable()) + ->modify('+' . $expiration . ' seconds') + ->format(Dates::DATE_TIME_FORMAT) + ; + + $payload = [ + 'token' => $token, + 'expiry' => $expirationDate, + ]; + + $cache->set($cacheKey, $payload, $expiration); + + $time = $_SERVER['REQUEST_TIME_FLOAT'] ?? time(); + $_SERVER = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME_FLOAT' => $time, + 'HTTP_AUTHORIZATION' => 'Bearer ' . $token, + 'REQUEST_URI' => RoutesEnum::companyGet->endpoint(), + ]; + + $_GET = [ + 'id' => $companyId, + ]; + + ob_start(); + require_once $env->appPath('public/index.php'); + $response = ob_get_clean(); + + $contents = json_decode($response, true); + + restore_error_handler(); + + $this->assertArrayHasKey('data', $contents); + $this->assertArrayHasKey('errors', $contents); + + $data = $contents['data']; + $errors = $contents['errors']; + + $expected = []; + $actual = $errors; + $this->assertSame($expected, $actual); + + $expected = [$companyId => $domainCompany->toArray()]; + $actual = $data; + $this->assertSame($expected, $actual); + } +} From 8ec4ab6491faa27209a70d4c52ccdb25eb663a20 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:12:22 -0600 Subject: [PATCH 299/301] [#.x] - added more tests --- .../Handler/CompanyHandlerGetManyTest.php | 83 +++++++++++++++++-- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetManyTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetManyTest.php index c55792f..f4b42f2 100644 --- a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetManyTest.php +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerGetManyTest.php @@ -19,6 +19,9 @@ use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Api\Tests\Fixtures\Domain\Migrations\CompaniesMigration; +use function count; +use function uniqid; + final class CompanyHandlerGetManyTest extends AbstractUnitTestCase { public function testHandlerSuccess(): void @@ -29,13 +32,18 @@ public function testHandlerSuccess(): void $factory = $this->container->get(CompanyCommandFactory::class); $companyMigration = new CompaniesMigration($this->getConnection()); - - $company1 = $this->getNewCompany($companyMigration); - $company2 = $this->getNewCompany($companyMigration); - $company3 = $this->getNewCompany($companyMigration); - $company4 = $this->getNewCompany($companyMigration); - $company5 = $this->getNewCompany($companyMigration); - + $companyName1 = uniqid('companyName'); + $companyName2 = uniqid('companyName'); + + $company1 = $this->getNewCompany($companyMigration, ['com_name' => $companyName1]); + $company2 = $this->getNewCompany($companyMigration, ['com_name' => $companyName1]); + $company3 = $this->getNewCompany($companyMigration, ['com_name' => $companyName2]); + $company4 = $this->getNewCompany($companyMigration, ['com_name' => $companyName2]); + $company5 = $this->getNewCompany($companyMigration, ['com_name' => $companyName2]); + + /** + * Make the call + */ $command = $factory->getMany([]); $payload = $handler->__invoke($command); @@ -52,7 +60,6 @@ public function testHandlerSuccess(): void $actual = count($data); $this->assertSame($expected, $actual); - $this->assertSame('', $data); $this->assertArrayHasKey($company1['com_id'], $data); $this->assertArrayHasKey($company2['com_id'], $data); $this->assertArrayHasKey($company3['com_id'], $data); @@ -78,5 +85,65 @@ public function testHandlerSuccess(): void $expected = $company5['com_name']; $actual = $data[$company5['com_id']]['name']; $this->assertSame($expected, $actual); + + /** + * Make the call for paginated + */ + $command = $factory->getMany(['page' => 2, 'perPage' => 2]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $expected = 2; + $actual = count($data); + $this->assertSame($expected, $actual); + + $this->assertArrayHasKey($company3['com_id'], $data); + $this->assertArrayHasKey($company4['com_id'], $data); + + $expected = $company3['com_name']; + $actual = $data[$company3['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company4['com_name']; + $actual = $data[$company4['com_id']]['name']; + $this->assertSame($expected, $actual); + + /** + * Make the call for name + */ + $command = $factory->getMany(['name' => $companyName1]); + $payload = $handler->__invoke($command); + + $expected = DomainStatus::SUCCESS; + $actual = $payload->getStatus(); + $this->assertSame($expected, $actual); + + $actual = $payload->getResult(); + $this->assertArrayHasKey('data', $actual); + + $data = $actual['data']; + + $expected = 2; + $actual = count($data); + $this->assertSame($expected, $actual); + + $this->assertArrayHasKey($company1['com_id'], $data); + $this->assertArrayHasKey($company2['com_id'], $data); + + $expected = $company1['com_name']; + $actual = $data[$company1['com_id']]['name']; + $this->assertSame($expected, $actual); + + $expected = $company2['com_name']; + $actual = $data[$company2['com_id']]['name']; + $this->assertSame($expected, $actual); } } From 172ca5c858302b6b3e70166b01b750b0d96e12dd Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:30:49 -0600 Subject: [PATCH 300/301] [#.x] - correcting tests --- .../Handler/CompanyHandlerDeleteTest.php | 6 +-- .../Handler/CompanyHandlerPostTest.php | 37 +++++++++++++---- .../Service/CompanyServiceDeleteTest.php | 6 +-- .../Service/CompanyServicePostTest.php | 41 +++++++++++++------ 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerDeleteTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerDeleteTest.php index e1dcdf3..1c650f4 100644 --- a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerDeleteTest.php +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerDeleteTest.php @@ -29,9 +29,9 @@ public function testHandlerWithCompanyId(): void $factory = $this->container->get(CompanyCommandFactory::class); /** - * We need to ask for a user to be deleted with an ID that does not - * exist in the database. To ensure that, we will create a user, - * delete it and then try to delete the same user with that ID + * We need to ask for a company to be deleted with an ID that does not + * exist in the database. To ensure that, we will create a company, + * delete it and then try to delete the same company with that ID */ $migration = new CompaniesMigration($this->getConnection()); $dbCompany = $this->getNewCompany($migration); diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPostTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPostTest.php index 0baa471..a73fbb3 100644 --- a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPostTest.php +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPostTest.php @@ -21,6 +21,7 @@ use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapper; use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Mapper\CompanyMapperInterface; use Phalcon\Api\Domain\Infrastructure\DataSource\Company\Repository\CompanyRepository; +use Phalcon\Api\Domain\Infrastructure\DataSource\User\Mapper\UserMapper; use Phalcon\Api\Tests\AbstractUnitTestCase; use Phalcon\Support\Registry; @@ -49,11 +50,15 @@ public function testHandlerFailureNoIdReturned(): void $handler = $this->container->get(CompanyPostHandler::class); /** @var CompanyMapper $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); /** @var CompanyCommandFactory $factory */ $factory = $this->container->get(CompanyCommandFactory::class); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; $companyData = $this->getNewCompanyData(); $companyData['com_id'] = 1; @@ -61,11 +66,12 @@ public function testHandlerFailureNoIdReturned(): void * $companyData is a db record. We need a domain object here */ $domainCompany = $companyMapper->domain($companyData); + $domainUser = $userMapper->domain($userData); /** * Store the domain user in the registry - to be used for *_com_id * field updates */ - $registry->set('user', $domainCompany); + $registry->set('user', $domainUser); $domainData = $domainCompany->toArray(); $command = $factory->insert($domainData); @@ -109,11 +115,15 @@ public function testHandlerFailurePdoError(): void $handler = $this->container->get(CompanyPostHandler::class); /** @var CompanyMapper $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); /** @var CompanyCommandFactory $factory */ $factory = $this->container->get(CompanyCommandFactory::class); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; $companyData = $this->getNewCompanyData(); $companyData['com_id'] = 1; @@ -121,11 +131,12 @@ public function testHandlerFailurePdoError(): void * $companyData is a db record. We need a domain object here */ $domainCompany = $companyMapper->domain($companyData); + $domainUser = $userMapper->domain($userData); /** * Store the domain user in the registry - to be used for *_com_id * field updates */ - $registry->set('user', $domainCompany); + $registry->set('user', $domainUser); $domainData = $domainCompany->toArray(); $command = $factory->insert($domainData); @@ -193,17 +204,23 @@ public function testHandlerSuccess(): void $handler = $this->container->get(CompanyPostHandler::class); /** @var CompanyMapperInterface $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); /** @var CompanyCommandFactory $factory */ $factory = $this->container->get(CompanyCommandFactory::class); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + $companyData = $this->getNewCompanyData(); /** * $companyData is a db record. We need a domain object here */ - $domainUser = $companyMapper->domain($companyData); + $domainCompany = $companyMapper->domain($companyData); + $domainUser = $userMapper->domain($userData); /** * Store the domain user in the registry - to be used for *_com_id @@ -211,7 +228,7 @@ public function testHandlerSuccess(): void */ $registry->set('user', $domainUser); - $domainData = $domainUser->toArray(); + $domainData = $domainCompany->toArray(); $command = $factory->insert($domainData); $payload = $handler->__invoke($command); @@ -295,17 +312,23 @@ public function testHandlerSuccessEmptyDates(): void $handler = $this->container->get(CompanyPostHandler::class); /** @var CompanyMapperInterface $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); /** @var CompanyCommandFactory $factory */ $factory = $this->container->get(CompanyCommandFactory::class); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + $companyData = $this->getNewCompanyData(); /** * $companyData is a db record. We need a domain object here */ - $domainUser = $companyMapper->domain($companyData); + $domainCompany = $companyMapper->domain($companyData); + $domainUser = $userMapper->domain($userData); /** * Store the domain user in the registry - to be used for *_com_id @@ -384,14 +407,14 @@ public function testHandlerSuccessEmptyDates(): void $actual = $data['createdDate']; $this->assertStringContainsString($today, $actual); - $expected = 0; + $expected = 1; $actual = $data['createdUserId']; $this->assertSame($expected, $actual); $actual = $data['updatedDate']; $this->assertStringContainsString($today, $actual); - $expected = 0; + $expected = 1; $actual = $data['updatedUserId']; $this->assertSame($expected, $actual); } diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServiceDeleteTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServiceDeleteTest.php index 21a6f35..892fb9d 100644 --- a/tests/Unit/Domain/Application/Company/Service/CompanyServiceDeleteTest.php +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServiceDeleteTest.php @@ -26,9 +26,9 @@ public function testServiceWithCompanyId(): void $service = $this->container->get(CompanyDeleteService::class); /** - * We need to ask for a user to be deleted with an ID that does not - * exist in the database. To ensure that, we will create a user, - * delete it and then try to delete the same user with that ID + * We need to ask for a company to be deleted with an ID that does not + * exist in the database. To ensure that, we will create a company, + * delete it and then try to delete the same company with that ID */ $migration = new CompaniesMigration($this->getConnection()); $dbCompany = $this->getNewCompany($migration); diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServicePostTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServicePostTest.php index a2b91ac..4e23634 100644 --- a/tests/Unit/Domain/Application/Company/Service/CompanyServicePostTest.php +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServicePostTest.php @@ -50,9 +50,13 @@ public function testServiceFailureNoIdReturned(): void $service = $this->container->get(CompanyPostService::class); /** @var CompanyMapper $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; $companyData = $this->getNewCompanyData(); $companyData['com_id'] = 1; @@ -60,11 +64,12 @@ public function testServiceFailureNoIdReturned(): void * $companyData is a db record. We need a domain object here */ $domainCompany = $companyMapper->domain($companyData); + $domainUser = $userMapper->domain($userData); /** * Store the domain user in the registry - to be used for *_com_id * field updates */ - $registry->set('user', $domainCompany); + $registry->set('user', $domainUser); $domainData = $domainCompany->toArray(); @@ -108,9 +113,13 @@ public function testServiceFailurePdoError(): void $service = $this->container->get(CompanyPostService::class); /** @var CompanyMapper $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; $companyData = $this->getNewCompanyData(); $companyData['com_id'] = 1; @@ -118,11 +127,12 @@ public function testServiceFailurePdoError(): void * $companyData is a db record. We need a domain object here */ $domainCompany = $companyMapper->domain($companyData); + $domainUser = $userMapper->domain($userData); /** * Store the domain user in the registry - to be used for *_com_id * field updates */ - $registry->set('user', $domainCompany); + $registry->set('user', $domainUser); $domainData = $domainCompany->toArray(); @@ -187,20 +197,22 @@ public function testServiceSuccess(): void /** @var CompanyPostService $service */ $service = $this->container->get(CompanyPostService::class); /** @var UserMapperInterface $companyMapper */ - $companyMapper = $this->container->get(UserMapper::class); + $userMapper = $this->container->get(UserMapper::class); /** @var CompanyMapperInterface $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); - $companyData = $this->getNewUserData(); - $companyData = $this->getNewCompanyData(); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; /** * $companyData is a db record. We need a domain object here */ - $domainUser = $companyMapper->domain($companyData); - + $domainCompany = $companyMapper->domain($companyData); + $domainUser = $userMapper->domain($userData); /** * Store the domain user in the registry - to be used for *_com_id * field updates @@ -292,16 +304,21 @@ public function testServiceSuccessEmptyDates(): void $service = $this->container->get(CompanyPostService::class); /** @var CompanyMapperInterface $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); + /** @var UserMapper $userMapper */ + $userMapper = $this->container->get(UserMapper::class); /** @var Registry $registry */ $registry = $this->container->get(Registry::class); - $companyData = $this->getNewCompanyData(); + $userData = $this->getNewUserData(); + $userData['usr_id'] = 1; + $companyData = $this->getNewCompanyData(); + $companyData['com_id'] = 1; /** * $companyData is a db record. We need a domain object here */ - $domainUser = $companyMapper->domain($companyData); - + $domainCompany = $companyMapper->domain($companyData); + $domainUser = $userMapper->domain($userData); /** * Store the domain user in the registry - to be used for *_com_id * field updates @@ -378,14 +395,14 @@ public function testServiceSuccessEmptyDates(): void $actual = $data['createdDate']; $this->assertStringContainsString($today, $actual); - $expected = 0; + $expected = 1; $actual = $data['createdUserId']; $this->assertSame($expected, $actual); $actual = $data['updatedDate']; $this->assertStringContainsString($today, $actual); - $expected = 0; + $expected = 1; $actual = $data['updatedUserId']; $this->assertSame($expected, $actual); } From 1301398da288338647d0bec9444f03cf648e034b Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Sat, 27 Dec 2025 12:36:43 -0600 Subject: [PATCH 301/301] [#.x] - correcting variable names --- .../Company/Handler/CompanyHandlerPutTest.php | 22 +++++++++---------- .../Company/Service/CompanyServicePutTest.php | 22 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPutTest.php b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPutTest.php index 7bf2334..fdd9343 100644 --- a/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPutTest.php +++ b/tests/Unit/Domain/Application/Company/Handler/CompanyHandlerPutTest.php @@ -40,7 +40,7 @@ public function testServiceFailureNoIdReturned(): void $findByCompany = $companyMapper->domain($companyData); - $userRepository = $this + $companyRepository = $this ->getMockBuilder(CompanyRepository::class) ->disableOriginalConstructor() ->onlyMethods( @@ -51,10 +51,10 @@ public function testServiceFailureNoIdReturned(): void ) ->getMock() ; - $userRepository->method('update')->willReturn(0); - $userRepository->method('findById')->willReturn($findByCompany); + $companyRepository->method('update')->willReturn(0); + $companyRepository->method('findById')->willReturn($findByCompany); - $this->container->setShared(CompanyRepository::class, $userRepository); + $this->container->setShared(CompanyRepository::class, $companyRepository); /** * Add a user in the repository for the session @@ -108,7 +108,7 @@ public function testServiceFailurePdoError(): void $findByCompany = $companyMapper->domain($companyData); - $userRepository = $this + $companyRepository = $this ->getMockBuilder(CompanyRepository::class) ->disableOriginalConstructor() ->onlyMethods( @@ -119,13 +119,13 @@ public function testServiceFailurePdoError(): void ) ->getMock() ; - $userRepository->method('findById')->willReturn($findByCompany); - $userRepository + $companyRepository->method('findById')->willReturn($findByCompany); + $companyRepository ->method('update') ->willThrowException(new PDOException($message)) ; - $this->container->setShared(CompanyRepository::class, $userRepository); + $this->container->setShared(CompanyRepository::class, $companyRepository); /** * Add a user in the repository for the session @@ -172,7 +172,7 @@ public function testServiceFailureRecordNotFound(): void { /** @var CompanyMapper $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); - $userRepository = $this + $companyRepository = $this ->getMockBuilder(CompanyRepository::class) ->disableOriginalConstructor() ->onlyMethods( @@ -182,9 +182,9 @@ public function testServiceFailureRecordNotFound(): void ) ->getMock() ; - $userRepository->method('findById')->willReturn(null); + $companyRepository->method('findById')->willReturn(null); - $this->container->setShared(CompanyRepository::class, $userRepository); + $this->container->setShared(CompanyRepository::class, $companyRepository); /** @var CompanyPutHandler $handler */ $handler = $this->container->get(CompanyPutHandler::class); diff --git a/tests/Unit/Domain/Application/Company/Service/CompanyServicePutTest.php b/tests/Unit/Domain/Application/Company/Service/CompanyServicePutTest.php index 1ad5ea2..f1dc5e5 100644 --- a/tests/Unit/Domain/Application/Company/Service/CompanyServicePutTest.php +++ b/tests/Unit/Domain/Application/Company/Service/CompanyServicePutTest.php @@ -39,7 +39,7 @@ public function testServiceFailureNoIdReturned(): void $findByCompany = $companyMapper->domain($companyData); - $userRepository = $this + $companyRepository = $this ->getMockBuilder(CompanyRepository::class) ->disableOriginalConstructor() ->onlyMethods( @@ -50,10 +50,10 @@ public function testServiceFailureNoIdReturned(): void ) ->getMock() ; - $userRepository->method('update')->willReturn(0); - $userRepository->method('findById')->willReturn($findByCompany); + $companyRepository->method('update')->willReturn(0); + $companyRepository->method('findById')->willReturn($findByCompany); - $this->container->setShared(CompanyRepository::class, $userRepository); + $this->container->setShared(CompanyRepository::class, $companyRepository); /** * Add a user in the repository for the session @@ -104,7 +104,7 @@ public function testServiceFailurePdoError(): void $findByCompany = $companyMapper->domain($companyData); - $userRepository = $this + $companyRepository = $this ->getMockBuilder(CompanyRepository::class) ->disableOriginalConstructor() ->onlyMethods( @@ -115,13 +115,13 @@ public function testServiceFailurePdoError(): void ) ->getMock() ; - $userRepository->method('findById')->willReturn($findByCompany); - $userRepository + $companyRepository->method('findById')->willReturn($findByCompany); + $companyRepository ->method('update') ->willThrowException(new PDOException($message)) ; - $this->container->setShared(CompanyRepository::class, $userRepository); + $this->container->setShared(CompanyRepository::class, $companyRepository); /** * Add a user in the repository for the session @@ -165,7 +165,7 @@ public function testServiceFailureRecordNotFound(): void { /** @var CompanyMapper $companyMapper */ $companyMapper = $this->container->get(CompanyMapper::class); - $userRepository = $this + $companyRepository = $this ->getMockBuilder(CompanyRepository::class) ->disableOriginalConstructor() ->onlyMethods( @@ -175,9 +175,9 @@ public function testServiceFailureRecordNotFound(): void ) ->getMock() ; - $userRepository->method('findById')->willReturn(null); + $companyRepository->method('findById')->willReturn(null); - $this->container->setShared(CompanyRepository::class, $userRepository); + $this->container->setShared(CompanyRepository::class, $companyRepository); /** @var CompanyPutService $service */ $service = $this->container->get(CompanyPutService::class);