Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
42 changes: 16 additions & 26 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
125 changes: 75 additions & 50 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.

95 changes: 95 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down