diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dfcb544 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.github +.php-cs-fixer.cache +vendor +coverage diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index e087a47..6dd3eaa 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -12,11 +12,16 @@ jobs: strategy: matrix: - php: [8.0, 8.1] + php: [8.3.36] steps: - uses: actions/checkout@v2 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Validate composer.json and composer.lock run: composer validate --strict diff --git a/.gitignore b/.gitignore index b990946..269dc20 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ composer.lock phpunit.xml cache.properties coverage.xml +/coverage/ .php_cs.cache .phpunit.result.cache .php-cs-fixer.cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3cf9bca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1 +FROM php:8.3.36-cli + +RUN apt-get update \ + && apt-get install -y --no-install-recommends unzip \ + && rm -rf /var/lib/apt/lists/* \ + && docker-php-ext-install pdo_mysql + +RUN pecl install xdebug \ + && docker-php-ext-enable xdebug + +ENV XDEBUG_MODE=off + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /app + +COPY . . + +RUN --mount=type=secret,id=host-ca,target=/run/secrets/host-ca.pem,required=false \ + if [ -s /run/secrets/host-ca.pem ]; then \ + cat /etc/ssl/certs/ca-certificates.crt /run/secrets/host-ca.pem > /tmp/ca-certificates.crt && \ + COMPOSER_CAFILE=/tmp/ca-certificates.crt \ + SSL_CERT_FILE=/tmp/ca-certificates.crt \ + composer install --prefer-dist --no-interaction --no-progress && \ + rm /tmp/ca-certificates.crt; \ + else \ + composer install --prefer-dist --no-interaction --no-progress; \ + fi + +CMD ["composer", "test"] diff --git a/README.md b/README.md index a76dcce..d0c9212 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,53 @@ $ composer require linio/database ## Tests -To run the test suite, you need install the dependencies via composer, then -run PHPUnit. +Run the test suite in Docker with PHP 8.3.36 and MySQL; PHP does not need to be +installed on the host: + + $ docker compose run --build --rm tests + +If your network uses a custom CA certificate bundle, set `SSL_CERT_FILE` to +that bundle. When Docker Bake requests permission to read the bundle outside +the project, build with access limited to that file, then run the tests: + +```bash +docker compose build --print | docker buildx bake --file - \ + --allow "fs.read=${SSL_CERT_FILE:?Set SSL_CERT_FILE to your CA bundle}" \ + --load tests +docker compose run --rm tests +``` + +Docker mounts the certificate bundle as a build secret to download Composer +dependencies. The temporary combined CA bundle is removed in the same build +step and is not stored in the image. + +The MySQL container remains running after the tests finish. Stop and remove it +with: + + $ docker compose down + +To run the test suite directly on the host instead, install the Composer +dependencies and run PHPUnit: $ composer install $ vendor/bin/phpunit +### Code coverage in Docker + +Rebuild the test image after updating the Dockerfile. Xdebug is installed but +disabled for normal test runs. Enable it to generate coverage reports: + +```bash +docker compose run --rm -e XDEBUG_MODE=coverage tests vendor/bin/phpunit \ + --coverage-text \ + --coverage-html /coverage/html \ + --coverage-clover /coverage/clover.xml +``` + +The `coverage` directory is mounted from the host so the reports remain +available after the container exits. Open the HTML report on macOS with +`open coverage/html/index.html`. Reports are excluded from Git and Docker builds. + ## Usage ```php diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..6a2f59b --- /dev/null +++ b/compose.yaml @@ -0,0 +1,32 @@ +services: + tests: + build: + context: . + secrets: + - host-ca + depends_on: + mysql: + condition: service_healthy + environment: + TEST_DATABASE_HOST: mysql + TEST_DATABASE_PORT: 3306 + TEST_DATABASE_DBNAME: framework_database_test + TEST_DATABASE_USERNAME: root + TEST_DATABASE_PASSWORD: "123456" + volumes: + - ./coverage:/coverage + + mysql: + image: mysql:8.4 + environment: + MYSQL_ROOT_PASSWORD: "123456" + MYSQL_DATABASE: framework_database_test + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p123456"] + interval: 5s + timeout: 5s + retries: 20 + +secrets: + host-ca: + file: ${SSL_CERT_FILE:-/dev/null} diff --git a/composer.json b/composer.json index df4741a..168588d 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "type": "library", "license": "BSD-3-Clause", "require": { - "php": "^8.1", + "php": "~8.3.36 || ~8.4.27", "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php index e9cca7b..e7ed4d7 100644 --- a/src/Adapter/AdapterInterface.php +++ b/src/Adapter/AdapterInterface.php @@ -78,7 +78,7 @@ public function rollBack(): void; /** * @throws DatabaseException */ - public function getLastInsertId(string $name = null): ?string; + public function getLastInsertId(?string $name = null): ?string; /** * @throws DatabaseException Support for the database has not been implemented yet diff --git a/src/Adapter/PdoAdapter.php b/src/Adapter/PdoAdapter.php index 4704496..1414ea5 100644 --- a/src/Adapter/PdoAdapter.php +++ b/src/Adapter/PdoAdapter.php @@ -224,7 +224,7 @@ public function rollBack(): void /** * @throws DatabaseException */ - public function getLastInsertId(string $name = null): ?string + public function getLastInsertId(?string $name = null): ?string { try { $lastInsertId = $this->getPdo()->lastInsertId($name); diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index 7ebdc14..877198e 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -260,7 +260,7 @@ public function rollBack(): bool /** * @throws DatabaseException */ - public function getLastInsertId(string $name = null): ?string + public function getLastInsertId(?string $name = null): ?string { try { return $this->getWriteAdapter()->getLastInsertId($name);