From 1fa050f0b7fb858e2df417ccb08cddb2afd784eb Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 6 Feb 2026 13:17:01 +0100 Subject: [PATCH 1/8] Convert to ExApp architecture and add CI/CD workflows Convert from PHP Nextcloud app to ExApp with Docker-based deployment. Add standardized CI/CD workflows for unstable release, dev-to-beta PR creation, branch protection, and lint checks. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/build-exapp.yaml | 60 ++++ .../pull-request-from-branch-check.yaml | 18 ++ .../workflows/pull-request-lint-check.yaml | 21 ++ .../workflows/push-development-to-beta.yaml | 44 +++ .github/workflows/unstable-release.yaml | 177 +++++++++++ Dockerfile | 63 ++++ Makefile | 24 ++ README.md | 115 +++++-- appinfo/info.xml | 94 ++++-- appinfo/routes.php | 7 - css/style.css | 5 - entrypoint.sh | 6 + ex_app/lib/main.py | 291 ++++++++++++++++++ lib/AppInfo/Application.php | 25 -- lib/Controller/PageController.php | 25 -- package.json | 53 ---- requirements.txt | 3 + src/App.vue | 24 -- src/main.js | 12 - templates/index.php | 12 - webpack.config.js | 21 -- 21 files changed, 864 insertions(+), 236 deletions(-) create mode 100644 .github/workflows/build-exapp.yaml create mode 100644 .github/workflows/pull-request-from-branch-check.yaml create mode 100644 .github/workflows/pull-request-lint-check.yaml create mode 100644 .github/workflows/push-development-to-beta.yaml create mode 100644 .github/workflows/unstable-release.yaml create mode 100644 Dockerfile create mode 100644 Makefile delete mode 100644 appinfo/routes.php delete mode 100644 css/style.css create mode 100644 entrypoint.sh create mode 100644 ex_app/lib/main.py delete mode 100644 lib/AppInfo/Application.php delete mode 100644 lib/Controller/PageController.php delete mode 100644 package.json create mode 100644 requirements.txt delete mode 100644 src/App.vue delete mode 100644 src/main.js delete mode 100644 templates/index.php delete mode 100644 webpack.config.js diff --git a/.github/workflows/build-exapp.yaml b/.github/workflows/build-exapp.yaml new file mode 100644 index 0000000..cf8593b --- /dev/null +++ b/.github/workflows/build-exapp.yaml @@ -0,0 +1,60 @@ +name: Build and Push ExApp Docker Image + +on: + push: + branches: + - main + - beta + tags: + - 'v*' + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: conductionnl/openklant-exapp + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/pull-request-from-branch-check.yaml b/.github/workflows/pull-request-from-branch-check.yaml new file mode 100644 index 0000000..ef097b6 --- /dev/null +++ b/.github/workflows/pull-request-from-branch-check.yaml @@ -0,0 +1,18 @@ +name: Main Branch Protection + +on: + pull_request: + branches: + - main + +jobs: + check-branch: + runs-on: ubuntu-latest + steps: + - name: Check branch + run: | + if [[ ${GITHUB_HEAD_REF} != development ]] && [[ ${GITHUB_HEAD_REF} != documentation ]] && ! [[ ${GITHUB_HEAD_REF} =~ ^hotfix/ ]]; + then + echo "Error: Pull request must come from 'development', 'documentation' or 'hotfix/' branch" + exit 1 + fi diff --git a/.github/workflows/pull-request-lint-check.yaml b/.github/workflows/pull-request-lint-check.yaml new file mode 100644 index 0000000..4a8c874 --- /dev/null +++ b/.github/workflows/pull-request-lint-check.yaml @@ -0,0 +1,21 @@ +name: Lint Check + +on: + pull_request: + branches: + - development + - main + +jobs: + lint-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install dependencies + run: npm i + + - name: Linting + run: npm run lint diff --git a/.github/workflows/push-development-to-beta.yaml b/.github/workflows/push-development-to-beta.yaml new file mode 100644 index 0000000..a23f924 --- /dev/null +++ b/.github/workflows/push-development-to-beta.yaml @@ -0,0 +1,44 @@ +name: Create PR to Beta + +permissions: + contents: write + pull-requests: write + +on: + push: + branches: + - development + +jobs: + create-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Create or update PR to beta + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check if beta branch exists + if ! git ls-remote --heads origin beta | grep -q beta; then + echo "Beta branch does not exist yet. Creating from development..." + git push origin origin/development:refs/heads/beta + fi + + # Check if a PR already exists + EXISTING_PR=$(gh pr list --base beta --head development --state open --json number --jq '.[0].number' || echo "") + + if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then + echo "PR #$EXISTING_PR already exists, it will auto-update with new commits" + else + gh pr create \ + --base beta \ + --head development \ + --title "Release: merge development into beta" \ + --body "Automated PR to sync development changes to beta for beta release. + + Merging this PR will trigger the beta release workflow." + fi diff --git a/.github/workflows/unstable-release.yaml b/.github/workflows/unstable-release.yaml new file mode 100644 index 0000000..9c0d185 --- /dev/null +++ b/.github/workflows/unstable-release.yaml @@ -0,0 +1,177 @@ +name: Unstable Release + +on: + push: + branches: + - development + +jobs: + release-management: + runs-on: ubuntu-latest + steps: + + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ssh-key: ${{ secrets.DEPLOY_KEY }} + + - name: Set app env + run: | + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Get current version and append unstable suffix + id: increment_version + run: | + git fetch origin main + main_version=$(git show origin/main:appinfo/info.xml | grep -oP '(?<=)[^<]+' || echo "") + current_version=$(grep -oP '(?<=)[^<]+' appinfo/info.xml || echo "") + + IFS='.' read -ra main_version_parts <<< "$main_version" + next_patch=$((main_version_parts[2] + 1)) + + unstable_counter=1 + if [[ $current_version =~ -unstable\.([0-9]+)$ ]]; then + current_patch=$(echo $current_version | grep -oP '^[0-9]+\.[0-9]+\.(\d+)' | cut -d. -f3) + if [ "$current_patch" -eq "$next_patch" ]; then + unstable_counter=$((BASH_REMATCH[1] + 1)) + fi + fi + + unstable_version="${main_version_parts[0]}.${main_version_parts[1]}.${next_patch}-unstable.${unstable_counter}" + + echo "NEW_VERSION=$unstable_version" >> $GITHUB_ENV + echo "new_version=$unstable_version" >> $GITHUB_OUTPUT + echo "Main version: $main_version" + echo "Current version: $current_version" + echo "Using unstable version: $unstable_version" + + - name: Update version in info.xml + run: | + sed -i "s|.*|${{ env.NEW_VERSION }}|" appinfo/info.xml + + - name: Commit version update + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + else + git add appinfo/info.xml + git commit -m "Bump unstable version to ${{ env.NEW_VERSION }} [skip ci]" + git push + fi + + - name: Prepare Signing Certificate and Key + run: | + echo "${{ secrets.NEXTCLOUD_SIGNING_CERT }}" > signing-cert.crt + echo "${{ secrets.NEXTCLOUD_SIGNING_KEY }}" > signing-key.key + + - name: Install npm dependencies + uses: actions/setup-node@v3 + with: + node-version: '18.x' + + - name: Set up PHP and install extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: zip, gd + + - run: npm ci + - run: npm run build + - run: composer install --no-dev --optimize-autoloader --classmap-authoritative + + - name: Copy the package files into the package + run: | + mkdir -p package/${{ github.event.repository.name }} + rsync -av --progress \ + --exclude='/package' \ + --exclude='/.git' \ + --exclude='/.github' \ + --exclude='/.cursor' \ + --exclude='/.vscode' \ + --exclude='/node_modules' \ + --exclude='/src' \ + --exclude='/tests' \ + --exclude='/package.json' \ + --exclude='/package-lock.json' \ + --exclude='/composer.json' \ + --exclude='/composer.lock' \ + --exclude='/phpcs.xml' \ + --exclude='/phpmd.xml' \ + --exclude='/psalm.xml' \ + --exclude='/phpunit.xml' \ + --exclude='/.phpunit.cache' \ + --exclude='.phpunit.result.cache' \ + --exclude='/jest.config.js' \ + --exclude='/webpack.config.js' \ + --exclude='/tsconfig.json' \ + --exclude='/.babelrc' \ + --exclude='/.eslintrc.js' \ + --exclude='/.prettierrc' \ + --exclude='/stylelint.config.js' \ + --exclude='/.gitignore' \ + --exclude='/.gitattributes' \ + --exclude='/signing-key.key' \ + --exclude='/signing-cert.crt' \ + ./ package/${{ github.event.repository.name }}/ + + - name: Create Tarball + run: | + cd package && tar -czf ../nextcloud-release.tar.gz ${{ github.event.repository.name }} + + - name: Sign the TAR.GZ file with OpenSSL + run: | + openssl dgst -sha512 -sign signing-key.key nextcloud-release.tar.gz | openssl base64 -out nextcloud-release.signature + + - name: Upload tarball as artifact + uses: actions/upload-artifact@v4 + with: + name: nextcloud-release-${{ env.NEW_VERSION }} + path: | + nextcloud-release.tar.gz + nextcloud-release.signature + retention-days: 30 + + - name: Git Version + id: version + uses: codacy/git-version@2.7.1 + with: + release-branch: development + + - name: Upload Unstable Release + uses: ncipollo/release-action@v1.12.0 + with: + tag: v${{ env.NEW_VERSION }} + name: Unstable Release ${{ env.NEW_VERSION }} + draft: false + prerelease: true + skipIfReleaseExists: true + + - name: Attach tarball to GitHub release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: nextcloud-release.tar.gz + asset_name: ${{ env.APP_NAME }}-${{ env.NEW_VERSION }}.tar.gz + tag: v${{ env.NEW_VERSION }} + overwrite: true + + - name: Upload app to Nextcloud appstore + uses: nextcloud-releases/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 + with: + app_name: ${{ env.APP_NAME }} + appstore_token: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }} + download_url: https://github.com/${{ github.repository }}/releases/download/v${{ env.NEW_VERSION }}/${{ env.APP_NAME }}-${{ env.NEW_VERSION }}.tar.gz + app_private_key: ${{ secrets.NEXTCLOUD_SIGNING_KEY }} + nightly: true + + - name: Verify release + run: | + echo "App version: ${{ env.NEW_VERSION }}" + echo "Tarball contents:" + tar -tvf nextcloud-release.tar.gz | head -50 + echo "info.xml contents:" + tar -xOf nextcloud-release.tar.gz ${{ env.APP_NAME }}/appinfo/info.xml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e587b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# OpenKlant ExApp for Nextcloud +# Wraps OpenKlant customer interaction registry with AppAPI integration +# +# OpenKlant is part of the Common Ground ecosystem for Dutch municipalities. +# It requires: +# - PostgreSQL database +# - Redis for caching (optional) +# +# See: https://github.com/maykinmedia/open-klant + +# Use upstream OpenKlant image as base (already has Django, uwsgi, etc.) +FROM maykinmedia/open-klant:2.2.0 + +# Install additional dependencies for ExApp wrapper +USER root + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + tini \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies for ExApp wrapper (FastAPI, uvicorn, httpx) +RUN pip install --no-cache-dir \ + "fastapi>=0.109.0" \ + "uvicorn>=0.27.0" \ + "httpx>=0.26.0" + +# Copy ExApp wrapper +COPY ex_app /app/ex_app +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Create directories for media and static files +RUN mkdir -p /app/media /app/static /app/log && \ + chown -R nobody:nogroup /app/media /app/static /app/log + +WORKDIR /app + +# Environment variables (set by AppAPI) +ENV APP_HOST=0.0.0.0 +ENV APP_PORT=9000 +ENV PYTHONUNBUFFERED=1 + +# OpenKlant configuration +ENV OPENKLANT_PORT=8000 +ENV DJANGO_SETTINGS_MODULE=openklant.conf.docker + +# OpenKlant requires these to be set (defaults for development) +ENV DB_HOST=localhost +ENV DB_NAME=openklant +ENV DB_USER=openklant +ENV DB_PASSWORD=openklant +ENV SECRET_KEY=change-me-in-production +ENV ALLOWED_HOSTS=* + +# Expose ports: 9000 for AppAPI, 8000 for OpenKlant +EXPOSE 9000 8000 + +# Health check - just verify the wrapper is responding (any status is ok during init) +HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \ + CMD curl -s http://localhost:${APP_PORT:-9000}/heartbeat | grep -q status || exit 1 + +ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24083aa --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: build push test clean + +APP_ID = openklant +REGISTRY = ghcr.io +IMAGE = conductionnl/$(APP_ID)-exapp +VERSION ?= latest + +build: + docker build -t $(REGISTRY)/$(IMAGE):$(VERSION) . + +push: build + docker push $(REGISTRY)/$(IMAGE):$(VERSION) + +test: + docker run --rm -it \ + -e APP_ID=$(APP_ID) \ + -e APP_VERSION=0.1.0 \ + -e APP_SECRET=test \ + -e NEXTCLOUD_URL=http://localhost \ + -p 9000:9000 \ + $(REGISTRY)/$(IMAGE):$(VERSION) + +clean: + docker rmi $(REGISTRY)/$(IMAGE):$(VERSION) || true diff --git a/README.md b/README.md index 4da73fd..d476209 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,13 @@ -# OpenKlant for Nextcloud +# OpenKlant ExApp for Nextcloud -Nextcloud integration app for [OpenKlant](https://github.com/maykinmedia/open-klant) customer interaction registry. +Nextcloud ExApp (External Application) that integrates [OpenKlant](https://github.com/maykinmedia/open-klant) customer interaction registry. ## About This App -This is a **Nextcloud wrapper app** that provides integration between Nextcloud and an external OpenKlant server. It does not contain the OpenKlant platform itself - it connects your Nextcloud instance to a running OpenKlant deployment. +This is a **Nextcloud ExApp** that packages the OpenKlant customer interaction registry as a containerized application managed by Nextcloud's AppAPI. When you install this app, Nextcloud will automatically deploy and manage the OpenKlant container. **For OpenKlant documentation, see:** https://github.com/maykinmedia/open-klant -## What This App Does - -- Adds an OpenKlant entry to the Nextcloud navigation -- Provides a UI within Nextcloud for managing customer interactions -- Links customer contacts to cases (zaken) and Nextcloud files -- Bridges the Common Ground / ZGW ecosystem with Nextcloud - ## What is OpenKlant? [OpenKlant](https://github.com/maykinmedia/open-klant) is an open-source customer interaction registry for Dutch municipalities and government organizations. Built by [Maykin Media](https://www.maykinmedia.nl/), it is part of the [Common Ground](https://commonground.nl/) ecosystem. @@ -24,54 +17,110 @@ APIs provided by OpenKlant: - **Contactmomenten API** - Contact moments registry (phone, email, in-person) - **Klanten API** - Customer information registry +## What This App Does + +- Packages OpenKlant as a Nextcloud ExApp +- Nextcloud automatically manages the container lifecycle +- Provides customer interaction APIs directly within Nextcloud +- Integrates with Nextcloud's AppAPI for seamless deployment + ## Requirements -- Nextcloud 28 or higher -- PHP 8.0 or higher -- A running [OpenKlant](https://github.com/maykinmedia/open-klant) server instance +- Nextcloud 30 or higher +- AppAPI app installed and configured with a deploy daemon +- Docker environment for ExApp containers + +### External Dependencies + +OpenKlant requires additional services for full functionality: + +| Service | Purpose | Required | +|---------|---------|----------| +| PostgreSQL | Database | Yes | +| Redis | Caching (optional) | No | ## Installation -### From the Nextcloud App Store +### Via Nextcloud App Store -Search for "OpenKlant" in your Nextcloud app store and click Install. +1. Ensure AppAPI is installed and configured +2. Search for "OpenKlant" in the Nextcloud app store +3. Click Install - Nextcloud will pull and start the container -### Manual Installation +### Manual Registration -1. Download the latest release from [GitHub Releases](https://github.com/ConductionNL/openklant/releases) -2. Extract to your Nextcloud `apps` or `custom_apps` directory -3. Enable the app: `occ app:enable openklant` +```bash +# Register the ExApp with AppAPI +docker exec -u www-data nextcloud php occ app_api:app:register \ + openklant your_daemon_name \ + --info-xml /path/to/appinfo/info.xml \ + --force-scopes + +# Enable the ExApp +docker exec -u www-data nextcloud php occ app_api:app:enable openklant +``` ## Configuration -After installation, configure the OpenKlant server URL and authentication credentials in the Nextcloud admin settings. OpenKlant supports Token, JWT, and OIDC authentication methods. +Configure via Nextcloud Admin Settings or environment variables: + +| Variable | Description | +|----------|-------------| +| `DB_HOST` | PostgreSQL database host | +| `DB_NAME` | Database name | +| `DB_USER` | Database username | +| `DB_PASSWORD` | Database password | +| `SECRET_KEY` | Django secret key | +| `ALLOWED_HOSTS` | Allowed hostnames (comma-separated) | ## Development +### Building the Docker Image + ```bash -# Install dependencies -composer install -npm install +# Build locally +make build -# Build frontend -npm run build +# Push to registry +make push + +# Test locally +make test +``` -# Watch for changes -npm run watch +### Project Structure -# Run linting -composer phpcs -npm run lint ``` +openklant/ +├── appinfo/ +│ └── info.xml # ExApp manifest +├── ex_app/ +│ └── lib/ +│ └── main.py # FastAPI wrapper for AppAPI +├── Dockerfile # Container definition +├── entrypoint.sh # Container startup +├── requirements.txt # Python dependencies +└── Makefile # Build automation +``` + +## Architecture + +This ExApp uses a FastAPI wrapper that: + +1. Implements AppAPI lifecycle endpoints (`/heartbeat`, `/init`, `/enabled`) +2. Runs Django migrations during initialization +3. Starts OpenKlant using uWSGI (production WSGI server) +4. Proxies requests to the OpenKlant backend +5. Reports health status back to Nextcloud ## Related Projects | Project | Description | Links | |---------|-------------|-------| | **OpenKlant** | Customer interaction registry | [GitHub](https://github.com/maykinmedia/open-klant) / [Maykin Media](https://www.maykinmedia.nl/) | -| **OpenZaak** | ZGW API backend (cases, documents) | [Website](https://openzaak.org/) / [Docs](https://open-zaak.readthedocs.io/) | -| **Valtimo** | BPM and case management platform | [Website](https://www.valtimo.nl/) / [Docs](https://docs.valtimo.nl/) | -| **Open Register** | Nextcloud register management | [GitHub](https://github.com/ConductionNL/openregister) | +| **Nextcloud AppAPI** | External app framework | [GitHub](https://github.com/nextcloud/app_api) / [Docs](https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/) | +| **OpenZaak** | ZGW API backend | [GitHub](https://github.com/open-zaak/open-zaak) / [Docs](https://open-zaak.readthedocs.io/) | +| **Valtimo** | BPM and case management | [Website](https://www.valtimo.nl/) / [Docs](https://docs.valtimo.nl/) | ## License diff --git a/appinfo/info.xml b/appinfo/info.xml index f2f231c..87f87bb 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -3,7 +3,7 @@ xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd"> openklant OpenKlant - OpenKlant customer interaction management integration for Nextcloud + OpenKlant customer interaction management for Nextcloud 0.1.0 agpl Conduction - OpenKlant office social https://github.com/ConductionNL/OpenKlant @@ -33,23 +32,80 @@ Integrate OpenKlant customer interaction registry with your Nextcloud instance. https://github.com/maykinmedia/open-klant https://github.com/maykinmedia/open-klant + https://github.com/maykinmedia/open-klant - - pgsql - sqlite - mysql - + - - - openklant - OpenKlant - openklant.page.index - app.svg - link - - + + + localhost:5000 + openklant-exapp + test + + + + + .* + GET,POST,PUT,DELETE,PATCH,OPTIONS + USER + [] + + + + + + DB_HOST + Database Host + PostgreSQL database host + + + DB_NAME + Database Name + PostgreSQL database name + + + DB_USER + Database User + PostgreSQL database user + + + DB_PASSWORD + Database Password + PostgreSQL database password + + + SECRET_KEY + Secret Key + Django secret key (generate a random string) + + + ALLOWED_HOSTS + Allowed Hosts + Comma-separated list of allowed hostnames (use * for development) + + + KEYCLOAK_URL + Keycloak URL + Keycloak server URL for SSO (e.g., http://keycloak:8080) + + + KEYCLOAK_REALM + Keycloak Realm + Keycloak realm name (default: commonground) + + + KEYCLOAK_CLIENT_ID + Keycloak Client ID + OIDC client ID for this app (default: openklant) + + + KEYCLOAK_CLIENT_SECRET + Keycloak Client Secret + OIDC client secret for this app + + + diff --git a/appinfo/routes.php b/appinfo/routes.php deleted file mode 100644 index 3b3edce..0000000 --- a/appinfo/routes.php +++ /dev/null @@ -1,7 +0,0 @@ - [ - ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], - ], -]; diff --git a/css/style.css b/css/style.css deleted file mode 100644 index 3d09146..0000000 --- a/css/style.css +++ /dev/null @@ -1,5 +0,0 @@ -/* OpenKlant Nextcloud App Styles */ - -#openklant { - padding: 20px; -} diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..1dab0f1 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +# Start the FastAPI wrapper for AppAPI communication +echo "Starting OpenKlant ExApp wrapper..." +exec python3 -m uvicorn ex_app.lib.main:app --host "${APP_HOST:-0.0.0.0}" --port "${APP_PORT:-9000}" diff --git a/ex_app/lib/main.py b/ex_app/lib/main.py new file mode 100644 index 0000000..698b297 --- /dev/null +++ b/ex_app/lib/main.py @@ -0,0 +1,291 @@ +""" +OpenKlant ExApp - FastAPI wrapper for Nextcloud AppAPI integration + +OpenKlant is a customer interaction registry for Dutch municipalities. +See: https://github.com/maykinmedia/open-klant +""" +import os +import subprocess +import asyncio +import base64 +from contextlib import asynccontextmanager + +import httpx +from fastapi import FastAPI, Request, BackgroundTasks +from fastapi.responses import JSONResponse, Response + +# Environment variables set by AppAPI +APP_ID = os.environ.get("APP_ID", "openklant") +APP_VERSION = os.environ.get("APP_VERSION", "0.1.0") +APP_SECRET = os.environ.get("APP_SECRET", "") +APP_HOST = os.environ.get("APP_HOST", "0.0.0.0") +APP_PORT = int(os.environ.get("APP_PORT", "9000")) +NEXTCLOUD_URL = os.environ.get("NEXTCLOUD_URL", "http://nextcloud") + +# OpenKlant configuration - Django/uWSGI runs on 8000 +OPENKLANT_PORT = int(os.environ.get("OPENKLANT_PORT", "8000")) +OPENKLANT_PROCESS = None + +# Keycloak/OIDC configuration +KEYCLOAK_URL = os.environ.get("KEYCLOAK_URL", "") +KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM", "commonground") +KEYCLOAK_CLIENT_ID = os.environ.get("KEYCLOAK_CLIENT_ID", "openklant") +KEYCLOAK_CLIENT_SECRET = os.environ.get("KEYCLOAK_CLIENT_SECRET", "") + + +def get_auth_header() -> dict: + """Generate AppAPI authentication header""" + auth = base64.b64encode(f":{APP_SECRET}".encode()).decode() + return { + "EX-APP-ID": APP_ID, + "EX-APP-VERSION": APP_VERSION, + "AUTHORIZATION-APP-API": auth, + } + + +async def report_status(progress: int) -> None: + """Report initialization progress to Nextcloud""" + try: + async with httpx.AsyncClient() as client: + await client.put( + f"{NEXTCLOUD_URL}/ocs/v1.php/apps/app_api/apps/status", + headers=get_auth_header(), + json={"progress": progress}, + timeout=10, + ) + except Exception as e: + print(f"Failed to report status: {e}") + + +def run_management_command(command: list, timeout: int = 120) -> bool: + """Run a Django management command""" + env = os.environ.copy() + env["DJANGO_SETTINGS_MODULE"] = "openklant.conf.docker" + try: + result = subprocess.run( + ["python", "/app/src/manage.py"] + command, + cwd="/app/src", + env=env, + capture_output=True, + text=True, + timeout=timeout, + ) + if result.returncode != 0: + print(f"Command {command} failed: {result.stderr}") + return False + return True + except subprocess.TimeoutExpired: + print(f"Command {command} timed out") + return False + except Exception as e: + print(f"Command {command} error: {e}") + return False + + +def get_oidc_env() -> dict: + """Get OIDC environment variables for Django if Keycloak is configured""" + if not KEYCLOAK_URL: + return {} + + oidc_url = f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}" + return { + # Mozilla Django OIDC settings + "OIDC_RP_CLIENT_ID": KEYCLOAK_CLIENT_ID, + "OIDC_RP_CLIENT_SECRET": KEYCLOAK_CLIENT_SECRET, + "OIDC_OP_AUTHORIZATION_ENDPOINT": f"{oidc_url}/protocol/openid-connect/auth", + "OIDC_OP_TOKEN_ENDPOINT": f"{oidc_url}/protocol/openid-connect/token", + "OIDC_OP_USER_ENDPOINT": f"{oidc_url}/protocol/openid-connect/userinfo", + "OIDC_OP_JWKS_ENDPOINT": f"{oidc_url}/protocol/openid-connect/certs", + "OIDC_OP_LOGOUT_ENDPOINT": f"{oidc_url}/protocol/openid-connect/logout", + # Enable OIDC authentication + "USE_OIDC_FOR_ADMIN_LOGIN": "True", + } + + +def start_openklant() -> None: + """Start the OpenKlant service using uWSGI""" + global OPENKLANT_PROCESS + if OPENKLANT_PROCESS is not None: + return + + env = os.environ.copy() + env["DJANGO_SETTINGS_MODULE"] = "openklant.conf.docker" + + # Add OIDC configuration if Keycloak is configured + env.update(get_oidc_env()) + if KEYCLOAK_URL: + print(f"OIDC configured with Keycloak at {KEYCLOAK_URL}") + + # Start OpenKlant using uWSGI (production server) + OPENKLANT_PROCESS = subprocess.Popen( + [ + "uwsgi", + "--http", f"0.0.0.0:{OPENKLANT_PORT}", + "--module", "openklant.wsgi:application", + "--chdir", "/app/src", + "--static-map", "/static=/app/static", + "--static-map", "/media=/app/media", + "--master", + "--processes", "2", + "--threads", "2", + "--harakiri", "60", + "--max-requests", "1000", + ], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=False, + ) + print(f"OpenKlant started with PID: {OPENKLANT_PROCESS.pid}") + + +def stop_openklant() -> None: + """Stop the OpenKlant service""" + global OPENKLANT_PROCESS + if OPENKLANT_PROCESS is not None: + OPENKLANT_PROCESS.terminate() + try: + OPENKLANT_PROCESS.wait(timeout=30) + except subprocess.TimeoutExpired: + OPENKLANT_PROCESS.kill() + OPENKLANT_PROCESS = None + print("OpenKlant stopped") + + +async def wait_for_openklant(timeout: int = 120) -> bool: + """Wait for OpenKlant to become healthy""" + for _ in range(timeout): + try: + async with httpx.AsyncClient() as client: + # OpenKlant root returns 200 or redirects + resp = await client.get( + f"http://localhost:{OPENKLANT_PORT}/", + timeout=2, + follow_redirects=False, + ) + if resp.status_code in (200, 302, 301): + return True + except Exception: + pass + await asyncio.sleep(1) + return False + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan handler""" + print(f"OpenKlant ExApp starting on {APP_HOST}:{APP_PORT}") + yield + stop_openklant() + print("OpenKlant ExApp shutdown complete") + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/heartbeat") +async def heartbeat(): + """Health check endpoint for AppAPI""" + try: + async with httpx.AsyncClient() as client: + resp = await client.get( + f"http://localhost:{OPENKLANT_PORT}/", + timeout=5, + follow_redirects=False, + ) + if resp.status_code in (200, 302, 301): + return JSONResponse({"status": "ok"}) + except Exception: + pass + return JSONResponse({"status": "error"}, status_code=503) + + +@app.post("/init") +async def init(background_tasks: BackgroundTasks): + """Initialization endpoint called by AppAPI during deployment""" + async def do_init(): + await report_status(0) + print("Starting OpenKlant initialization...") + + await report_status(10) + # Run database migrations + print("Running database migrations...") + if not run_management_command(["migrate", "--noinput"]): + print("WARNING: Migrations failed - database may not be configured") + + await report_status(30) + # Collect static files + print("Collecting static files...") + run_management_command(["collectstatic", "--noinput"]) + + await report_status(50) + # Start OpenKlant + start_openklant() + + await report_status(70) + if await wait_for_openklant(timeout=120): + await report_status(100) + print("OpenKlant initialization complete") + else: + print("OpenKlant failed to start - check database configuration") + await report_status(0) + + background_tasks.add_task(do_init) + return JSONResponse({"status": "init_started"}) + + +@app.put("/enabled") +async def enabled(request: Request): + """Enable/disable endpoint called by AppAPI""" + data = await request.json() + is_enabled = data.get("enabled", False) + + if is_enabled: + start_openklant() + await wait_for_openklant(timeout=90) + else: + stop_openklant() + + return JSONResponse({"status": "ok"}) + + +@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) +async def proxy(request: Request, path: str): + """Proxy all other requests to OpenKlant""" + try: + async with httpx.AsyncClient() as client: + url = f"http://localhost:{OPENKLANT_PORT}/{path}" + + # Forward headers, including Authorization for OIDC tokens + headers = { + k: v for k, v in request.headers.items() + if k.lower() not in ("host", "content-length") + } + + resp = await client.request( + method=request.method, + url=url, + content=await request.body(), + headers=headers, + params=request.query_params, + timeout=60, + ) + + return Response( + content=resp.content, + status_code=resp.status_code, + headers={ + k: v for k, v in resp.headers.items() + if k.lower() not in ("content-encoding", "transfer-encoding") + }, + ) + except httpx.RequestError as e: + return JSONResponse( + {"error": f"Proxy error: {str(e)}"}, + status_code=502, + ) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host=APP_HOST, port=APP_PORT) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php deleted file mode 100644 index 3bbee09..0000000 --- a/lib/AppInfo/Application.php +++ /dev/null @@ -1,25 +0,0 @@ -=0.109.0 +uvicorn>=0.27.0 +httpx>=0.26.0 diff --git a/src/App.vue b/src/App.vue deleted file mode 100644 index d10b4cc..0000000 --- a/src/App.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 6a5e897..0000000 --- a/src/main.js +++ /dev/null @@ -1,12 +0,0 @@ -import Vue from 'vue' -import App from './App.vue' - -Vue.mixin({ methods: { t, n } }) - -const appElement = document.getElementById('openklant') -if (appElement) { - new Vue({ - el: appElement, - render: h => h(App), - }) -} diff --git a/templates/index.php b/templates/index.php deleted file mode 100644 index 93ec11d..0000000 --- a/templates/index.php +++ /dev/null @@ -1,12 +0,0 @@ - - -
-
-
diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 3639794..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,21 +0,0 @@ -const path = require('path') -const webpackConfig = require('@nextcloud/webpack-vue-config') - -const buildMode = process.env.NODE_ENV -const isDev = buildMode === 'development' -webpackConfig.devtool = isDev ? 'cheap-source-map' : 'source-map' - -webpackConfig.stats = { - colors: true, - modules: false, -} - -const appId = 'openklant' -webpackConfig.entry = { - main: { - import: path.join(__dirname, 'src', 'main.js'), - filename: appId + '-main.js', - }, -} - -module.exports = webpackConfig From 3b3902aefefb7f1e544d4b72c223da5c88849609 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 6 Feb 2026 12:18:21 +0000 Subject: [PATCH 2/8] Bump unstable version to 0.1.1-unstable.1 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 87f87bb..6897f56 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -18,7 +18,7 @@ This is an ExApp (External Application) - Nextcloud will automatically manage th **Note:** OpenKlant requires PostgreSQL database for full functionality. ]]> - 0.1.0 + 0.1.1-unstable.1 agpl Conduction office From bc427b5c0253ea882e5dfa732ad382340d881818 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 6 Feb 2026 13:36:35 +0100 Subject: [PATCH 3/8] Update branch protection: main from beta, beta from development Enforce that PRs to main can only come from beta or hotfix branches, and PRs to beta can only come from development or hotfix branches. Co-Authored-By: Claude Opus 4.6 --- .../pull-request-from-branch-check.yaml | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request-from-branch-check.yaml b/.github/workflows/pull-request-from-branch-check.yaml index ef097b6..1c264ed 100644 --- a/.github/workflows/pull-request-from-branch-check.yaml +++ b/.github/workflows/pull-request-from-branch-check.yaml @@ -1,9 +1,10 @@ -name: Main Branch Protection +name: Branch Protection on: pull_request: branches: - main + - beta jobs: check-branch: @@ -11,8 +12,21 @@ jobs: steps: - name: Check branch run: | - if [[ ${GITHUB_HEAD_REF} != development ]] && [[ ${GITHUB_HEAD_REF} != documentation ]] && ! [[ ${GITHUB_HEAD_REF} =~ ^hotfix/ ]]; - then - echo "Error: Pull request must come from 'development', 'documentation' or 'hotfix/' branch" - exit 1 + TARGET="${{ github.base_ref }}" + SOURCE="${{ github.head_ref }}" + + if [[ "$TARGET" == "main" ]]; then + if [[ "$SOURCE" != "beta" ]] && ! [[ "$SOURCE" =~ ^hotfix ]]; then + echo "Error: Pull requests to main must come from 'beta' or a branch starting with 'hotfix'" + echo "Source branch: $SOURCE" + exit 1 + fi + elif [[ "$TARGET" == "beta" ]]; then + if [[ "$SOURCE" != "development" ]] && ! [[ "$SOURCE" =~ ^hotfix ]]; then + echo "Error: Pull requests to beta must come from 'development' or a branch starting with 'hotfix'" + echo "Source branch: $SOURCE" + exit 1 + fi fi + + echo "Branch check passed: $SOURCE -> $TARGET" From 07ce477b92bc7a11d200344c805ac9c4d84d4605 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 6 Feb 2026 12:41:17 +0000 Subject: [PATCH 4/8] Bump unstable version to 0.1.1-unstable.2 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 6897f56..8bef49f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -18,7 +18,7 @@ This is an ExApp (External Application) - Nextcloud will automatically manage th **Note:** OpenKlant requires PostgreSQL database for full functionality. ]]> - 0.1.1-unstable.1 + 0.1.1-unstable.2 agpl Conduction office From ab8a90f4f25cc97a599dc9cef712f5fdbb7e4ae7 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 5 Mar 2026 11:20:41 +0100 Subject: [PATCH 5/8] fix: Add PHPCS/PHPMD/Psalm quality tooling (#3) * fix: Return HTTP 200 from heartbeat endpoint during init wait AppAPI requires a successful heartbeat before calling /init, but the wrapper returned 503 when the internal Django service wasn't running yet. Return 200 with {"status":"waiting"} to break the chicken-and-egg cycle. Co-Authored-By: Claude Opus 4.6 * fix: Add PHPCS/PHPMD/Psalm quality tooling Add phpcs.xml, phpmd.xml, psalm.xml, custom NamedParametersSniff, and update composer.json with quality dependencies. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- LICENSE | 391 +- README.md | 165 +- composer.json | 40 +- composer.lock | 4556 +++++++++++++++++ ex_app/lib/main.py | 7 +- .../Sniffs/Functions/NamedParametersSniff.php | 408 ++ phpcs.xml | 1 + phpmd.xml | 83 + psalm.xml | 96 + 9 files changed, 5455 insertions(+), 292 deletions(-) create mode 100644 composer.lock create mode 100644 phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php create mode 100644 phpmd.xml create mode 100644 psalm.xml diff --git a/LICENSE b/LICENSE index 261eeb9..6d8cea4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,190 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +EUROPEAN UNION PUBLIC LICENCE v. 1.2 +EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the +terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). +The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following +notice immediately following the copyright notice for the Work: + Licensed under the EUPL +or has expressed by any other means his willingness to license under the EUPL. + +1.Definitions +In this Licence, the following terms have the following meaning: +— ‘The Licence’:this Licence. +— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available +as Source Code and also as Executable Code as the case may be. +— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or +modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work +required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in +the country mentioned in Article 15. +— ‘The Work’:the Original Work or its Derivative Works. +— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and +modify. +— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by +a computer as a program. +— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence. +— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to +the creation of a Derivative Work. +— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the +Licence. +— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating, +transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential +functionalities at the disposal of any other natural or legal person. + +2.Scope of the rights granted by the Licence +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for +the duration of copyright vested in the Original Work: +— use the Work in any circumstance and for all usage, +— reproduce the Work, +— modify the Work, and make Derivative Works based upon the Work, +— communicate to the public, including the right to make available or display the Work or copies thereof to the public +and perform publicly, as the case may be, the Work, +— distribute the Work or copies thereof, +— lend and rent the Work or copies thereof, +— sublicense rights in the Work or copies thereof. +Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the +applicable law permits so. +In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed +by law in order to make effective the licence of the economic rights here above listed. +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the +extent necessary to make use of the rights granted on the Work under this Licence. + +3.Communication of the Source Code +The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as +Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with +each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to +the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to +distribute or communicate the Work. + +4.Limitations on copyright +Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the +exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5.Obligations of the Licensee +The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those +obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to +the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the +Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work +to carry prominent notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this +Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless +the Original Work is expressly distributed only under this version of the Licence — for example by communicating +‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the +Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both +the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done +under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed +in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide +a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available +for as long as the Licensee continues to distribute or communicate the Work. +Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names +of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6.Chain of Authorship +The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or +licensed to him/her and that he/she has the power and authority to grant the Licence. +Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or +licensed to him/her and that he/she has the power and authority to grant the Licence. +Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions +to the Work, under the terms of this Licence. + +7.Disclaimer of Warranty +The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work +and may therefore contain defects or ‘bugs’ inherent to this type of development. +For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind +concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or +errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this +Licence. +This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. + +8.Disclaimer of Liability +Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be +liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the +Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss +of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, +the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. + +9.Additional agreements +While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services +consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10.Acceptance of the Licence +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window +displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms +and conditions. +Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You +by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution +or Communication by You of the Work or copies thereof. + +11.Information to the public +In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, +by offering to download the Work from a remote location) the distribution channel or media (for example, a website) +must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence +and the way it may be accessible, concluded, stored and reproduced by the Licensee. + +12.Termination of the Licence +The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms +of the Licence. +Such a termination will not terminate the licences of any person who has received the Work from the Licensee under +the Licence, provided such persons remain in full compliance with the Licence. + +13.Miscellaneous +Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the +Work. +If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or +enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid +and enforceable. +The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of +the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. +New versions of the Licence will be published with a unique version number. +All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take +advantage of the linguistic version of their choice. + +14.Jurisdiction +Without prejudice to specific agreement between parties, +— any litigation resulting from the interpretation of this License, arising between the European Union institutions, +bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice +of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, +— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to +the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. + +15.Applicable Law +Without prejudice to specific agreement between parties, +— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, +resides or has his registered office, +— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside +a European Union Member State. + + + Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: +— GNU General Public License (GPL) v. 2, v. 3 +— GNU Affero General Public License (AGPL) v. 3 +— Open Software License (OSL) v. 2.1, v. 3.0 +— Eclipse Public License (EPL) v. 1.0 +— CeCILL v. 2.0, v. 2.1 +— Mozilla Public Licence (MPL) v. 2 +— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software +— European Union Public Licence (EUPL) v. 1.1, v. 1.2 +— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above licences without producing +a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the +covered Source Code from exclusive appropriation. +All other changes or additions to this Appendix require the production of a new EUPL version. diff --git a/README.md b/README.md index d476209..5ff9b57 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,66 @@ -# OpenKlant ExApp for Nextcloud +

+ OpenKlant logo +

-Nextcloud ExApp (External Application) that integrates [OpenKlant](https://github.com/maykinmedia/open-klant) customer interaction registry. +

OpenKlant

-## About This App +

+ Nextcloud integration for OpenKlant — customer interaction management for Dutch municipalities +

-This is a **Nextcloud ExApp** that packages the OpenKlant customer interaction registry as a containerized application managed by Nextcloud's AppAPI. When you install this app, Nextcloud will automatically deploy and manage the OpenKlant container. +

+ Latest release + License +

-**For OpenKlant documentation, see:** https://github.com/maykinmedia/open-klant +--- -## What is OpenKlant? +> **:warning: This is a Nextcloud integration wrapper only.** +> +> This app packages [OpenKlant](https://github.com/maykinmedia/open-klant) for deployment via Nextcloud's [AppAPI](https://github.com/cloud-py-api/app_api). Conduction B.V. does not provide support, licensing, guarantees, or services for the underlying OpenKlant software. +> +> For support, licensing, and pricing of OpenKlant, contact the original developers: +> **[Maykin Media](https://www.maykinmedia.nl/)** — [OpenKlant documentation](https://open-klant.readthedocs.io/) -[OpenKlant](https://github.com/maykinmedia/open-klant) is an open-source customer interaction registry for Dutch municipalities and government organizations. Built by [Maykin Media](https://www.maykinmedia.nl/), it is part of the [Common Ground](https://commonground.nl/) ecosystem. +--- -APIs provided by OpenKlant: -- **Klantinteracties API** - Customer interaction tracking and management -- **Contactmomenten API** - Contact moments registry (phone, email, in-person) -- **Klanten API** - Customer information registry +This Nextcloud ExApp (External Application) integrates the [OpenKlant](https://github.com/maykinmedia/open-klant) customer interaction registry into your Nextcloud environment. When installed, Nextcloud automatically deploys and manages the OpenKlant container through the AppAPI framework, making customer interaction management available without separate infrastructure setup. -## What This App Does +## What is OpenKlant? -- Packages OpenKlant as a Nextcloud ExApp -- Nextcloud automatically manages the container lifecycle -- Provides customer interaction APIs directly within Nextcloud -- Integrates with Nextcloud's AppAPI for seamless deployment +[OpenKlant](https://github.com/maykinmedia/open-klant) is an open-source customer interaction registry built for Dutch municipalities and government organizations. Developed by [Maykin Media](https://www.maykinmedia.nl/), it is part of the [Common Ground](https://commonground.nl/) ecosystem and implements the ZGW (Zaakgericht Werken) API standards for managing customer contacts and interactions. -## Requirements +OpenKlant exposes three core APIs: + +- **Klantinteracties API** — Customer interaction tracking and management +- **Contactmomenten API** — Contact moments registry (phone, email, in-person) +- **Klanten API** — Customer information registry -- Nextcloud 30 or higher -- AppAPI app installed and configured with a deploy daemon -- Docker environment for ExApp containers +## What This App Does -### External Dependencies +- **Packages OpenKlant as a Nextcloud ExApp** — wraps the upstream `maykinmedia/open-klant` Docker image with a FastAPI-based AppAPI integration layer +- **Automated container lifecycle** — Nextcloud handles deployment, initialization, health checks, and shutdown through AppAPI +- **Database migrations on init** — Django migrations and static file collection run automatically during first deployment +- **Request proxying** — all API requests are forwarded to the internal OpenKlant instance, making the Klantinteracties, Contactmomenten, and Klanten APIs available through Nextcloud +- **Optional SSO via Keycloak** — supports OIDC authentication through a configurable Keycloak connection -OpenKlant requires additional services for full functionality: +## Requirements -| Service | Purpose | Required | -|---------|---------|----------| -| PostgreSQL | Database | Yes | -| Redis | Caching (optional) | No | +| Requirement | Details | +|-------------|---------| +| Nextcloud | 30 or higher (tested up to 33) | +| AppAPI | Installed and configured with a Docker deploy daemon | +| Docker | Required for ExApp container management | +| PostgreSQL | External database for OpenKlant data storage (required) | +| Redis | Caching backend (optional, improves performance) | ## Installation ### Via Nextcloud App Store -1. Ensure AppAPI is installed and configured -2. Search for "OpenKlant" in the Nextcloud app store -3. Click Install - Nextcloud will pull and start the container +1. Ensure the [AppAPI](https://apps.nextcloud.com/apps/app_api) app is installed and a deploy daemon is configured +2. Search for **OpenKlant** in the Nextcloud app store +3. Click **Install** — Nextcloud will pull and start the container automatically ### Manual Registration @@ -62,70 +77,50 @@ docker exec -u www-data nextcloud php occ app_api:app:enable openklant ## Configuration -Configure via Nextcloud Admin Settings or environment variables: - -| Variable | Description | -|----------|-------------| -| `DB_HOST` | PostgreSQL database host | -| `DB_NAME` | Database name | -| `DB_USER` | Database username | -| `DB_PASSWORD` | Database password | -| `SECRET_KEY` | Django secret key | -| `ALLOWED_HOSTS` | Allowed hostnames (comma-separated) | - -## Development - -### Building the Docker Image - -```bash -# Build locally -make build - -# Push to registry -make push - -# Test locally -make test -``` - -### Project Structure - -``` -openklant/ -├── appinfo/ -│ └── info.xml # ExApp manifest -├── ex_app/ -│ └── lib/ -│ └── main.py # FastAPI wrapper for AppAPI -├── Dockerfile # Container definition -├── entrypoint.sh # Container startup -├── requirements.txt # Python dependencies -└── Makefile # Build automation -``` +Environment variables can be set through Nextcloud Admin Settings or passed directly to the container: + +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `DB_HOST` | PostgreSQL database host | Yes | `localhost` | +| `DB_NAME` | PostgreSQL database name | Yes | `openklant` | +| `DB_USER` | PostgreSQL database username | Yes | `openklant` | +| `DB_PASSWORD` | PostgreSQL database password | Yes | `openklant` | +| `SECRET_KEY` | Django secret key (use a random string in production) | Yes | `change-me-in-production` | +| `ALLOWED_HOSTS` | Comma-separated list of allowed hostnames | Yes | `*` | +| `KEYCLOAK_URL` | Keycloak server URL for SSO (e.g., `http://keycloak:8080`) | No | — | +| `KEYCLOAK_REALM` | Keycloak realm name | No | `commonground` | +| `KEYCLOAK_CLIENT_ID` | OIDC client ID for this app | No | `openklant` | +| `KEYCLOAK_CLIENT_SECRET` | OIDC client secret for this app | No | — | ## Architecture -This ExApp uses a FastAPI wrapper that: +This ExApp uses a **FastAPI wrapper** that bridges Nextcloud's AppAPI protocol with the upstream OpenKlant Django application: + +1. **Entrypoint** — Uvicorn starts the FastAPI wrapper on port 9000 (the AppAPI communication port) +2. **Initialization** (`/init`) — Runs Django database migrations, collects static files, then starts the OpenKlant uWSGI server on port 8000 +3. **Health checks** (`/heartbeat`) — Verifies the internal OpenKlant instance is responding and reports status back to Nextcloud +4. **Lifecycle management** (`/enabled`) — Starts or stops the OpenKlant uWSGI process based on the app's enabled state in Nextcloud +5. **Request proxying** (`/{path}`) — All other requests are forwarded to the internal OpenKlant instance on port 8000, preserving headers, query parameters, and request bodies -1. Implements AppAPI lifecycle endpoints (`/heartbeat`, `/init`, `/enabled`) -2. Runs Django migrations during initialization -3. Starts OpenKlant using uWSGI (production WSGI server) -4. Proxies requests to the OpenKlant backend -5. Reports health status back to Nextcloud +The container is based on the upstream `maykinmedia/open-klant:2.2.0` image, with the FastAPI wrapper and its dependencies (uvicorn, httpx) added on top. -## Related Projects +## Links -| Project | Description | Links | -|---------|-------------|-------| -| **OpenKlant** | Customer interaction registry | [GitHub](https://github.com/maykinmedia/open-klant) / [Maykin Media](https://www.maykinmedia.nl/) | -| **Nextcloud AppAPI** | External app framework | [GitHub](https://github.com/nextcloud/app_api) / [Docs](https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/) | -| **OpenZaak** | ZGW API backend | [GitHub](https://github.com/open-zaak/open-zaak) / [Docs](https://open-zaak.readthedocs.io/) | -| **Valtimo** | BPM and case management | [Website](https://www.valtimo.nl/) / [Docs](https://docs.valtimo.nl/) | +| Resource | URL | +|----------|-----| +| OpenKlant source code | [github.com/maykinmedia/open-klant](https://github.com/maykinmedia/open-klant) | +| OpenKlant documentation | [open-klant.readthedocs.io](https://open-klant.readthedocs.io/) | +| Maykin Media (original developer) | [maykinmedia.nl](https://www.maykinmedia.nl/) | +| This wrapper (GitHub) | [github.com/ConductionNL/openklant](https://github.com/ConductionNL/openklant) | +| Report wrapper issues | [github.com/ConductionNL/openklant/issues](https://github.com/ConductionNL/openklant/issues) | +| Nextcloud AppAPI docs | [docs.nextcloud.com/server/latest/developer_manual/exapp_development/](https://docs.nextcloud.com/server/latest/developer_manual/exapp_development/) | +| Common Ground | [commonground.nl](https://commonground.nl/) | ## License -AGPL-3.0 - See [LICENSE](LICENSE) for details. +EUPL-1.2 — See [LICENSE](LICENSE) for the full text. -## Author +## Authors -[Conduction B.V.](https://conduction.nl) - info@conduction.nl +Built by [Conduction](https://conduction.nl) — Nextcloud integration wrapper only. +OpenKlant is developed by [Maykin Media](https://www.maykinmedia.nl/). diff --git a/composer.json b/composer.json index 0e494db..53dce05 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "conductionnl/openklant", "description": "OpenKlant customer interaction management integration for Nextcloud", - "license": "AGPL-3.0-or-later", + "license": "EUPL-1.2", "authors": [ { "name": "Conduction b.v.", @@ -20,9 +20,40 @@ "cs:fix": "./vendor/bin/phpcbf --standard=phpcs.xml", "phpcs": "./vendor/bin/phpcs --standard=phpcs.xml", "phpcs:fix": "./vendor/bin/phpcbf --standard=phpcs.xml", - "psalm": "psalm --threads=1 --no-cache || echo 'Psalm not installed, skipping...'", + "phpmd": "phpmd lib text phpmd.xml || echo 'PHPMD not installed, skipping...'", + "phpmetrics": "./vendor/bin/phpmetrics --report-html=phpmetrics lib/", + "phpmetrics:violations": "./vendor/bin/phpmetrics --violations-xml=phpmetrics/violations.xml lib/", + "psalm": "./vendor/bin/psalm --threads=1 --no-cache || echo 'Psalm not installed, skipping...'", + "phpstan": "./vendor/bin/phpstan analyse --memory-limit=1G || echo 'PHPStan not installed, skipping...'", "test:unit": "./vendor/bin/phpunit --colors=always || echo 'Tests require Nextcloud environment, skipping...'", - "test:all": "./vendor/bin/phpunit --colors=always || echo 'Tests require Nextcloud environment, skipping...'" + "test:all": "./vendor/bin/phpunit --colors=always || echo 'Tests require Nextcloud environment, skipping...'", + "check": "E=0; for CMD in lint phpcs psalm test:unit; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E", + "check:full": "E=0; for CMD in lint phpcs psalm phpstan test:all; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E", + "check:strict": "E=0; for CMD in lint phpcs phpmd psalm phpstan test:all; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E", + "fix": [ + "@cs:fix" + ], + "test:coverage": "./vendor/bin/phpunit --coverage-html=coverage/html --coverage-clover=coverage/clover.xml --colors=always", + "coverage:check": "php -r \"\\$xml = simplexml_load_file('coverage/clover.xml'); \\$metrics = \\$xml->project->metrics; \\$statements = (int)\\$metrics['statements']; \\$covered = (int)\\$metrics['coveredstatements']; \\$percentage = \\$statements > 0 ? round((\\$covered / \\$statements) * 100, 2) : 0; echo 'Coverage: ' . \\$percentage . '%' . PHP_EOL; exit(\\$percentage < 75 ? 1 : 0);\"", + "quality:phpcs-score": "./vendor/bin/phpcs --standard=phpcs.xml --report=json lib/ | php -r \"\\$json = json_decode(file_get_contents('php://stdin'), true); \\$errors = \\$json['totals']['errors'] ?? 0; \\$warnings = \\$json['totals']['warnings'] ?? 0; \\$score = 1000 - \\$errors - (\\$warnings / 2); echo 'PHPCS Score: ' . \\$score . ' (Errors: ' . \\$errors . ', Warnings: ' . \\$warnings . ')' . PHP_EOL;\"", + "quality:phpmd-score": "phpmd lib/ json phpmd.xml | php -r \"\\$input = file_get_contents('php://stdin'); \\$json = json_decode(\\$input, true); \\$violations = count(\\$json['files'] ?? []); \\$score = 1000 - (\\$violations * 10); echo 'PHPMD Score: ' . \\$score . ' (Violations: ' . \\$violations . ')' . PHP_EOL;\" || echo 'PHPMD not available'", + "quality:psalm-score": "./vendor/bin/psalm --output-format=json --no-cache | php -r \"\\$input = file_get_contents('php://stdin'); \\$json = json_decode(\\$input, true); \\$errors = count(\\$json ?? []); \\$score = 1000 - (\\$errors * 5); echo 'Psalm Score: ' . \\$score . ' (Errors: ' . \\$errors . ')' . PHP_EOL;\" || echo 'Psalm not available'", + "quality:phpstan-score": "./vendor/bin/phpstan analyse --memory-limit=1G --error-format=json --no-progress | php -r \"\\$input = file_get_contents('php://stdin'); \\$json = json_decode(\\$input, true); \\$errors = \\$json['totals']['file_errors'] ?? 0; \\$score = 1000 - (\\$errors * 5); echo 'PHPStan Score: ' . \\$score . ' (Errors: ' . \\$errors . ')' . PHP_EOL;\" || echo 'PHPStan not available'", + "quality:score": [ + "@quality:phpcs-score", + "@quality:phpmd-score", + "@quality:psalm-score", + "@quality:phpstan-score" + ], + "phpqa": "./vendor/bin/phpqa --report --analyzedDirs lib --buildDir phpqa", + "phpqa:full": "./vendor/bin/phpqa --report --analyzedDirs lib --buildDir phpqa --tools phpcs:0,phpmd:0,phploc:0,phpmetrics,phpcpd:0,parallel-lint:0", + "phpqa:ci": "./vendor/bin/phpqa --report --analyzedDirs lib --buildDir phpqa --tools phpcs,phpmd,phploc,phpmetrics,phpcpd,parallel-lint", + "qa:check": [ + "@phpqa" + ], + "qa:full": [ + "@phpqa:full" + ] }, "require": { "php": "^8.1" @@ -30,6 +61,9 @@ "require-dev": { "nextcloud/coding-standard": "^1.4", "nextcloud/ocp": "^31.0", + "phpmd/phpmd": "^2.15", + "phpmetrics/phpmetrics": "^2.8", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.5", "squizlabs/php_codesniffer": "^3.9", "vimeo/psalm": "^5.26" diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..843418b --- /dev/null +++ b/composer.lock @@ -0,0 +1,4556 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "3a4bd9353396f4d5977ea0001757e80e", + "packages": [], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v2.6.5", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/d7dda98dae26e56f3f6fcfbf1c1f819c9a993207", + "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "react/promise": "^2", + "vimeo/psalm": "^3.12" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.5" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-09-03T19:41:28+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v1.8.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-13T18:00:56+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, + "time": "2021-06-11T22:34:44+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + }, + "time": "2024-04-30T00:40:11+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "kubawerlos/php-cs-fixer-custom-fixers", + "version": "v3.36.0", + "source": { + "type": "git", + "url": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers.git", + "reference": "e1f97f6463f0b2a22e0dd320948a04132ff9c501" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/e1f97f6463f0b2a22e0dd320948a04132ff9c501", + "reference": "e1f97f6463f0b2a22e0dd320948a04132ff9c501", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-tokenizer": "*", + "friendsofphp/php-cs-fixer": "^3.87", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6.24 || ^10.5.51 || ^11.5.44" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpCsFixerCustomFixers\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kuba Werłos", + "email": "werlos@gmail.com" + } + ], + "description": "A set of custom fixers for PHP CS Fixer", + "support": { + "issues": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues", + "source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.36.0" + }, + "funding": [ + { + "url": "https://github.com/kubawerlos", + "type": "github" + } + ], + "time": "2026-01-31T07:02:11+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v4.5.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" + }, + "time": "2024-09-08T10:13:13+00:00" + }, + { + "name": "nextcloud/coding-standard", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/nextcloud/coding-standard.git", + "reference": "8e06808c1423e9208d63d1bd205b9a38bd400011" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nextcloud/coding-standard/zipball/8e06808c1423e9208d63d1bd205b9a38bd400011", + "reference": "8e06808c1423e9208d63d1bd205b9a38bd400011", + "shasum": "" + }, + "require": { + "kubawerlos/php-cs-fixer-custom-fixers": "^3.22", + "php": "^8.0", + "php-cs-fixer/shim": "^3.17" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nextcloud\\CodingStandard\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christoph Wurst", + "email": "christoph@winzerhof-wurst.at" + } + ], + "description": "Nextcloud coding standards for the php cs fixer", + "keywords": [ + "dev" + ], + "support": { + "issues": "https://github.com/nextcloud/coding-standard/issues", + "source": "https://github.com/nextcloud/coding-standard/tree/v1.4.0" + }, + "time": "2025-06-19T12:27:27+00:00" + }, + { + "name": "nextcloud/ocp", + "version": "v31.0.9", + "source": { + "type": "git", + "url": "https://github.com/nextcloud-deps/ocp.git", + "reference": "abd32429d794ede1d92b7b0a88a1070371c907b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/abd32429d794ede1d92b7b0a88a1070371c907b5", + "reference": "abd32429d794ede1d92b7b0a88a1070371c907b5", + "shasum": "" + }, + "require": { + "php": "~8.1 || ~8.2 || ~8.3 || ~8.4", + "psr/clock": "^1.0", + "psr/container": "^2.0.2", + "psr/event-dispatcher": "^1.0", + "psr/log": "^3.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-stable31": "31.0.0-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Christoph Wurst", + "email": "christoph@winzerhof-wurst.at" + }, + { + "name": "Joas Schilling", + "email": "coding@schilljs.com" + } + ], + "description": "Composer package containing Nextcloud's public OCP API and the unstable NCU API", + "support": { + "issues": "https://github.com/nextcloud-deps/ocp/issues", + "source": "https://github.com/nextcloud-deps/ocp/tree/v31.0.9" + }, + "time": "2025-07-31T00:57:37+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.19.5", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "51bd93cc741b7fc3d63d20b6bdcd99fdaa359837" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/51bd93cc741b7fc3d63d20b6bdcd99fdaa359837", + "reference": "51bd93cc741b7fc3d63d20b6bdcd99fdaa359837", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.5" + }, + "time": "2025-12-06T11:45:25+00:00" + }, + { + "name": "pdepend/pdepend", + "version": "2.16.2", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.19" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-12-17T18:09:59+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-cs-fixer/shim", + "version": "v3.94.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/shim.git", + "reference": "80fd29f44a736136a2f05bae5464816a444b91d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/80fd29f44a736136a2f05bae5464816a444b91d1", + "reference": "80fd29f44a736136a2f05bae5464816a444b91d1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "replace": { + "friendsofphp/php-cs-fixer": "self.version" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer", + "php-cs-fixer.phar" + ], + "type": "application", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/PHP-CS-Fixer/shim/issues", + "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.94.2" + }, + "time": "2026-02-20T16:14:17+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.6", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" + }, + "time": "2025-12-22T21:13:58+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + }, + "time": "2025-11-21T15:09:14+00:00" + }, + { + "name": "phpmd/phpmd", + "version": "2.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.16.1", + "php": ">=5.3.9" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.8", + "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", + "keywords": [ + "dev", + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.15.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2023-12-11T08:22:20+00:00" + }, + { + "name": "phpmetrics/phpmetrics", + "version": "v2.9.1", + "source": { + "type": "git", + "url": "https://github.com/phpmetrics/PhpMetrics.git", + "reference": "e2e68ddd1543bc3f44402c383f7bccb62de1ece3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmetrics/PhpMetrics/zipball/e2e68ddd1543bc3f44402c383f7bccb62de1ece3", + "reference": "e2e68ddd1543bc3f44402c383f7bccb62de1ece3", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^3|^4|^5" + }, + "replace": { + "halleck45/php-metrics": "*", + "halleck45/phpmetrics": "*" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "bin": [ + "bin/phpmetrics" + ], + "type": "library", + "autoload": { + "files": [ + "./src/functions.php" + ], + "psr-0": { + "Hal\\": "./src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Lépine", + "email": "lepinejeanfrancois@yahoo.fr", + "homepage": "http://www.lepine.pro", + "role": "Copyright Holder" + } + ], + "description": "Static analyzer tool for PHP : Coupling, Cyclomatic complexity, Maintainability Index, Halstead's metrics... and more !", + "homepage": "http://www.phpmetrics.org", + "keywords": [ + "analysis", + "qa", + "quality", + "testing" + ], + "support": { + "issues": "https://github.com/PhpMetrics/PhpMetrics/issues", + "source": "https://github.com/phpmetrics/PhpMetrics/tree/v2.9.1" + }, + "funding": [ + { + "url": "https://github.com/Halleck45", + "type": "github" + } + ], + "time": "2025-09-25T05:21:02+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.33", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", + "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-02-28T20:30:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "spatie/array-to-xml", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/88b2f3852a922dd73177a68938f8eb2ec70c7224", + "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "spatie/pest-plugin-snapshots": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" + } + ], + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", + "keywords": [ + "array", + "convert", + "xml" + ], + "support": { + "source": "https://github.com/spatie/array-to-xml/tree/3.4.4" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-12-15T09:00:41+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "symfony/config", + "version": "v6.4.34", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/ce9cb0c0d281aaf188b802d4968e42bfb60701e9", + "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v6.4.34" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-24T17:34:50+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.34", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/7b1f1c37eff5910ddda2831345467e593a5120ad", + "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.34" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-23T15:42:15+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v6.4.34", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "91e49958b8a6092e48e4711894a1aeb1b151c62a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/91e49958b8a6092e48e4711894a1aeb1b151c62a", + "reference": "91e49958b8a6092e48e4711894a1aeb1b151c62a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4.20|^7.2.5" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<6.3", + "symfony/yaml": "<5.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.1|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.34" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-24T15:33:38+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.4.34", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/01ffe0411b842f93c571e5c391f289c3fdd498c3", + "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.4.34" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-24T17:51:06+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.34", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/2adaf4106f2ef4c67271971bde6d3fe0a6936432", + "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.34" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-08T20:44:54+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v6.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/466fcac5fa2e871f83d31173f80e9c2684743bfc", + "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v6.4.26" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T09:57:09+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + }, + { + "name": "vimeo/psalm", + "version": "5.26.1", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", + "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.4.2", + "amphp/byte-stream": "^1.5", + "composer-runtime-api": "^2", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.1", + "felixfbecker/language-server-protocol": "^1.5.2", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "nikic/php-parser": "^4.17", + "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" + }, + "conflict": { + "nikic/php-parser": "4.17.0" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "amphp/phpunit-util": "^2.0", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", + "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalter" + ], + "type": "project", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php", + "static analysis" + ], + "support": { + "docs": "https://psalm.dev/docs", + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm" + }, + "time": "2024-09-08T18:53:08+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^7.2 || ^8.0" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.12.1" + }, + "time": "2025-10-29T15:56:20+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.1" + }, + "plugin-api-version": "2.6.0" +} diff --git a/ex_app/lib/main.py b/ex_app/lib/main.py index 698b297..f1062d5 100644 --- a/ex_app/lib/main.py +++ b/ex_app/lib/main.py @@ -185,7 +185,8 @@ async def lifespan(app: FastAPI): @app.get("/heartbeat") async def heartbeat(): - """Health check endpoint for AppAPI""" + """Health check endpoint for AppAPI - always returns 200 so AppAPI can proceed with init""" + service_up = False try: async with httpx.AsyncClient() as client: resp = await client.get( @@ -194,10 +195,10 @@ async def heartbeat(): follow_redirects=False, ) if resp.status_code in (200, 302, 301): - return JSONResponse({"status": "ok"}) + service_up = True except Exception: pass - return JSONResponse({"status": "error"}, status_code=503) + return JSONResponse({"status": "ok" if service_up else "waiting"}) @app.post("/init") diff --git a/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php b/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php new file mode 100644 index 0000000..92740a9 --- /dev/null +++ b/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php @@ -0,0 +1,408 @@ +method(name: $value) + * - self::method(name: $value) / static::method(name: $value) + * - parent::method(name: $value) + * - new OurClass(name: $value) — classes from the same app namespace + * + * Allows positional arguments for external code: + * - PHP built-in functions (strlen, array_map, sprintf, etc.) + * - Nextcloud/third-party method calls ($variable->method() where $variable !== $this) + * - Any call we cannot determine is "our code" + * + * @author Conduction + * @package CustomSniffs + */ + +namespace CustomSniffs\Sniffs\Functions; + +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; + +/** + * NamedParametersSniff — enforces named parameters for internal code. + */ +class NamedParametersSniff implements Sniff +{ + + + /** + * Returns tokens this sniff listens for. + * + * @return array + */ + public function register(): array + { + return [T_STRING]; + + }//end register() + + + /** + * Process a T_STRING token — check if it's a function/method call to our code. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr Position of the T_STRING token. + * + * @return void + */ + public function process(File $phpcsFile, $stackPtr): void + { + $tokens = $phpcsFile->getTokens(); + $functionName = $tokens[$stackPtr]['content']; + + // Must be followed by ( to be a function/method call. + $openParen = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($openParen === false || $tokens[$openParen]['code'] !== T_OPEN_PARENTHESIS) { + return; + } + + // Skip function/method definitions. + if ($this->isFunctionDefinition(phpcsFile: $phpcsFile, stackPtr: $stackPtr) === true) { + return; + } + + // Only check calls to our own code. + if ($this->isInternalCall(phpcsFile: $phpcsFile, stackPtr: $stackPtr) === false) { + return; + } + + // Get the closing parenthesis. + if (isset($tokens[$openParen]['parenthesis_closer']) === false) { + return; + } + + $closeParen = $tokens[$openParen]['parenthesis_closer']; + + // Check if there are any arguments. + $firstContent = $phpcsFile->findNext( + types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], + start: ($openParen + 1), + end: $closeParen, + exclude: true + ); + if ($firstContent === false) { + return; + } + + // Check if all arguments use named parameters. + if ($this->hasUnnamedArguments(phpcsFile: $phpcsFile, openParen: $openParen, closeParen: $closeParen) === true) { + $error = 'All arguments in calls to internal code must use named parameters: %s(paramName: $value)'; + $phpcsFile->addError($error, $stackPtr, 'RequireNamedParameters', [$functionName]); + } + + }//end process() + + + /** + * Check if the T_STRING at $stackPtr is part of a function/method definition (not a call). + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr Position of the T_STRING token. + * + * @return bool True if this is a definition, false if it's a call. + */ + private function isFunctionDefinition(File $phpcsFile, int $stackPtr): bool + { + $tokens = $phpcsFile->getTokens(); + $prev = ($stackPtr - 1); + while ($prev >= 0) { + $code = $tokens[$prev]['code']; + if ($code === T_FUNCTION) { + return true; + } + + // Stop at statement/block boundaries. + if ($code === T_SEMICOLON + || $code === T_OPEN_CURLY_BRACKET + || $code === T_CLOSE_CURLY_BRACKET + ) { + return false; + } + + $prev--; + } + + return false; + + }//end isFunctionDefinition() + + + /** + * Determine if the function/method call at $stackPtr is to our own code. + * + * Matches: + * - $this->method() + * - self::method() / static::method() / parent::method() + * - new OurClass() where OurClass is from the same app namespace + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr Position of the T_STRING (function/method name). + * + * @return bool True if call is to internal code. + */ + private function isInternalCall(File $phpcsFile, int $stackPtr): bool + { + $tokens = $phpcsFile->getTokens(); + + $prev = $phpcsFile->findPrevious( + types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], + start: ($stackPtr - 1), + end: null, + exclude: true + ); + if ($prev === false) { + return false; + } + + $prevCode = $tokens[$prev]['code']; + + // Case 1: $this->method(). + if ($prevCode === T_OBJECT_OPERATOR) { + $beforeArrow = $phpcsFile->findPrevious( + types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], + start: ($prev - 1), + end: null, + exclude: true + ); + return ($beforeArrow !== false + && $tokens[$beforeArrow]['code'] === T_VARIABLE + && $tokens[$beforeArrow]['content'] === '$this'); + } + + // Case 2: self::method() / static::method() / parent::method(). + if ($prevCode === T_DOUBLE_COLON) { + $beforeColon = $phpcsFile->findPrevious( + types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], + start: ($prev - 1), + end: null, + exclude: true + ); + return ($beforeColon !== false + && in_array($tokens[$beforeColon]['code'], [T_SELF, T_STATIC, T_PARENT], true) === true); + } + + // Case 3: new OurClass(). + if ($prevCode === T_NEW) { + return $this->isClassFromOurNamespace(phpcsFile: $phpcsFile, classNamePtr: $stackPtr); + } + + return false; + + }//end isInternalCall() + + + /** + * Check if the class at $classNamePtr is from the same app namespace as the current file. + * + * Compares the class's use-import path against the file's OCA\AppName\ prefix. + * + * @param File $phpcsFile The file being scanned. + * @param int $classNamePtr Position of the class name token. + * + * @return bool True if the class is from our app namespace. + */ + private function isClassFromOurNamespace(File $phpcsFile, int $classNamePtr): bool + { + $tokens = $phpcsFile->getTokens(); + $className = $tokens[$classNamePtr]['content']; + + // Find the file's namespace declaration. + $appPrefix = $this->getAppNamespacePrefix(phpcsFile: $phpcsFile); + if ($appPrefix === null) { + return false; + } + + // Scan use-statements for an import matching this class name from our namespace. + for ($i = 0; $i < $phpcsFile->numTokens; $i++) { + if ($tokens[$i]['code'] === T_USE) { + $usePath = $this->getUseStatementPath(phpcsFile: $phpcsFile, usePtr: $i); + if ($usePath === null) { + continue; + } + + // Extract the imported short name (last segment). + $segments = explode(separator: '\\', string: $usePath); + $importedName = end($segments); + + if ($importedName === $className + && str_starts_with(haystack: $usePath, needle: $appPrefix) === true + ) { + return true; + } + }//end if + + // Stop scanning after the class declaration. + if (in_array($tokens[$i]['code'], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true) === true) { + break; + } + }//end for + + return false; + + }//end isClassFromOurNamespace() + + + /** + * Get the app namespace prefix (e.g., "OCA\MyDash") from the file's namespace declaration. + * + * @param File $phpcsFile The file being scanned. + * + * @return string|null The app prefix or null if not found. + */ + private function getAppNamespacePrefix(File $phpcsFile): ?string + { + $tokens = $phpcsFile->getTokens(); + for ($i = 0; $i < $phpcsFile->numTokens; $i++) { + if ($tokens[$i]['code'] === T_NAMESPACE) { + $namespace = ''; + $j = ($i + 1); + while ($j < $phpcsFile->numTokens && $tokens[$j]['code'] !== T_SEMICOLON) { + if ($tokens[$j]['code'] !== T_WHITESPACE) { + $namespace .= $tokens[$j]['content']; + } + + $j++; + } + + // Extract first two segments: OCA\AppName. + $parts = explode(separator: '\\', string: $namespace); + if (count($parts) >= 2) { + return $parts[0].'\\'.$parts[1]; + } + + return null; + }//end if + }//end for + + return null; + + }//end getAppNamespacePrefix() + + + /** + * Extract the full path from a use-statement starting at $usePtr. + * + * @param File $phpcsFile The file being scanned. + * @param int $usePtr Position of the T_USE token. + * + * @return string|null The use path or null if parsing failed. + */ + private function getUseStatementPath(File $phpcsFile, int $usePtr): ?string + { + $tokens = $phpcsFile->getTokens(); + $usePath = ''; + $j = ($usePtr + 1); + while ($j < $phpcsFile->numTokens) { + $code = $tokens[$j]['code']; + if ($code === T_SEMICOLON || $code === T_OPEN_CURLY_BRACKET) { + break; + } + + // Stop at 'as' keyword (aliases) — use the path before it. + if ($code === T_AS) { + break; + } + + if ($code !== T_WHITESPACE) { + $usePath .= $tokens[$j]['content']; + } + + $j++; + } + + $usePath = trim(string: $usePath); + return ($usePath !== '') ? $usePath : null; + + }//end getUseStatementPath() + + + /** + * Check if the arguments between $openParen and $closeParen contain any unnamed arguments. + * + * An argument is "named" if its first significant token is followed by T_COLON. + * Handles nested parentheses, brackets, and braces correctly. + * + * @param File $phpcsFile The file being scanned. + * @param int $openParen Position of the opening parenthesis. + * @param int $closeParen Position of the closing parenthesis. + * + * @return bool True if any argument is positional (unnamed). + */ + private function hasUnnamedArguments(File $phpcsFile, int $openParen, int $closeParen): bool + { + $tokens = $phpcsFile->getTokens(); + $parenDepth = 0; + $bracketDepth = 0; + $braceDepth = 0; + $atArgumentStart = true; + + for ($i = ($openParen + 1); $i < $closeParen; $i++) { + $code = $tokens[$i]['code']; + + // Track nesting depth. + if ($code === T_OPEN_PARENTHESIS) { + $parenDepth++; + } elseif ($code === T_CLOSE_PARENTHESIS) { + $parenDepth--; + } elseif ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) { + $bracketDepth++; + } elseif ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) { + $bracketDepth--; + } elseif ($code === T_OPEN_CURLY_BRACKET) { + $braceDepth++; + } elseif ($code === T_CLOSE_CURLY_BRACKET) { + $braceDepth--; + } + + // Only examine tokens at the top level of the argument list. + if ($parenDepth > 0 || $bracketDepth > 0 || $braceDepth > 0) { + continue; + } + + // Comma at top level → next argument starts. + if ($code === T_COMMA) { + $atArgumentStart = true; + continue; + } + + // Skip whitespace and comments at argument start. + if ($atArgumentStart === true + && in_array($code, [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true) === true + ) { + continue; + } + + if ($atArgumentStart === true) { + $atArgumentStart = false; + + // Skip spread operator (...$args). + if ($code === T_ELLIPSIS) { + continue; + } + + // Check if this argument is named: token followed by ':'. + $nextNonWs = $phpcsFile->findNext( + types: [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], + start: ($i + 1), + end: $closeParen, + exclude: true + ); + + $isNamed = ($nextNonWs !== false && $tokens[$nextNonWs]['code'] === T_COLON); + + if ($isNamed === false) { + return true; + } + }//end if + }//end for + + return false; + + }//end hasUnnamedArguments() + + +}//end class diff --git a/phpcs.xml b/phpcs.xml index d56fdcf..de725ae 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -64,4 +64,5 @@ + diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..105800f --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,83 @@ + + + + + PHPMD ruleset for openklant, based on the Conduction/OpenRegister standard. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..7c25db5 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c9c5e884a5c78046727ce52c4566153f2f877239 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sat, 23 May 2026 10:37:09 +0200 Subject: [PATCH 6/8] chore(deps): bump composer config.platform.php to 8.3 (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the composer platform pin with the runtime constraint ('require.php' bumped to ^8.3 fleet-wide yesterday). Keeps composer's resolver consistent — it'll pick package versions assuming PHP 8.3 is the target runtime, which matches what the container (PHP 8.3.30) and CI matrices already use. Part of the fleet-wide PHP 8.3 sweep. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 53dce05..79e11ca 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "optimize-autoloader": true, "sort-packages": true, "platform": { - "php": "8.1" + "php": "8.3" } } } From df8319d46f6e25075e87704e0d9bd4889b99ea2c Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sat, 23 May 2026 10:37:22 +0200 Subject: [PATCH 7/8] chore(deps): bump PHP requirement to ^8.3 (#6) Brings the composer require.php constraint in line with the runtime the container + CI already use (PHP 8.3). Drops formal support for PHP 8.1 and 8.2. Several vendor packages (e.g. azjezz/psl@4.3.0) use PHP 8.3 typed class constants, making the codebase functionally 8.3-only already. Part of the fleet-wide PHP 8.3 sweep. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 79e11ca..53a3098 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ ] }, "require": { - "php": "^8.1" + "php": "^8.3" }, "require-dev": { "nextcloud/coding-standard": "^1.4", From 2f6b24f680ab43c89e2f7b4c99b0edf795c3ee83 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sat, 23 May 2026 10:37:32 +0200 Subject: [PATCH 8/8] fix(ci): skip lint-check when repo has no package.json (#5) --- .github/workflows/pull-request-lint-check.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-lint-check.yaml b/.github/workflows/pull-request-lint-check.yaml index 4a8c874..4f8c735 100644 --- a/.github/workflows/pull-request-lint-check.yaml +++ b/.github/workflows/pull-request-lint-check.yaml @@ -12,10 +12,22 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 + + - name: Check for package.json + id: has_pkg + run: | + if [ -f package.json ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "::notice::No package.json found at repo root; lint-check is a no-op for this repo." + fi - name: Install dependencies + if: steps.has_pkg.outputs.exists == 'true' run: npm i - name: Linting + if: steps.has_pkg.outputs.exists == 'true' run: npm run lint