diff --git a/.gitattributes b/.gitattributes index 136bd42..c6992fa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,8 +3,10 @@ tests/ export-ignore .gitattributes export-ignore .gitignore export-ignore -phpcs.xml export-ignore +AGENTS.md export-ignore composer.* export-ignore +Makefile export-ignore +phpcs.xml export-ignore phpstan.neon export-ignore phpunit.xml export-ignore README.md export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f357059 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 17d4001..11b9d85 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,21 +9,25 @@ permissions: contents: "read" concurrency: - group: "${{ github.workflow }}-${{ github.ref }}" + group: "ci-${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: composer: name: "composer config validation" runs-on: "ubuntu-latest" - timeout-minutes: 5 + timeout-minutes: 10 steps: - - uses: "actions/checkout@v7" + - name: "git checkout" + uses: "actions/checkout@v7" + with: + persist-credentials: false - name: "Validate composer.json" run: "composer validate --strict" php: - name: "PHP ${{ matrix.php-version }} (${{ matrix.dependencies }}) - PHPUnit, PHPStan, PHPCS" + name: "PHP ${{ matrix.php-version }} (${{ matrix.dependencies }})" + needs: "composer" runs-on: "ubuntu-latest" timeout-minutes: 15 strategy: @@ -42,42 +46,28 @@ jobs: steps: - name: "git checkout" uses: "actions/checkout@v7" - + with: + persist-credentials: false - name: "setup PHP" uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" coverage: "xdebug" - - - name: "get composer cache directory" - id: "composer-cache" - run: "echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT" - - - name: "cache composer dependencies" - uses: "actions/cache@v4" - with: - path: "${{ steps.composer-cache.outputs.dir }}" - key: "${{ runner.os }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}" - restore-keys: | - ${{ runner.os }}-composer-${{ matrix.dependencies }}- - ${{ runner.os }}-composer- - + tools: "composer:v2" - name: "install composer dependencies (locked)" if: "matrix.dependencies == 'locked'" run: "composer install --prefer-dist --no-progress --no-interaction" - - name: "install composer dependencies (lowest)" if: "matrix.dependencies == 'lowest'" run: "composer update --prefer-lowest --prefer-dist --no-progress --no-interaction" - + - name: "audit dependencies for known security vulnerabilities" + if: "matrix.php-version == '8.1'" + run: "composer audit" - name: "lint PHP files" run: "find src tests -type f -name '*.php' -print0 | xargs -0 -n1 php -l" - - name: "run PHPUnit" run: "php vendor/bin/phpunit --coverage-text --colors=never" - - name: "run PHPStan" - run: "php vendor/bin/phpstan analyse -c phpstan.neon --no-progress" - + run: "php vendor/bin/phpstan analyse --no-progress" - name: "run PHPCS" - run: "php vendor/bin/phpcs --standard=phpcs.xml" + run: "php vendor/bin/phpcs" diff --git a/AGENTS.md b/AGENTS.md index d8416b2..dea35fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,81 +1,106 @@ -# AGENTS.md - php-sap/datetime +# AGENTS.md -## Quick Overview +## Project Overview This library adds SAP date/time format support on top of PHP core date classes. Production code is intentionally minimal: two classes in `src/` and no runtime dependencies. +Part of [PHP/SAP](https://php-sap.github.io). + +## Ecosystem + +[PHP/SAP](https://php-sap.github.io) is split across five focused repositories that build on +each other instead of one monolithic package: + +| Repository | Role | Depends on (`composer.json`) | +|-----------------------------|---------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| +| `php-sap/interfaces` | Contract-only interfaces (`IApi`, `IConfiguration`, `IFunction`, exceptions). No concrete classes. | — | +| `php-sap/datetime` | SAP date/time format support on top of native `DateTime`/`DateInterval`. | — | +| `php-sap/common` | Generic abstract classes, API/config value objects, and exceptions implementing `interfaces`. | `interfaces`, `datetime` | +| `php-sap/integration-tests` | Shared abstract PHPUnit test infrastructure and SAP module mocks reused by concrete connector packages. | `interfaces`, `common`, `datetime` | +| `php-sap/saprfc-kralik` | Concrete adapter for Gregor Kralik's `ext-sapnwrfc` extension. | `interfaces`, `common` (+ `integration-tests` for tests only) | + +**→ You are here: `php-sap/datetime`** — the SAP date/time formatting helper. + +This package has no dependency on any other PHP/SAP package and no runtime dependencies at +all; it is consumed by `common` (and, transitively, by every connector) purely for SAP +date/time parsing. Keep it that way — don't introduce a dependency back into the ecosystem. + +## Architecture -## Architecture That Matters - `src/SapDateTime.php`: extends `DateTime` and centralizes SAP format handling. - `src/SapDateTime.php`: `createFromFormat()` dispatches by SAP constants (`SAP_WEEK`, `SAP_DATE`, `SAP_TIME`, `SAP_TIMESTAMP`). - `src/SapDateTime.php`: `createFromSapWeek()` validates with `SapDateTime::$sapWeekRegex` before parsing. - `src/SapDateInterval.php`: extends `DateInterval` and overrides `createFromDateString()` to parse SAP `HHMMSS`. - Design intent: keep native `DateTime`/`DateInterval` behavior while adding SAP-specific factories. -## Core Data Flow +### Core Data Flow - Input SAP string -> validate (regex for week/time) -> convert to PHP-compatible format -> return native-compatible object. - Invalid SAP input returns `false` from factory methods (`DateTime|false`, `DateInterval|false`). - Timezone support flows through `DateTimeZone|null` factory arguments in `SapDateTime`. -## Developer Workflow (Repo-Local) +### Integration Points +- Upstream API surface: PHP `DateTime`, `DateInterval`, `DateTimeZone`. +- External libraries are dev-only (PHPUnit, PHP_CodeSniffer, PHPStan) via `composer.json`. +- Package namespace is `phpsap\DateTime\` (PSR-4), tests use `tests\phpsap\DateTime\`. -All commands run inside official PHP Docker images so the host machine does not need a -local PHP installation. Use PHP 8.1, 8.2, 8.3 and 8.4 (matching the CI matrix in -`.github/workflows/main.yml`) for anything version-sensitive (PHPStan, PHP lint). -If you are behind a proxy, forward `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` into the -container whenever the command needs network access (e.g. `composer install`). +## Developer Workflows + +All commands run through the `Makefile` via Docker, so the host machine does not need a +local PHP installation. Run `make help` for the full target list. Use PHP 8.1, 8.2, 8.3, +and 8.4 (matching the CI matrix in `.github/workflows/main.yml`) for anything +version-sensitive (PHPStan, PHP lint, tests). If you are behind a proxy, `install` and +`audit` already forward `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`; pass +`CA_CERT_FILE=/path/to/ca.pem` to trust a corporate proxy root CA inside the container. ```bash -# Install/update dependencies (needs network access -> forward proxy settings) -docker run --rm --init --interactive --tty \ - --user "$(id -u)":"$(id -g)" \ - --env HTTP_PROXY --env HTTPS_PROXY --env NO_PROXY \ - --volume "$(pwd)":/app --workdir /app \ - composer:2 install - -# Run tests (no network access needed) -docker run --rm --init \ - --user "$(id -u)":"$(id -g)" \ - --volume "$(pwd)":/app --workdir /app \ - php:8.1-cli php vendor/bin/phpunit - -# Fix code style (run first, no network access needed) -docker run --rm --init \ - --user "$(id -u)":"$(id -g)" \ - --volume "$(pwd)":/app --workdir /app \ - php:8.1-cli php vendor/bin/phpcbf - -# Check remaining style issues (no network access needed) -docker run --rm --init \ - --user "$(id -u)":"$(id -g)" \ - --volume "$(pwd)":/app --workdir /app \ - php:8.1-cli php vendor/bin/phpcs - -# Run static analysis for every supported PHP version (no network access needed; -# --memory-limit=-1 works around the image's low default memory_limit) -for PHP_VERSION in 8.1 8.2 8.3 8.4; do - docker run --rm --init \ - --user "$(id -u)":"$(id -g)" \ - --volume "$(pwd)":/app --workdir /app \ - "php:${PHP_VERSION}-cli" php vendor/bin/phpstan analyse --memory-limit=-1 -done +# Install/update dependencies for a given PHP version (set DEPENDENCIES_LOWEST=1 for +# --prefer-lowest, matching the CI "lowest" matrix job) +make install PHP_VERSION=8.1 + +# Run PHPUnit +make test PHP_VERSION=8.1 + +# Syntax-check every .php file in src/ and tests/, matches CI +make lint PHP_VERSION=8.1 + +# Run PHPStan +make analyze PHP_VERSION=8.1 + +# Auto-fix code style (run this before "sniff") +make beautify PHP_VERSION=8.1 + +# Check code style (uses phpcs.xml) +make sniff PHP_VERSION=8.1 + +# Check dependencies for known vulnerabilities +make audit + +# Run composer validate --strict +make validate ``` -## Conventions You Should Follow Here +**Always use these Makefile targets instead of inventing ad-hoc `docker run`/`composer`/ +`php` commands.** If a task needs something the Makefile doesn't expose directly (e.g. +PHPUnit for a single test file/method), take the exact `docker run` invocation from the +matching Makefile target (image, `DOCKER_USER`, `DOCKER_MOUNT`, env forwarding) and only +append the extra arguments — don't build the command from scratch. + +## Conventions - Use strict typing in every PHP file: `declare(strict_types=1);`. - Keep explicit return unions for factories (example: `DateTime|false` in `SapDateTime::createFromFormat`). - Keep public SAP format constants on `SapDateTime` as the source of truth. - Extend behavior via small static factories instead of introducing new service layers. - Match test style in `tests/SapDateTimeTest.php` and `tests/SapDateIntervalTest.php`: data providers with broad valid/invalid cases. -## Integration Points -- Upstream API surface: PHP `DateTime`, `DateInterval`, `DateTimeZone`. -- External libraries are dev-only (PHPUnit, PHP_CodeSniffer, PHPStan) via `composer.json`. -- Package namespace is `phpsap\DateTime\` (PSR-4), tests use `tests\phpsap\DateTime\`. - -## Safe Extension Pattern +## Safe Change Strategy for Agents - Add a new SAP format by: 1. adding a constant in `SapDateTime`, 2. adding validation/parsing in `SapDateTime::createFromFormat()`, 3. adding focused provider-driven tests in `tests/SapDateTimeTest.php`. - Preserve compatibility with native PHP behavior and existing false-on-invalid semantics. +- Don't add a dependency on `interfaces`/`common`/any other PHP/SAP package; this library + must stay usable standalone. +- Write documentation, comments, and new code in English to match the repository style. +- Always run QA/build commands through the `Makefile` targets, not self-invented `docker run` + commands. For one-off variants (a single test, a single file), base the invocation on the + relevant Makefile target and only append the extra arguments. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3e5f13e --- /dev/null +++ b/Makefile @@ -0,0 +1,95 @@ +SHELL := /bin/sh +.DEFAULT_GOAL := help + +COMPOSER_IMAGE := composer:2 +COMPOSER_CACHE_DIR := $(HOME)/.cache/composer +DOCKER_USER := --user "$$(id -u)":"$$(id -g)" +DOCKER_MOUNT := --volume "$$(pwd)":/app --workdir /app + +# --prefer-lowest only has an effect on "composer update", not "composer +# install" (which just reproduces composer.lock) - switch commands so the +# flag actually does something, matching the CI "lowest" matrix job. +ifdef DEPENDENCIES_LOWEST +COMPOSER_INSTALL_CMD := update --prefer-lowest +else +COMPOSER_INSTALL_CMD := install +endif + +# Optional: set CA_CERT_FILE to a PEM file (e.g. a corporate proxy root CA) +# to make it trusted for HTTPS network access inside the containers used by +# "install" and "audit" (e.g. 'make install PHP_VERSION=8.1 CA_CERT_FILE=/path/to/ca.pem'). +ifdef CA_CERT_FILE +CA_MOUNT := --volume "$(CA_CERT_FILE)":/tmp/extra-ca.crt:ro +CA_TRUST_CMD := cat /etc/ssl/certs/ca-certificates.crt /tmp/extra-ca.crt > /tmp/ca-bundle.pem && export CURL_CA_BUNDLE=/tmp/ca-bundle.pem SSL_CERT_FILE=/tmp/ca-bundle.pem && +else +CA_MOUNT := +CA_TRUST_CMD := +endif + +.PHONY: help clean check-php-version install test lint analyze beautify sniff audit validate + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}' + +clean: ## Remove vendor/ composer.lock and .phpunit.result.cache (reset to a fresh checkout) + rm -rf vendor composer.lock .phpunit.result.cache + +check-php-version: + @if [ -z "$(PHP_VERSION)" ]; then \ + echo "Error: PHP_VERSION must be set, e.g. 'make $(MAKECMDGOALS) PHP_VERSION=8.1'." >&2; \ + exit 1; \ + fi + +install: check-php-version ## Install composer dependencies for PHP_VERSION (set DEPENDENCIES_LOWEST for --prefer-lowest, CA_CERT_FILE for a corporate proxy CA) + @mkdir -p "$(COMPOSER_CACHE_DIR)" + docker run --rm -t --init $(DOCKER_USER) \ + --env HTTP_PROXY --env HTTPS_PROXY --env NO_PROXY \ + --env COMPOSER_CACHE_DIR=/tmp/composer-cache \ + --volume "$(COMPOSER_CACHE_DIR)":/tmp/composer-cache \ + $(CA_MOUNT) \ + $(DOCKER_MOUNT) \ + $(COMPOSER_IMAGE) sh -c '\ + $(CA_TRUST_CMD) \ + composer config platform.php "$(PHP_VERSION)" && \ + composer $(COMPOSER_INSTALL_CMD) --prefer-dist --no-interaction --no-progress; \ + status=$$?; \ + composer config --unset platform.php; \ + composer config --unset platform 2>/dev/null; \ + composer config --unset config 2>/dev/null; \ + if [ $$status -eq 0 ]; then \ + composer update --lock --no-interaction --no-progress; \ + status=$$?; \ + fi; \ + exit $$status \ + ' + +test: check-php-version ## Run PHPUnit for PHP_VERSION + docker run --rm -t --init $(DOCKER_USER) $(DOCKER_MOUNT) \ + "php:$(PHP_VERSION)-cli" php vendor/bin/phpunit + +lint: check-php-version ## Syntax-check every .php file in src/ and tests/ for PHP_VERSION + docker run --rm --init $(DOCKER_USER) $(DOCKER_MOUNT) \ + "php:$(PHP_VERSION)-cli" sh -c "find src tests -type f -name '*.php' -print0 | xargs -0 -n1 php -l" + +analyze: check-php-version ## Run PHPStan for PHP_VERSION + docker run --rm -t --init $(DOCKER_USER) $(DOCKER_MOUNT) \ + "php:$(PHP_VERSION)-cli" php vendor/bin/phpstan analyse --memory-limit=-1 + +beautify: check-php-version ## Run PHPCBF (auto-fix code style) for PHP_VERSION + docker run --rm --init $(DOCKER_USER) $(DOCKER_MOUNT) \ + "php:$(PHP_VERSION)-cli" php vendor/bin/phpcbf + +sniff: check-php-version ## Run PHPCS (code style check) for PHP_VERSION + docker run --rm --init $(DOCKER_USER) $(DOCKER_MOUNT) \ + "php:$(PHP_VERSION)-cli" php vendor/bin/phpcs + +audit: ## Run composer audit (checks dependencies for known vulnerabilities; CA_CERT_FILE for a corporate proxy CA) + docker run --rm --init $(DOCKER_USER) \ + --env HTTP_PROXY --env HTTPS_PROXY --env NO_PROXY \ + $(CA_MOUNT) $(DOCKER_MOUNT) \ + $(COMPOSER_IMAGE) sh -c '$(CA_TRUST_CMD) composer audit' + +validate: ## Run composer validate --strict + docker run --rm --init $(DOCKER_USER) $(DOCKER_MOUNT) \ + $(COMPOSER_IMAGE) composer validate --strict diff --git a/README.md b/README.md index df15738..629dce8 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,17 @@ echo $dateTime->format(SapDateTime::SAP_TIMESTAMP) . PHP_EOL; */ ``` +## Development + +All development commands (install, test, lint, analyze, beautify, sniff, audit, +validate) run via Docker through the `Makefile`, so no local PHP installation is +needed. Run `make help` to list all targets. Most targets require `PHP_VERSION`, +e.g.: + +```sh +make install PHP_VERSION=8.1 +``` + [license-mit]: https://img.shields.io/badge/license-MIT-blue.svg [datetime]: https://www.php.net/manual/en/class.datetime.php [dateinterval]: https://www.php.net/manual/en/class.dateinterval.php diff --git a/composer.json b/composer.json index fb18b5c..1a8e3e7 100644 --- a/composer.json +++ b/composer.json @@ -28,9 +28,9 @@ "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "^9.6", - "squizlabs/php_codesniffer": "^3.7", - "phpstan/phpstan": "^2.2" + "phpunit/phpunit": "^9.6.33", + "squizlabs/php_codesniffer": "^4.0", + "phpstan/phpstan": "^2.2.3" }, "autoload": { "psr-4": {