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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ tests/ export-ignore
.gitignore export-ignore
AGENTS.md export-ignore
composer.* export-ignore
Makefile export-ignore
phpcs.xml export-ignore
phpstan.neon export-ignore
phpunit.xml export-ignore
Expand Down
16 changes: 5 additions & 11 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ name: "CI"

on:
push:
branches:
- "main"
pull_request:
branches:
- "main"
workflow_dispatch:

permissions:
Expand All @@ -17,7 +13,6 @@ concurrency:
cancel-in-progress: true

jobs:
# composer validation
composer:
name: "composer config validation"
runs-on: "ubuntu-latest"
Expand All @@ -30,9 +25,8 @@ jobs:
- name: "Validate composer.json"
run: "composer validate --strict"

# PHP lint, PHPUnit, PHPStan and PHPCS for different PHP versions
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
Expand Down Expand Up @@ -60,20 +54,20 @@ jobs:
extensions: "json"
coverage: "xdebug"
tools: "composer:v2"
cache: "composer"
- name: "install composer dependencies (locked)"
if: "matrix.dependencies == 'locked'"
run: "composer install --prefer-dist --no-interaction --no-progress"
- name: "install composer dependencies (lowest)"
if: "matrix.dependencies == 'lowest'"
run: "composer update --prefer-lowest --prefer-dist --no-interaction --no-progress"
- name: "audit dependencies for known security vulnerabilities"
if: "matrix.php-version == '8.1'"
run: "composer audit"
- name: "lint PHP files"
run: "find exceptions/ src/ tests/ -type f -name '*.php' -print0 | xargs -0 -n1 php -l"
- name: "run PHPUnit"
run: "php vendor/bin/phpunit --coverage-text"
run: "php vendor/bin/phpunit --coverage-text --colors=never"
- name: "run PHPStan"
run: "vendor/bin/phpstan analyse --no-progress"
run: "php vendor/bin/phpstan analyse --no-progress"
- name: "run PHPCS"
run: "vendor/bin/phpcs"
run: "php vendor/bin/phpcs"
140 changes: 85 additions & 55 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,31 @@
`php-sap/common` is a **PHP library** providing SAP RFC (Remote Function Call) abstractions
that are independent of the underlying PHP SAP extension (e.g. `saprfc` or `sapnwrfc`).
It defines abstract base classes, API type descriptors, configuration models, and exceptions
consumed by concrete SAP connector packages.
consumed by concrete SAP connector packages. Part of [PHP/SAP](https://php-sap.github.io).

Namespaces:
- `phpsap\classes\*` → `src/`
- `phpsap\exceptions\*` → `exceptions/`
- `tests\phpsap\classes\*` → `tests/`

## Developer Commands
## Ecosystem

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, and 8.3 (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`).
[PHP/SAP](https://php-sap.github.io) is split across five focused repositories that build on
each other instead of one monolithic package:

```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; 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
```
| 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/common`** — the generic implementation of `interfaces`.

This package implements the interfaces generically; extension-specific glue (marshaling
parameters to a native SAP module) belongs in a concrete connector like `saprfc-kralik`,
not here.

## Architecture

Expand Down Expand Up @@ -109,24 +84,79 @@ Both implement `ISapException` from `php-sap/interfaces`.
- Subclasses implement `connect()`, `execute()`, `extractApi()`.
- Stores parameters via `JsonSerializable`'s `set()`/`get()` methods.

## Testing Conventions

- Test helpers live in `tests/helper/` (e.g. `AbstractFunctionInstance.php` — provides
fake `extractApi()` and `invoke()` via static properties for controlled test scenarios).
- Tests mirror source layout: `tests/Api/`, `tests/Config/`, `tests/Util/`.
- PHPUnit 9, bootstrap: `vendor/autoload.php`, strict coverage enforced via `phpunit.xml`.
- PHPStan level 9 (`phpstan.neon`); intentional type mismatches in tests are suppressed
with `@phpstan-ignore-next-line`.

## Key Interfaces
### Key Interfaces

All public contracts are defined in `php-sap/interfaces` (vendor dependency):
`IApi`, `IApiElement`, `IValue`, `IMember`, `IStruct`, `ITable`,
`IFunction`, `IConfiguration`, `IConfigTypeA`, `IConfigTypeB`, `ISapException`.
When adding new public methods, check this package for the matching interface first.

## CI Tooling Notes
## 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, and
8.3 (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 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 exceptions/, 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
```

**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.

`phpstan/phpstan` and `squizlabs/php_codesniffer` are managed as Composer `require-dev`
dependencies (no separate download step needed, unlike `saprfc-kralik`'s `phpcs.phar`).
PHPStan runs at **level 9** (`phpstan.neon`); intentional type mismatches in tests are
suppressed with `@phpstan-ignore-next-line`.

## Conventions

- Test helpers live in `tests/helper/` (e.g. `AbstractFunctionInstance.php` — provides
fake `extractApi()` and `invoke()` via static properties for controlled test scenarios).
- Tests mirror source layout: `tests/Api/`, `tests/Config/`, `tests/Util/`.
- PHPUnit 9, bootstrap: `vendor/autoload.php`, strict coverage enforced via `phpunit.xml`.

- `phpstan/phpstan` and `squizlabs/php_codesniffer` are managed as Composer `require-dev` dependencies.
- CI runs `vendor/bin/phpstan` and `vendor/bin/phpcs` after `composer install`.
## Safe Change Strategy for Agents

- Before adding a public method, check `php-sap/interfaces` for the matching contract first
— this package must stay a faithful, generic implementation of those interfaces.
- Before adding a new SAP type, extend `TypeTrait`'s constants and the corresponding
`CastPrimitivesTrait` logic together; don't add one without the other.
- Extension-specific glue (e.g. marshaling parameters for a native SAP module) does not
belong here — that's the job of concrete connector packages like `saprfc-kralik`.
- Keep new code PHPStan level 9 clean; only suppress with `@phpstan-ignore-next-line` for
intentional test-only type mismatches, matching the existing pattern.
- 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 exceptions/, src/ and tests/ for PHP_VERSION
docker run --rm --init $(DOCKER_USER) $(DOCKER_MOUNT) \
"php:$(PHP_VERSION)-cli" sh -c "find exceptions 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 -t --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 -t --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 @@ -5,5 +5,16 @@
Exceptions and abstract classes containing logic for [PHP/SAP][phpsap] that is
not specific to the underlying PHP module.

## 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
```

[phpsap]: https://php-sap.github.io
[license-mit]: https://img.shields.io/badge/license-MIT-blue.svg
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"require": {
"php": "^8.1",
"ext-json": "*",
"php-sap/interfaces": "^5.1",
"php-sap/datetime": "^1.5"
"php-sap/interfaces": "^5.1.6",
"php-sap/datetime": "^1.5.2"
},
"autoload": {
"psr-4": {
Expand All @@ -34,8 +34,8 @@
}
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^9.6.33",
"phpstan/phpstan": "^2.2.3",
"squizlabs/php_codesniffer": "^4.0"
}
}