diff --git a/.docker/frankenphp/conf.d/app.ini b/.docker/frankenphp/conf.d/app.ini index 10a062f2..08c71700 100644 --- a/.docker/frankenphp/conf.d/app.ini +++ b/.docker/frankenphp/conf.d/app.ini @@ -12,7 +12,7 @@ opcache.max_accelerated_files = 20000 opcache.memory_consumption = 256 opcache.enable_file_override = 1 -memory_limit = 256M +memory_limit = 512M upload_max_filesize=256M -post_max_size=300M \ No newline at end of file +post_max_size=300M diff --git a/.docker/frankenphp/docker-entrypoint.sh b/.docker/frankenphp/docker-entrypoint.sh index 1655af5a..56b3bc31 100644 --- a/.docker/frankenphp/docker-entrypoint.sh +++ b/.docker/frankenphp/docker-entrypoint.sh @@ -26,6 +26,28 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then composer install --prefer-dist --no-progress --no-interaction fi + # Install additional composer packages if COMPOSER_EXTRA_PACKAGES is set + if [ -n "$COMPOSER_EXTRA_PACKAGES" ]; then + echo "Installing additional composer packages: $COMPOSER_EXTRA_PACKAGES" + # Note: COMPOSER_EXTRA_PACKAGES is intentionally not quoted to allow word splitting + # This enables passing multiple package names separated by spaces + # shellcheck disable=SC2086 + composer require $COMPOSER_EXTRA_PACKAGES --no-install --no-interaction --no-progress + if [ $? -eq 0 ]; then + echo "Running composer install to install packages without dev dependencies..." + composer install --no-dev --no-interaction --no-progress --optimize-autoloader + if [ $? -eq 0 ]; then + echo "Successfully installed additional composer packages" + else + echo "Failed to install composer dependencies" + exit 1 + fi + else + echo "Failed to add additional composer packages to composer.json" + exit 1 + fi + fi + if grep -q ^DATABASE_URL= .env; then echo "Waiting for database to be ready..." ATTEMPTS_LEFT_TO_REACH_DATABASE=60 diff --git a/.docker/frankenphp/worker.Caddyfile b/.docker/frankenphp/worker.Caddyfile index d384ae4c..eaea1892 100644 --- a/.docker/frankenphp/worker.Caddyfile +++ b/.docker/frankenphp/worker.Caddyfile @@ -1,4 +1,3 @@ worker { file ./public/index.php - env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime } diff --git a/.docker/partdb-entrypoint.sh b/.docker/partdb-entrypoint.sh index f5071e22..61e8d1e6 100644 --- a/.docker/partdb-entrypoint.sh +++ b/.docker/partdb-entrypoint.sh @@ -39,6 +39,28 @@ if [ -d /var/www/html/var/db ]; then fi fi +# Install additional composer packages if COMPOSER_EXTRA_PACKAGES is set +if [ -n "$COMPOSER_EXTRA_PACKAGES" ]; then + echo "Installing additional composer packages: $COMPOSER_EXTRA_PACKAGES" + # Note: COMPOSER_EXTRA_PACKAGES is intentionally not quoted to allow word splitting + # This enables passing multiple package names separated by spaces + # shellcheck disable=SC2086 + sudo -E -u www-data composer require $COMPOSER_EXTRA_PACKAGES --no-install --no-interaction --no-progress + if [ $? -eq 0 ]; then + echo "Running composer install to install packages without dev dependencies..." + sudo -E -u www-data composer install --no-dev --no-interaction --no-progress --optimize-autoloader + if [ $? -eq 0 ]; then + echo "Successfully installed additional composer packages" + else + echo "Failed to install composer dependencies" + exit 1 + fi + else + echo "Failed to add additional composer packages to composer.json" + exit 1 + fi +fi + # Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile) php-fpmPHP_VERSION -F & diff --git a/.env b/.env index 982d4bbd..8d5e5a54 100644 --- a/.env +++ b/.env @@ -31,8 +31,7 @@ DATABASE_EMULATE_NATURAL_SORT=0 # General settings ################################################################################### -# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates -# This must end with a slash! +# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates or when no request context is available. DEFAULT_URI="https://partdb.changeme.invalid/" ################################################################################### @@ -60,6 +59,28 @@ ERROR_PAGE_ADMIN_EMAIL='' # If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them... ERROR_PAGE_SHOW_HELP=1 +################################################################################### +# Update Manager settings +################################################################################### + +# Disable web-based updates from the Update Manager UI (0=enabled, 1=disabled). +# When disabled, use the CLI command "php bin/console partdb:update" instead. +DISABLE_WEB_UPDATES=1 + +# Disable backup restore from the Update Manager UI (0=enabled, 1=disabled). +# Restoring backups is a destructive operation that could overwrite your database. +DISABLE_BACKUP_RESTORE=1 + +# Disable backup download from the Update Manager UI (0=enabled, 1=disabled). +# Backups contain sensitive data including password hashes and secrets. +# When enabled, users must confirm their password before downloading. +DISABLE_BACKUP_DOWNLOAD=1 + +# Watchtower integration for Docker-based updates. +# Set these to enable one-click updates via the Update Manager UI. +# See https://containrrr.dev/watchtower/ for Watchtower setup. +WATCHTOWER_API_URL= +WATCHTOWER_API_TOKEN= ################################################################################### # SAML Single sign on-settings @@ -106,6 +127,10 @@ SAML_SP_PRIVATE_KEY="MIIE..." # In demo mode things it is not possible for a user to change his password and his settings. DEMO_MODE=0 +# When this is set to 1, users can make Part-DB directly download a file specified as a URL from the local network and create it as a local file. +# This allows users access to all resources available in the local network, which could be a security risk, so use this only if you trust your users and have a secure local network. +ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK=0 + # Change this to true, if no url rewriting (like mod_rewrite for Apache) is available # In that case all URL contains the index.php front controller in URL NO_URL_REWRITE_AVAILABLE=0 @@ -134,4 +159,13 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###> symfony/framework-bundle ### APP_ENV=prod APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 +APP_SHARE_DIR=var/share ###< symfony/framework-bundle ### + +###> symfony/ai-generic-platform ### +# GENERIC_BASE_URL=https://api.example.com/v1 +###< symfony/ai-generic-platform ### + +###> symfony/ai-open-router-platform ### +OPENROUTER_API_KEY= +###< symfony/ai-open-router-platform ### diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..637a66b5 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,186 @@ +# Copilot Instructions for Part-DB + +Part-DB is an Open-Source inventory management system for electronic components built with Symfony 7.4 and modern web technologies. + +## Technology Stack + +- **Backend**: PHP 8.2+, Symfony 7.4, Doctrine ORM +- **Frontend**: Bootstrap 5, Hotwire Stimulus/Turbo, TypeScript, Webpack Encore +- **Database**: MySQL 5.7+/MariaDB 10.4+/PostgreSQL 10+/SQLite +- **Testing**: PHPUnit with DAMA Doctrine Test Bundle +- **Code Quality**: Easy Coding Standard (ECS), PHPStan (level 5) + +## Project Structure + +- `src/`: PHP application code organized by purpose (Controller, Entity, Service, Form, etc.) +- `assets/`: Frontend TypeScript/JavaScript and CSS files +- `templates/`: Twig templates for views +- `tests/`: PHPUnit tests mirroring the `src/` structure +- `config/`: Symfony configuration files +- `public/`: Web-accessible files +- `translations/`: Translation files for multi-language support + +## Coding Standards + +### PHP Code + +- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) and [Symfony coding standards](https://symfony.com/doc/current/contributing/code/standards.html) +- Use type hints for all parameters and return types +- Always declare strict types: `declare(strict_types=1);` at the top of PHP files +- Use PHPDoc blocks for complex logic or when type information is needed + +### TypeScript/JavaScript + +- Use TypeScript for new frontend code +- Follow existing Stimulus controller patterns in `assets/controllers/` +- Use Bootstrap 5 components and utilities +- Leverage Hotwire Turbo for dynamic page updates + +### Naming Conventions + +- Entities: Use descriptive names that reflect database models (e.g., `Part`, `StorageLocation`) +- Controllers: Suffix with `Controller` (e.g., `PartController`) +- Services: Descriptive names reflecting their purpose (e.g., `PartService`, `LabelGenerator`) +- Tests: Match the class being tested with `Test` suffix (e.g., `PartTest`, `PartControllerTest`) + +## Development Workflow + +### Dependencies + +- Install PHP dependencies: `composer install` +- Install JS dependencies: `yarn install` +- Build frontend assets: `yarn build` (production) or `yarn watch` (development) + +### Database + +- Create database: `php bin/console doctrine:database:create --env=dev` +- Run migrations: `php bin/console doctrine:migrations:migrate --env=dev` +- Load fixtures: `php bin/console partdb:fixtures:load -n --env=dev` + +Or use Makefile shortcuts: +- `make dev-setup`: Complete development environment setup +- `make dev-reset`: Reset development environment (cache clear + migrate) + +### Testing + +- Set up test environment: `make test-setup` +- Run all tests: `php bin/phpunit` +- Run specific test: `php bin/phpunit tests/Path/To/SpecificTest.php` +- Run tests with coverage: `php bin/phpunit --coverage-html var/coverage` +- Test environment uses SQLite by default for speed + +### Static Analysis + +- Run PHPStan: `composer phpstan` or `COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G vendor/bin/phpstan analyse src --level 5` +- PHPStan configuration is in `phpstan.dist.neon` + +### Running the Application + +- Development server: `symfony serve` (requires Symfony CLI) +- Or configure Apache/nginx to serve from `public/` directory +- Set `APP_ENV=dev` in `.env.local` for development mode + +## Best Practices + +### Security + +- Always sanitize user input +- Use Symfony's security component for authentication/authorization +- Check permissions using the permission system before allowing actions +- Never expose sensitive data in logs or error messages +- Use parameterized queries (Doctrine handles this automatically) + +### Performance + +- Use Doctrine query builder for complex queries instead of DQL when possible +- Lazy load relationships to avoid N+1 queries +- Cache results when appropriate using Symfony's cache component +- Use pagination for large result sets (DataTables integration available) + +### Database + +- Always create migrations for schema changes: `php bin/console make:migration` +- Review migration files before running them +- Use Doctrine annotations or attributes for entity mapping +- Follow existing entity patterns for relationships and lifecycle callbacks + +### Frontend + +- Use Stimulus controllers for interactive components +- Leverage Turbo for dynamic page updates without full page reloads +- Use Bootstrap 5 classes for styling +- Keep JavaScript modular and organized in controllers +- Use the translation system for user-facing strings + +### Translations + +- Use translation keys, not hardcoded strings: `{{ 'part.info.title'|trans }}` +- Add new translation keys to `translations/` files +- Primary language is English (en) +- Translations are managed via Crowdin, but can be edited locally if needed + +### Testing + +- Write unit tests for services and helpers +- Write functional tests for controllers +- Use fixtures for test data +- Tests should be isolated and not depend on execution order +- Mock external dependencies when appropriate +- Follow existing test patterns in the repository + +## Common Patterns + +### Creating an Entity + +1. Create entity class in `src/Entity/` with Doctrine attributes +2. Generate migration: `php bin/console make:migration` +3. Review and run migration: `php bin/console doctrine:migrations:migrate` +4. Create repository if needed in `src/Repository/` +5. Add fixtures in `src/DataFixtures/` for testing + +### Adding a Form + +1. Create form type in `src/Form/` +2. Extend `AbstractType` and implement `buildForm()` and `configureOptions()` +3. Use in controller and render in Twig template +4. Follow existing form patterns for consistency + +### Creating a Controller Action + +1. Add method to appropriate controller in `src/Controller/` +2. Use route attributes for routing +3. Check permissions using security voters +4. Return Response or render Twig template +5. Add corresponding template in `templates/` + +### Adding a Service + +1. Create service class in `src/Services/` +2. Use dependency injection via constructor +3. Tag service in `config/services.yaml` if needed +4. Services are autowired by default + +## Important Notes + +- Part-DB uses fine-grained permissions - always check user permissions before actions +- Multi-language support is critical - use translation keys everywhere +- The application supports multiple database backends - write portable code +- Responsive design is important - test on mobile/tablet viewports +- Event system is used for logging changes - emit events when appropriate +- API Platform is integrated for REST API endpoints + +## Multi-tenancy Considerations + +- Part-DB is designed as a single-tenant application with multiple users +- User groups have different permission levels +- Always scope queries to respect user permissions +- Use the security context to get current user information + +## Resources + +- [Documentation](https://docs.part-db.de/) +- [Contributing Guide](CONTRIBUTING.md) +- [Symfony Documentation](https://symfony.com/doc/current/index.html) +- [Doctrine Documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/current/) +- [Bootstrap 5 Documentation](https://getbootstrap.com/docs/5.1/) +- [Hotwire Documentation](https://hotwired.dev/) diff --git a/.github/workflows/assets_artifact_build.yml b/.github/workflows/assets_artifact_build.yml index 447f95bf..a74ae7cc 100644 --- a/.github/workflows/assets_artifact_build.yml +++ b/.github/workflows/assets_artifact_build.yml @@ -8,6 +8,9 @@ on: branches: - '*' - "!l10n_*" # Dont test localization branches + tags: + - 'v*.*.*' + - 'v*.*.*-**' pull_request: branches: - '*' @@ -17,12 +20,14 @@ jobs: assets_artifact_build: name: Build assets artifact runs-on: ubuntu-22.04 + permissions: + contents: write env: APP_ENV: prod steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -37,7 +42,7 @@ jobs: run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -51,7 +56,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -60,9 +65,9 @@ jobs: ${{ runner.os }}-yarn- - name: Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '22' - name: Install yarn dependencies run: yarn install @@ -80,13 +85,20 @@ jobs: run: zip -r /tmp/partdb_assets.zip public/build/ vendor/ - name: Upload assets artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Only dependencies and built assets path: /tmp/partdb_assets.zip - name: Upload full artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Full Part-DB including dependencies and built assets path: /tmp/partdb_with_assets.zip + + - name: Upload assets as release attachment + if: startsWith(github.ref, 'refs/tags/') + run: | + gh release upload "${{ github.ref_name }}" /tmp/partdb_assets.zip /tmp/partdb_with_assets.zip --clobber + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index c912e769..210dbc18 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -15,16 +15,28 @@ on: - 'v*.*.*-**' jobs: - docker: - runs-on: ubuntu-latest + build: + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + platform-slug: amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + platform-slug: arm64 + - platform: linux/arm/v7 + runner: ubuntu-24.04-arm + platform-slug: armv7 + runs-on: ${{ matrix.runner }} steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Docker meta id: docker_meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: # list of Docker images to use as base name for tags images: | @@ -32,13 +44,12 @@ jobs: # Mark the image build from master as latest (as we dont have really releases yet) tags: | type=edge,branch=master - type=ref,event=branch, - type=ref,event=tag, + type=ref,event=branch + type=ref,event=tag type=schedule type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - type=ref,event=branch type=ref,event=pr labels: | org.opencontainers.image.source=${{ github.event.repository.clone_url }} @@ -49,31 +60,101 @@ jobs: org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-symfony org.opencontainers.image.authors=Jan Böhmer org.opencontainers.licenses=AGPL-3.0-or-later + # Disable automatic 'latest' tag in build jobs - it will be created in merge job + flavor: | + latest=false - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: 'arm64,arm' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to DockerHub if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v6 + name: Build and push by digest + id: build + uses: docker/build-push-action@v7 with: context: . - platforms: linux/amd64,linux/arm64,linux/arm/v7 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.docker_meta.outputs.tags }} + platforms: ${{ matrix.platform }} labels: ${{ steps.docker_meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + outputs: type=image,name=jbtronics/part-db1,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + cache-from: type=gha,scope=build-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} + + - + name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - + name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: digests-${{ matrix.platform-slug }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - build + if: github.event_name != 'pull_request' + steps: + - + name: Download digests + uses: actions/download-artifact@v8 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - + name: Docker meta + id: docker_meta + uses: docker/metadata-action@v6 + with: + images: | + jbtronics/part-db1 + tags: | + type=edge,branch=master + type=ref,event=branch + type=ref,event=tag + type=schedule + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=ref,event=pr + + - + name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - + name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'jbtronics/part-db1@sha256:%s ' *) + + - + name: Inspect image + run: | + docker buildx imagetools inspect jbtronics/part-db1:${{ steps.docker_meta.outputs.version }} diff --git a/.github/workflows/docker_frankenphp.yml b/.github/workflows/docker_frankenphp.yml index 0b2eb874..36ec322d 100644 --- a/.github/workflows/docker_frankenphp.yml +++ b/.github/workflows/docker_frankenphp.yml @@ -15,16 +15,28 @@ on: - 'v*.*.*-**' jobs: - docker: - runs-on: ubuntu-latest + build: + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + platform-slug: amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + platform-slug: arm64 + - platform: linux/arm/v7 + runner: ubuntu-24.04-arm + platform-slug: armv7 + runs-on: ${{ matrix.runner }} steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Docker meta id: docker_meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: # list of Docker images to use as base name for tags images: | @@ -32,13 +44,12 @@ jobs: # Mark the image build from master as latest (as we dont have really releases yet) tags: | type=edge,branch=master - type=ref,event=branch, - type=ref,event=tag, + type=ref,event=branch + type=ref,event=tag type=schedule type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - type=ref,event=branch type=ref,event=pr labels: | org.opencontainers.image.source=${{ github.event.repository.clone_url }} @@ -49,32 +60,102 @@ jobs: org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-server org.opencontainers.image.authors=Jan Böhmer org.opencontainers.licenses=AGPL-3.0-or-later + # Disable automatic 'latest' tag in build jobs - it will be created in merge job + flavor: | + latest=false - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: 'arm64,arm' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to DockerHub if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v6 + name: Build and push by digest + id: build + uses: docker/build-push-action@v7 with: context: . file: Dockerfile-frankenphp - platforms: linux/amd64,linux/arm64,linux/arm/v7 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.docker_meta.outputs.tags }} + platforms: ${{ matrix.platform }} labels: ${{ steps.docker_meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + outputs: type=image,name=partdborg/part-db,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + cache-from: type=gha,scope=build-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} + + - + name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - + name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: digests-${{ matrix.platform-slug }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - build + if: github.event_name != 'pull_request' + steps: + - + name: Download digests + uses: actions/download-artifact@v8 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - + name: Docker meta + id: docker_meta + uses: docker/metadata-action@v6 + with: + images: | + partdborg/part-db + tags: | + type=edge,branch=master + type=ref,event=branch + type=ref,event=tag + type=schedule + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=ref,event=pr + + - + name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - + name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'partdborg/part-db@sha256:%s ' *) + + - + name: Inspect image + run: | + docker buildx imagetools inspect partdborg/part-db:${{ steps.docker_meta.outputs.version }} diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 1de98ee9..f47ce87b 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -34,7 +34,7 @@ jobs: run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7c0965b..5b756228 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,7 +46,7 @@ jobs: if: matrix.db-type == 'postgres' - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -81,7 +81,7 @@ jobs: id: composer-cache run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -92,7 +92,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -104,9 +104,9 @@ jobs: run: composer install --prefer-dist --no-progress - name: Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '22' - name: Install yarn dependencies run: yarn install @@ -129,7 +129,7 @@ jobs: run: ./bin/phpunit --coverage-clover=coverage.xml - name: Upload coverage - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: env_vars: PHP_VERSION,DB_TYPE token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index dd5c43db..704d6202 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,10 @@ uploads/* !uploads/.keep +# Some people use Certbot or similar tools to make SSL certificates. +# Also see https://www.rfc-editor.org/rfc/rfc5785 +public/.well-known/ + # Do not keep cache files .php_cs.cache .phpcs-cache @@ -50,4 +54,11 @@ phpstan.neon ###< phpstan/phpstan ### .claude/ -CLAUDE.md \ No newline at end of file +CLAUDE.md + +.codex +migrations/.codex +docker-data/ +scripts/ +db/ +docker-compose.yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d31c904e..5994a115 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # How to contribute -Thank you for consider to contribute to Part-DB! -Please read the text below, so your contributed content can be contributed easily to Part-DB. +Thank you for considering contributing to Part-DB! +Please read the text below, so your contributed content can be incorporated into Part-DB easily. You can contribute to Part-DB in various ways: * Report bugs and request new features via [issues](https://github.com/Part-DB/Part-DB-server/issues) @@ -18,38 +18,38 @@ Part-DB uses translation keys (e.g. part.info.title) that are sorted by their us was translated in other languages (this is possible via the "Other languages" dropdown in the translation editor). ## Project structure -Part-DB uses symfony's recommended [project structure](https://symfony.com/doc/current/best_practices.html). +Part-DB uses Symfony's recommended [project structure](https://symfony.com/doc/current/best_practices.html). Interesting folders are: -* `public`: Everything in this directory will be publicy accessible via web. Use this folder to serve static images. -* `assets`: The frontend assets are saved here. You can find the javascript and CSS code here. -* `src`: Part-DB's PHP code is saved here. Note that the sub directories are structured by the classes purposes (so use `Controller` Controllers, `Entities` for Database models, etc.) -* `translations`: The translations used in Part-DB are saved here +* `public`: Everything in this directory will be publicly accessible via web. Use this folder to serve static images. +* `assets`: The frontend assets are saved here. You can find the JavaScript and CSS code here. +* `src`: Part-DB's PHP code is saved here. Note that the subdirectories are structured by the classes' purposes (so use `Controller` for Controllers, `Entity` for Database models, etc.) +* `translations`: The translations used in Part-DB are saved here. * `templates`: The templates (HTML) that are used by Twig to render the different pages. Email templates are also saved here. -* `tests/`: Tests that can be run by PHPunit. +* `tests/`: Tests that can be run by PHPUnit. ## Development environment -For setting up an development you will need to install PHP, composer, a database server (MySQL or MariaDB) and yarn (which needs an nodejs environment). -* Copy `.env` to `.env.local` and change `APP_ENV` to `APP_ENV=dev`. That way you will get development tools (symfony profiler) and other features that +For setting up a development environment, you will need to install PHP, Composer, a database server (MySQL or MariaDB) and yarn (which needs a Node.js environment). +* Copy `.env` to `.env.local` and change `APP_ENV` to `APP_ENV=dev`. That way you will get development tools (Symfony profiler) and other features that will simplify development. -* Run `composer install` (without -o) to install PHP dependencies and `yarn install` to install frontend dependencies -* Run `yarn watch`. The program will run in the background and compile the frontend files whenever you change something in the CSS or TypeScript files -* For running Part-DB it is recommended to use [Symfony CLI](https://symfony.com/download). -That way you can run a correct configured webserver with `symfony serve` +* Run `composer install` (without -o) to install PHP dependencies and `yarn install` to install frontend dependencies. +* Run `yarn watch`. The program will run in the background and compile the frontend files whenever you change something in the CSS or TypeScript files. +* For running Part-DB, it is recommended to use [Symfony CLI](https://symfony.com/download). +That way you can run a correctly configured webserver with `symfony serve`. ## Coding style -Code should follow the [PSR12-Standard](https://www.php-fig.org/psr/psr-12/) and symfony's [coding standards](https://symfony.com/doc/current/contributing/code/standards.html). +Code should follow the [PSR-12 Standard](https://www.php-fig.org/psr/psr-12/) and Symfony's [coding standards](https://symfony.com/doc/current/contributing/code/standards.html). Part-DB uses [Easy Coding Standard](https://github.com/symplify/easy-coding-standard) to check and fix coding style violations: -* To check your code for valid code style run `vendor/bin/ecs check src/` -* To fix violations run `vendor/bin/ecs check src/` (please checks afterwards if the code is valid afterwards) +* To check your code for valid code style, run `vendor/bin/ecs check src/` +* To fix violations, run `vendor/bin/ecs check src/ --fix` (please check afterwards if the code is still valid) ## GitHub actions -Part-DB uses GitHub actions to run various tests and checks on the code: +Part-DB uses GitHub Actions to run various tests and checks on the code: * Yarn dependencies can compile -* PHPunit tests run successful -* Config files, translations and templates has valid syntax -* Doctrine schema valid -* No known vulnerable dependecies are used -* Static analysis successful (phpstan with `--level=2`) +* PHPUnit tests run successfully +* Config files, translations, and templates have valid syntax +* Doctrine schema is valid +* No known vulnerable dependencies are used +* Static analysis is successful (phpstan with `--level=2`) -Further the code coverage of the PHPunit tests is determined and uploaded to [CodeCov](https://codecov.io/gh/Part-DB/Part-DB-server). +Further, the code coverage of the PHPUnit tests is determined and uploaded to [CodeCov](https://codecov.io/gh/Part-DB/Part-DB-server). diff --git a/Dockerfile b/Dockerfile index cb18c78f..e848acc1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,75 @@ +# syntax=docker/dockerfile:1 ARG BASE_IMAGE=debian:bookworm-slim ARG PHP_VERSION=8.4 +ARG NODE_VERSION=22 +# Node.js build stage for building frontend assets +# Use native platform for build stage as it's platform-independent +FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm-slim AS node-builder +ARG TARGETARCH +WORKDIR /app +# Install composer and minimal PHP for running Symfony commands +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Use BuildKit cache mounts for apt in builder stage +RUN --mount=type=cache,id=apt-cache-node-$TARGETARCH,target=/var/cache/apt \ + --mount=type=cache,id=apt-lists-node-$TARGETARCH,target=/var/lib/apt/lists \ + apt-get update && apt-get install -y --no-install-recommends \ + php-cli \ + php-xml \ + php-mbstring \ + unzip \ + git \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Copy composer files and install dependencies (needed for Symfony UX assets) +COPY composer.json composer.lock symfony.lock ./ + +# Use BuildKit cache for Composer downloads +RUN --mount=type=cache,id=composer-cache,target=/root/.cache/composer \ + composer install --no-scripts --no-autoloader --no-dev --prefer-dist --ignore-platform-reqs + +# Copy all application files needed for cache warmup and webpack build +COPY .env* ./ +COPY bin ./bin +COPY config ./config +COPY src ./src +COPY translations ./translations +COPY public ./public +COPY assets ./assets +COPY webpack.config.js ./ + +# Generate autoloader +RUN composer dump-autoload + +# Create required directories for cache warmup +RUN mkdir -p var/cache var/log uploads public/media + +# Dump translations, which we need for cache warmup +RUN php bin/console cache:warmup -n --env=prod 2>&1 + +# Copy package files and install node dependencies +COPY package.json yarn.lock ./ +# Use BuildKit cache for yarn/npm +RUN --mount=type=cache,id=yarn-cache,target=/root/.cache/yarn \ + --mount=type=cache,id=npm-cache,target=/root/.npm \ + yarn install --network-timeout 600000 + +# Build the assets +RUN yarn build + +# Clean up +RUN yarn cache clean && rm -rf node_modules/ + +# Base stage for PHP FROM ${BASE_IMAGE} AS base ARG PHP_VERSION +ARG TARGETARCH -# Install needed dependencies for PHP build -#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \ -# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \ -# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* - -RUN apt-get update && apt-get -y install \ +# Use BuildKit cache mounts for apt in base stage +RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \ + --mount=type=cache,id=apt-lists-$TARGETARCH,target=/var/lib/apt/lists \ + apt-get update && apt-get -y install \ apt-transport-https \ lsb-release \ ca-certificates \ @@ -39,21 +99,10 @@ RUN apt-get update && apt-get -y install \ gpg \ sudo \ && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* \ -# Create workdir and set permissions if directory does not exists && mkdir -p /var/www/html \ && chown -R www-data:www-data /var/www/html \ -# delete the "index.html" that installing Apache drops in here && rm -rvf /var/www/html/* -# Install node and yarn -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ - curl -sL https://deb.nodesource.com/setup_22.x | bash - && \ - apt-get update && apt-get install -y \ - nodejs \ - yarn \ - && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* - # Install composer COPY --from=composer:latest /usr/bin/composer /usr/bin/composer @@ -67,14 +116,12 @@ ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars # : ${APACHE_RUN_USER:=www-data} # export APACHE_RUN_USER # so that they can be overridden at runtime ("-e APACHE_RUN_USER=...") -RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \ - set -eux; . "$APACHE_ENVVARS"; \ - \ - # logs should go to stdout / stderr - ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \ - ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \ - ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \ - chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR"; +RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS" && \ + set -eux; . "$APACHE_ENVVARS" && \ + ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log" && \ + ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log" && \ + ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log" && \ + chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR" # --- @@ -143,7 +190,6 @@ COPY --chown=www-data:www-data . . # Setup apache2 RUN a2dissite 000-default.conf && \ a2ensite symfony.conf && \ -# Enable php-fpm a2enmod proxy_fcgi setenvif && \ a2enconf php${PHP_VERSION}-fpm && \ a2enconf docker-php && \ @@ -151,12 +197,13 @@ RUN a2dissite 000-default.conf && \ # Install composer and yarn dependencies for Part-DB USER www-data -RUN composer install -a --no-dev && \ +# Use BuildKit cache for Composer when running as www-data by setting COMPOSER_CACHE_DIR +RUN --mount=type=cache,id=composer-cache,target=/tmp/.composer-cache \ + COMPOSER_CACHE_DIR=/tmp/.composer-cache composer install -a --no-dev && \ composer clear-cache -RUN yarn install --network-timeout 600000 && \ - yarn build && \ - yarn cache clean && \ - rm -rf node_modules/ + +# Copy built frontend assets from node-builder stage +COPY --from=node-builder --chown=www-data:www-data /app/public/build ./public/build # Use docker env to output logs to stdout ENV APP_ENV=docker @@ -168,10 +215,12 @@ USER root RUN sed -i "s/PHP_VERSION/${PHP_VERSION}/g" ./.docker/partdb-entrypoint.sh # Copy entrypoint and apache2-foreground to /usr/local/bin and make it executable -RUN install ./.docker/partdb-entrypoint.sh /usr/local/bin && \ - install ./.docker/apache2-foreground /usr/local/bin +# Convert CRLF -> LF and install entrypoint scripts with executable mode +RUN sed -i 's/\r$//' ./.docker/partdb-entrypoint.sh ./.docker/apache2-foreground && \ + install -m 0755 ./.docker/partdb-entrypoint.sh /usr/local/bin/ && \ + install -m 0755 ./.docker/apache2-foreground /usr/local/bin/ ENTRYPOINT ["partdb-entrypoint.sh"] -CMD ["apache2-foreground"] +CMD ["/usr/local/bin/apache2-foreground"] # https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop STOPSIGNAL SIGWINCH diff --git a/Dockerfile-frankenphp b/Dockerfile-frankenphp index f381f330..bdf9c1fd 100644 --- a/Dockerfile-frankenphp +++ b/Dockerfile-frankenphp @@ -1,6 +1,72 @@ -FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream +ARG NODE_VERSION=22 -RUN apt-get update && apt-get -y install \ +# Node.js build stage for building frontend assets +# Use native platform for build stage as it's platform-independent +FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm-slim AS node-builder +ARG TARGETARCH +WORKDIR /app + +# Install composer and minimal PHP for running Symfony commands +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Use BuildKit cache mounts for apt in builder stage +RUN --mount=type=cache,id=apt-cache-node-$TARGETARCH,target=/var/cache/apt \ + --mount=type=cache,id=apt-lists-node-$TARGETARCH,target=/var/lib/apt/lists \ + apt-get update && apt-get install -y --no-install-recommends \ + php-cli \ + php-xml \ + php-mbstring \ + unzip \ + git \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Copy composer files and install dependencies (needed for Symfony UX assets) +COPY composer.json composer.lock symfony.lock ./ + +# Use BuildKit cache for Composer downloads +RUN --mount=type=cache,id=composer-cache,target=/root/.cache/composer \ + composer install --no-scripts --no-autoloader --no-dev --prefer-dist --ignore-platform-reqs + +# Copy all application files needed for cache warmup and webpack build +COPY .env* ./ +COPY bin ./bin +COPY config ./config +COPY src ./src +COPY translations ./translations +COPY public ./public +COPY assets ./assets +COPY webpack.config.js ./ + +# Generate autoloader +RUN composer dump-autoload + +# Create required directories for cache warmup +RUN mkdir -p var/cache var/log uploads public/media + +# Dump translations, which we need for cache warmup +RUN php bin/console cache:warmup -n --env=prod 2>&1 + +# Copy package files and install node dependencies +COPY package.json yarn.lock ./ + +# Use BuildKit cache for yarn/npm +RUN --mount=type=cache,id=yarn-cache,target=/root/.cache/yarn \ + --mount=type=cache,id=npm-cache,target=/root/.npm \ + yarn install --network-timeout 600000 + + +# Build the assets +RUN yarn build + +# Clean up +RUN yarn cache clean && rm -rf node_modules/ + +# FrankenPHP base stage +FROM dunglas/frankenphp:1-php8.4-bookworm AS frankenphp_upstream +ARG TARGETARCH +RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \ + --mount=type=cache,id=apt-lists-$TARGETARCH,target=/var/lib/apt/lists \ + apt-get update && apt-get -y install \ curl \ ca-certificates \ mariadb-client \ @@ -13,34 +79,6 @@ RUN apt-get update && apt-get -y install \ zip \ && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*; -RUN set -eux; \ - # Prepare keyrings directory - mkdir -p /etc/apt/keyrings; \ - \ - # Import Yarn GPG key - curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg \ - | tee /etc/apt/keyrings/yarn.gpg >/dev/null; \ - chmod 644 /etc/apt/keyrings/yarn.gpg; \ - \ - # Add Yarn repo with signed-by - echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian stable main" \ - | tee /etc/apt/sources.list.d/yarn.list; \ - \ - # Run NodeSource setup script (unchanged) - curl -sL https://deb.nodesource.com/setup_22.x | bash -; \ - \ - # Install Node.js + Yarn - apt-get update; \ - apt-get install -y --no-install-recommends \ - nodejs \ - yarn; \ - \ - # Cleanup - apt-get -y autoremove; \ - apt-get clean autoclean; \ - rm -rf /var/lib/apt/lists/* - - # Install PHP RUN set -eux; \ install-php-extensions \ @@ -86,14 +124,11 @@ COPY --link . ./ RUN set -eux; \ mkdir -p var/cache var/log; \ composer dump-autoload --classmap-authoritative --no-dev; \ - composer dump-env prod; \ composer run-script --no-dev post-install-cmd; \ chmod +x bin/console; sync; -RUN yarn install --network-timeout 600000 && \ - yarn build && \ - yarn cache clean && \ - rm -rf node_modules/ +# Copy built frontend assets from node-builder stage +COPY --from=node-builder /app/public/build ./public/build # Use docker env to output logs to stdout ENV APP_ENV=docker @@ -112,8 +147,8 @@ VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"] HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1 # See https://caddyserver.com/docs/conventions#file-locations for details -ENV XDG_CONFIG_HOME /config -ENV XDG_DATA_HOME /data +ENV XDG_CONFIG_HOME=/config +ENV XDG_DATA_HOME=/data EXPOSE 80 EXPOSE 443 diff --git a/README.md b/README.md index 291b574a..3c738025 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/?branch=master) ![PHPUnit Tests](https://github.com/Part-DB/Part-DB-symfony/workflows/PHPUnit%20Tests/badge.svg) ![Static analysis](https://github.com/Part-DB/Part-DB-symfony/workflows/Static%20analysis/badge.svg) [![codecov](https://codecov.io/gh/Part-DB/Part-DB-server/branch/master/graph/badge.svg)](https://codecov.io/gh/Part-DB/Part-DB-server) @@ -29,8 +28,8 @@ If you want to test Part-DB without installing it, you can use [this](https://de You can log in with username: *user* and password: *user*. -Every change to the master branch gets automatically deployed, so it represents the current development progress and is -may not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading +Every change to the master branch gets automatically deployed, so it represents the current development progress and +may not be completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading the page for the first time. @@ -62,6 +61,8 @@ for the first time. * Automatic thumbnail generation for pictures * Use cloud providers (like Octopart, Digikey, Farnell, LCSC or TME) to automatically get part information, datasheets, and prices for parts +* Retrieve part information from arbitrary shop websites, using either conventional data extraction from structured metadata, or AI based data extraction. +A browser plugin allows to quickly submit parts from any website to your Part-DB instance, and even allows to circumvent anti-bot measures on shop websites. * API to access Part-DB from other applications/scripts * [Integration with KiCad](https://docs.part-db.de/usage/eda_integration.html): Use Part-DB as the central datasource for your KiCad and see available parts from Part-DB directly inside KiCad. @@ -74,11 +75,11 @@ Part-DB is also used by small companies and universities for managing their inve ## Requirements * A **web server** (like Apache2 or nginx) that is capable of - running [Symfony 6](https://symfony.com/doc/current/reference/requirements.html), + running [Symfony 7](https://symfony.com/doc/current/reference/requirements.html), this includes a minimum PHP version of **PHP 8.2** * A **MySQL** (at least 5.7) /**MariaDB** (at least 10.4) database server, or **PostgreSQL** 10+ if you do not want to use SQLite. * Shell access to your server is highly recommended! -* For building the client-side assets **yarn** and **nodejs** (>= 20.0) is needed. +* For building the client-side assets **yarn** and **nodejs** (>= 22.0) is needed. ## Installation @@ -142,7 +143,7 @@ There you will find various methods to support development on a monthly or a one ## Built with -* [Symfony 5](https://symfony.com/): The main framework used for the serverside PHP +* [Symfony 6](https://symfony.com/): The main framework used for the serverside PHP * [Bootstrap 5](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com/): Used as website theme * [Fontawesome](https://fontawesome.com/): Used as icon set * [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend diff --git a/VERSION b/VERSION index c043eea7..d8b69897 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.1 +2.12.0 diff --git a/assets/commands/kicad_populate_default_mappings.json b/assets/commands/kicad_populate_default_mappings.json new file mode 100644 index 00000000..a942667c --- /dev/null +++ b/assets/commands/kicad_populate_default_mappings.json @@ -0,0 +1,206 @@ +{ + "_comment": "Default KiCad footprint/symbol mappings for partdb:kicad:populate command. Based on KiCad 9.x standard libraries. Use --mapping-file to override or extend these mappings.", + "footprints": { + "SOT-23": "Package_TO_SOT_SMD:SOT-23", + "SOT-23-3": "Package_TO_SOT_SMD:SOT-23", + "SOT-23-5": "Package_TO_SOT_SMD:SOT-23-5", + "SOT-23-6": "Package_TO_SOT_SMD:SOT-23-6", + "SOT-223": "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + "SOT-223-3": "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + "SOT-89": "Package_TO_SOT_SMD:SOT-89-3", + "SOT-89-3": "Package_TO_SOT_SMD:SOT-89-3", + "SOT-323": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SOT-363": "Package_TO_SOT_SMD:SOT-363_SC-70-6", + "TSOT-25": "Package_TO_SOT_SMD:SOT-23-5", + "SC-70-5": "Package_TO_SOT_SMD:SOT-353_SC-70-5", + "SC-70-6": "Package_TO_SOT_SMD:SOT-363_SC-70-6", + "TO-220": "Package_TO_SOT_THT:TO-220-3_Vertical", + "TO-220AB": "Package_TO_SOT_THT:TO-220-3_Vertical", + "TO-220AB-3": "Package_TO_SOT_THT:TO-220-3_Vertical", + "TO-220FP": "Package_TO_SOT_THT:TO-220F-3_Vertical", + "TO-247-3": "Package_TO_SOT_THT:TO-247-3_Vertical", + "TO-92": "Package_TO_SOT_THT:TO-92_Inline", + "TO-92-3": "Package_TO_SOT_THT:TO-92_Inline", + "TO-252": "Package_TO_SOT_SMD:TO-252-2", + "TO-252-2L": "Package_TO_SOT_SMD:TO-252-2", + "TO-252-3L": "Package_TO_SOT_SMD:TO-252-3", + "TO-263": "Package_TO_SOT_SMD:TO-263-2", + "TO-263-2": "Package_TO_SOT_SMD:TO-263-2", + "D2PAK": "Package_TO_SOT_SMD:TO-252-2", + "DPAK": "Package_TO_SOT_SMD:TO-252-2", + "SOIC-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "ESOP-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "SOIC-14": "Package_SO:SOIC-14_3.9x8.7mm_P1.27mm", + "SOIC-16": "Package_SO:SOIC-16_3.9x9.9mm_P1.27mm", + "TSSOP-8": "Package_SO:TSSOP-8_3x3mm_P0.65mm", + "TSSOP-14": "Package_SO:TSSOP-14_4.4x5mm_P0.65mm", + "TSSOP-16": "Package_SO:TSSOP-16_4.4x5mm_P0.65mm", + "TSSOP-16L": "Package_SO:TSSOP-16_4.4x5mm_P0.65mm", + "TSSOP-20": "Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm", + "MSOP-8": "Package_SO:MSOP-8_3x3mm_P0.65mm", + "MSOP-10": "Package_SO:MSOP-10_3x3mm_P0.5mm", + "MSOP-16": "Package_SO:MSOP-16_3x4mm_P0.5mm", + "SO-5": "Package_TO_SOT_SMD:SOT-23-5", + "DIP-4": "Package_DIP:DIP-4_W7.62mm", + "DIP-6": "Package_DIP:DIP-6_W7.62mm", + "DIP-8": "Package_DIP:DIP-8_W7.62mm", + "DIP-14": "Package_DIP:DIP-14_W7.62mm", + "DIP-16": "Package_DIP:DIP-16_W7.62mm", + "DIP-18": "Package_DIP:DIP-18_W7.62mm", + "DIP-20": "Package_DIP:DIP-20_W7.62mm", + "DIP-24": "Package_DIP:DIP-24_W7.62mm", + "DIP-28": "Package_DIP:DIP-28_W7.62mm", + "DIP-40": "Package_DIP:DIP-40_W15.24mm", + "QFN-8": "Package_DFN_QFN:QFN-8-1EP_3x3mm_P0.65mm_EP1.55x1.55mm", + "QFN-12(3x3)": "Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.65x1.65mm", + "QFN-16": "Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm", + "QFN-20": "Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm", + "QFN-24": "Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm", + "QFN-32": "Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm", + "QFN-48": "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm", + "TQFP-32": "Package_QFP:TQFP-32_7x7mm_P0.8mm", + "TQFP-44": "Package_QFP:TQFP-44_10x10mm_P0.8mm", + "TQFP-48": "Package_QFP:TQFP-48_7x7mm_P0.5mm", + "TQFP-48(7x7)": "Package_QFP:TQFP-48_7x7mm_P0.5mm", + "TQFP-64": "Package_QFP:TQFP-64_10x10mm_P0.5mm", + "TQFP-100": "Package_QFP:TQFP-100_14x14mm_P0.5mm", + "LQFP-32": "Package_QFP:LQFP-32_7x7mm_P0.8mm", + "LQFP-48": "Package_QFP:LQFP-48_7x7mm_P0.5mm", + "LQFP-64": "Package_QFP:LQFP-64_10x10mm_P0.5mm", + "LQFP-100": "Package_QFP:LQFP-100_14x14mm_P0.5mm", + + "SOD-123": "Diode_SMD:D_SOD-123", + "SOD-123F": "Diode_SMD:D_SOD-123F", + "SOD-123FL": "Diode_SMD:D_SOD-123F", + "SOD-323": "Diode_SMD:D_SOD-323", + "SOD-523": "Diode_SMD:D_SOD-523", + "SOD-882": "Diode_SMD:D_SOD-882", + "SOD-882D": "Diode_SMD:D_SOD-882", + "SMA(DO-214AC)": "Diode_SMD:D_SMA", + "SMA": "Diode_SMD:D_SMA", + "SMB": "Diode_SMD:D_SMB", + "SMC": "Diode_SMD:D_SMC", + + "DO-35": "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal", + "DO-35(DO-204AH)": "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal", + "DO-41": "Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal", + "DO-201": "Diode_THT:D_DO-201_P15.24mm_Horizontal", + + "DFN-2(0.6x1)": "Package_DFN_QFN:DFN-2-1EP_0.6x1.0mm_P0.65mm_EP0.2x0.55mm", + "DFN1006-2": "Package_DFN_QFN:DFN-2_1.0x0.6mm", + "DFN-6": "Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.65mm_EP1x1.6mm", + "DFN-8": "Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.5mm", + + "0201": "Resistor_SMD:R_0201_0603Metric", + "0402": "Resistor_SMD:R_0402_1005Metric", + "0603": "Resistor_SMD:R_0603_1608Metric", + "0805": "Resistor_SMD:R_0805_2012Metric", + "1206": "Resistor_SMD:R_1206_3216Metric", + "1210": "Resistor_SMD:R_1210_3225Metric", + "1812": "Resistor_SMD:R_1812_4532Metric", + "2010": "Resistor_SMD:R_2010_5025Metric", + "2512": "Resistor_SMD:R_2512_6332Metric", + "2917": "Resistor_SMD:R_2917_7343Metric", + "2920": "Resistor_SMD:R_2920_7350Metric", + + "CASE-A-3216-18(mm)": "Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A", + "CASE-B-3528-21(mm)": "Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B", + "CASE-C-6032-28(mm)": "Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C", + "CASE-D-7343-31(mm)": "Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D", + "CASE-E-7343-43(mm)": "Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-E", + + "SMD,D4xL5.4mm": "Capacitor_SMD:CP_Elec_4x5.4", + "SMD,D5xL5.4mm": "Capacitor_SMD:CP_Elec_5x5.4", + "SMD,D6.3xL5.4mm": "Capacitor_SMD:CP_Elec_6.3x5.4", + "SMD,D6.3xL7.7mm": "Capacitor_SMD:CP_Elec_6.3x7.7", + "SMD,D8xL6.5mm": "Capacitor_SMD:CP_Elec_8x6.5", + "SMD,D8xL10mm": "Capacitor_SMD:CP_Elec_8x10", + "SMD,D10xL10mm": "Capacitor_SMD:CP_Elec_10x10", + "SMD,D10xL10.5mm": "Capacitor_SMD:CP_Elec_10x10.5", + + "Through Hole,D5xL11mm": "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm", + "Through Hole,D6.3xL11mm": "Capacitor_THT:CP_Radial_D6.3mm_P2.50mm", + "Through Hole,D8xL11mm": "Capacitor_THT:CP_Radial_D8.0mm_P3.50mm", + "Through Hole,D10xL16mm": "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm", + "Through Hole,D10xL20mm": "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm", + "Through Hole,D12.5xL20mm": "Capacitor_THT:CP_Radial_D12.5mm_P5.00mm", + + "LED 3mm": "LED_THT:LED_D3.0mm", + "LED 5mm": "LED_THT:LED_D5.0mm", + "LED 0603": "LED_SMD:LED_0603_1608Metric", + "LED 0805": "LED_SMD:LED_0805_2012Metric", + "SMD5050-4P": "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm", + "SMD5050-6P": "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm", + + "HC-49": "Crystal:Crystal_HC49-4H_Vertical", + "HC-49/U": "Crystal:Crystal_HC49-4H_Vertical", + "HC-49/S": "Crystal:Crystal_HC49-U_Vertical", + "HC-49/US": "Crystal:Crystal_HC49-U_Vertical", + + "USB-A": "Connector_USB:USB_A_Stewart_SS-52100-001_Horizontal", + "USB-B": "Connector_USB:USB_B_OST_USB-B1HSxx_Horizontal", + "USB-Mini-B": "Connector_USB:USB_Mini-B_Lumberg_2486_01_Horizontal", + "USB-Micro-B": "Connector_USB:USB_Micro-B_Molex-105017-0001", + "USB-C": "Connector_USB:USB_C_Receptacle_GCT_USB4085", + + "1x2 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical", + "1x3 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical", + "1x4 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical", + "1x5 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical", + "1x6 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical", + "1x8 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical", + "1x10 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Vertical", + "2x2 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x02_P2.54mm_Vertical", + "2x3 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_Vertical", + "2x4 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x04_P2.54mm_Vertical", + "2x5 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x05_P2.54mm_Vertical", + "2x10 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x10_P2.54mm_Vertical", + "2x20 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x20_P2.54mm_Vertical", + "SIP-3-2.54mm": "Package_SIP:SIP-3_P2.54mm", + "SIP-4-2.54mm": "Package_SIP:SIP-4_P2.54mm", + "SIP-5-2.54mm": "Package_SIP:SIP-5_P2.54mm" + }, + "categories": { + "Electrolytic": "Device:C_Polarized", + "Polarized": "Device:C_Polarized", + "Tantalum": "Device:C_Polarized", + "Zener": "Device:D_Zener", + "Schottky": "Device:D_Schottky", + "TVS": "Device:D_TVS", + "LED": "Device:LED", + "NPN": "Device:Q_NPN_BCE", + "PNP": "Device:Q_PNP_BCE", + "N-MOSFET": "Device:Q_NMOS_GDS", + "NMOS": "Device:Q_NMOS_GDS", + "N-MOS": "Device:Q_NMOS_GDS", + "P-MOSFET": "Device:Q_PMOS_GDS", + "PMOS": "Device:Q_PMOS_GDS", + "P-MOS": "Device:Q_PMOS_GDS", + "MOSFET": "Device:Q_NMOS_GDS", + "JFET": "Device:Q_NJFET_DSG", + "Ferrite": "Device:Ferrite_Bead", + "Crystal": "Device:Crystal", + "Oscillator": "Oscillator:Oscillator_Crystal", + "Fuse": "Device:Fuse", + "Transformer": "Device:Transformer_1P_1S", + "Resistor": "Device:R", + "Capacitor": "Device:C", + "Inductor": "Device:L", + "Diode": "Device:D", + "Transistor": "Device:Q_NPN_BCE", + "Voltage Regulator": "Regulator_Linear:LM317_TO-220", + "LDO": "Regulator_Linear:AMS1117-3.3", + "Op-Amp": "Amplifier_Operational:LM358", + "Comparator": "Comparator:LM393", + "Optocoupler": "Isolator:PC817", + "Relay": "Relay:Relay_DPDT", + "Connector": "Connector:Conn_01x02", + "Switch": "Switch:SW_Push", + "Button": "Switch:SW_Push", + "Potentiometer": "Device:R_POT", + "Trimpot": "Device:R_POT", + "Thermistor": "Device:Thermistor", + "Varistor": "Device:Varistor", + "Photo": "Device:LED" + } +} diff --git a/assets/controllers/backup_restore_controller.js b/assets/controllers/backup_restore_controller.js new file mode 100644 index 00000000..85ee327b --- /dev/null +++ b/assets/controllers/backup_restore_controller.js @@ -0,0 +1,55 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Controller } from '@hotwired/stimulus'; + +/** + * Stimulus controller for backup restore confirmation dialogs. + * Shows a confirmation dialog with backup details before allowing restore. + */ +export default class extends Controller { + static values = { + filename: { type: String, default: '' }, + date: { type: String, default: '' }, + confirmTitle: { type: String, default: 'Restore Backup' }, + confirmMessage: { type: String, default: 'Are you sure you want to restore from this backup?' }, + confirmWarning: { type: String, default: 'This will overwrite your current database. This action cannot be undone!' }, + }; + + connect() { + this.element.addEventListener('submit', this.handleSubmit.bind(this)); + } + + handleSubmit(event) { + // Always prevent default first + event.preventDefault(); + + // Build confirmation message + const message = this.confirmTitleValue + '\n\n' + + 'Backup: ' + this.filenameValue + '\n' + + 'Date: ' + this.dateValue + '\n\n' + + this.confirmMessageValue + '\n\n' + + '⚠️ ' + this.confirmWarningValue; + + // Only submit if user confirms + if (confirm(message)) { + this.element.submit(); + } + } +} diff --git a/assets/controllers/bulk_import_controller.js b/assets/controllers/bulk_import_controller.js index 49e4d60f..a04ff13e 100644 --- a/assets/controllers/bulk_import_controller.js +++ b/assets/controllers/bulk_import_controller.js @@ -3,14 +3,16 @@ import { generateCsrfHeaders } from "./csrf_protection_controller" export default class extends Controller { static targets = ["progressBar", "progressText"] - static values = { + static values = { jobId: Number, partId: Number, researchUrl: String, researchAllUrl: String, markCompletedUrl: String, markSkippedUrl: String, - markPendingUrl: String + markPendingUrl: String, + quickApplyUrl: String, + quickApplyAllUrl: String } connect() { @@ -119,13 +121,11 @@ export default class extends Controller { async markSkipped(event) { const partId = event.currentTarget.dataset.partId - const reason = prompt('Reason for skipping (optional):') || '' - + try { const url = this.markSkippedUrlValue.replace('__PART_ID__', partId) const data = await this.fetchWithErrorHandling(url, { - method: 'POST', - body: JSON.stringify({ reason }) + method: 'POST' }) if (data.success) { @@ -321,6 +321,94 @@ export default class extends Controller { } } + async quickApply(event) { + event.preventDefault() + event.stopPropagation() + + const partId = event.currentTarget.dataset.partId + const providerKey = event.currentTarget.dataset.providerKey + const providerId = event.currentTarget.dataset.providerId + const button = event.currentTarget + const originalHtml = button.innerHTML + + button.disabled = true + button.innerHTML = ' Applying...' + + try { + const url = this.quickApplyUrlValue.replace('__PART_ID__', partId) + const data = await this.fetchWithErrorHandling(url, { + method: 'POST', + body: JSON.stringify({ providerKey, providerId }) + }, 60000) + + if (data.success) { + this.updateProgressDisplay(data) + this.showSuccessMessage(data.message || 'Part updated successfully') + sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString()) + window.location.reload() + } else { + this.showErrorMessage(data.error || 'Quick apply failed') + button.innerHTML = originalHtml + button.disabled = false + } + } catch (error) { + console.error('Error in quick apply:', error) + this.showErrorMessage(error.message || 'Quick apply failed') + button.innerHTML = originalHtml + button.disabled = false + } + } + + async quickApplyAll(event) { + event.preventDefault() + event.stopPropagation() + + if (!confirm('This will apply the top search result to all pending parts without individual review. Continue?')) { + return + } + + const button = event.currentTarget + const spinner = document.getElementById('quick-apply-all-spinner') + const originalHtml = button.innerHTML + + button.disabled = true + if (spinner) { + spinner.style.display = 'inline-block' + } + + try { + const data = await this.fetchWithErrorHandling(this.quickApplyAllUrlValue, { + method: 'POST' + }, 300000) + + if (data.success) { + this.updateProgressDisplay(data) + + let message = data.message || 'Bulk apply completed' + if (data.errors && data.errors.length > 0) { + message += '\nErrors:\n' + data.errors.join('\n') + } + + this.showSuccessMessage(message) + sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString()) + window.location.reload() + } else { + this.showErrorMessage(data.error || 'Bulk apply failed') + button.innerHTML = originalHtml + button.disabled = false + } + } catch (error) { + console.error('Error in quick apply all:', error) + this.showErrorMessage(error.message || 'Bulk apply failed') + button.innerHTML = originalHtml + button.disabled = false + } finally { + if (spinner) { + spinner.style.display = 'none' + } + } + } + showSuccessMessage(message) { this.showToast('success', message) } diff --git a/assets/controllers/common/dirty_form_controller.js b/assets/controllers/common/dirty_form_controller.js new file mode 100644 index 00000000..aad2e6b0 --- /dev/null +++ b/assets/controllers/common/dirty_form_controller.js @@ -0,0 +1,274 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import {Controller} from "@hotwired/stimulus"; +import {visit} from "@hotwired/turbo"; +import * as bootbox from "bootbox"; +import "../../css/components/bootbox_extensions.css"; +import "../../css/components/dirty_form.css"; + +/** + * Attach to a
element (or a wrapper containing a ) to prevent accidental navigation + * away when the form has unsaved changes. + * + * Dirty detection is event-driven: `change` and `input` events bubble up to the form and trigger + * a check of whether any element's current value differs from the DOM default recorded in the HTML + * (`defaultValue` / `defaultChecked` / `option.defaultSelected`). Using both events covers both + * native widgets (which fire `change`) and rich-text editors like CKEditor (which fire `input` + * when they sync their underlying textarea). + * + * Validation failures (server returns 200 with `.is-invalid` fields) are always treated as dirty: + * the submitted data was never saved, so navigating away would lose it. This removes the need for + * any snapshot mechanism — the `.is-invalid` classes in the re-rendered HTML are the signal. + * + * Intercepts three navigation paths: + * 1. Any link click (capture phase) + * 2. window beforeunload + * 3. turbo:before-visit + * + * Values: + * - confirmTitle (String) – dialog title + * - confirmMessage (String) – dialog body text + */ +export default class extends Controller { + static values = { + confirmTitle: {type: String, default: 'Unsaved Changes'}, + confirmMessage: {type: String, default: 'You have unsaved changes. Are you sure you want to leave this page?'}, + }; + + connect() { + this._form = (this.element.tagName === 'FORM') ? this.element : this.element.querySelector('form'); + this._isDirty = false; + this._submitting = false; + this._navigating = false; + + this._changeHandler = this._handleChange.bind(this); + this._linkClickHandler = this._handleLinkClick.bind(this); + this._beforeUnloadHandler = this._handleBeforeUnload.bind(this); + this._turboBeforeVisitHandler = this._handleTurboBeforeVisit.bind(this); + this._turboSubmitEndHandler = this._handleTurboSubmitEnd.bind(this); + + if (this._form) { + this._form.addEventListener('change', this._changeHandler); + // CKEditor (and other rich-text widgets) dispatch `input` rather than `change` + // when their underlying textarea value is updated. + this._form.addEventListener('input', this._changeHandler); + } + document.addEventListener('click', this._linkClickHandler, true); + window.addEventListener('beforeunload', this._beforeUnloadHandler); + document.addEventListener('turbo:before-visit', this._turboBeforeVisitHandler); + document.addEventListener('turbo:submit-end', this._turboSubmitEndHandler); + + const modal = this.element.closest('.modal'); + if (modal) { + this._modal = modal; + this._modalHideHandler = this._handleModalHide.bind(this); + modal.addEventListener('hide.bs.modal', this._modalHideHandler); + } + } + + disconnect() { + if (this._form) { + this._form.removeEventListener('change', this._changeHandler); + this._form.removeEventListener('input', this._changeHandler); + } + document.removeEventListener('click', this._linkClickHandler, true); + window.removeEventListener('beforeunload', this._beforeUnloadHandler); + document.removeEventListener('turbo:before-visit', this._turboBeforeVisitHandler); + document.removeEventListener('turbo:submit-end', this._turboSubmitEndHandler); + + if (this._modal && this._modalHideHandler) { + this._modal.removeEventListener('hide.bs.modal', this._modalHideHandler); + } + } + + /** data-action="submit->common--dirty-form#submit" — suppresses the guard while saving. */ + submit() { + this._submitting = true; + } + + /** + * data-action="reset->common--dirty-form#resetDirtyState" — marks the form as clean after + * a programmatic reset. Native change events are not fired by form.reset(), so we set the + * flag directly. Turbo also calls form.reset() internally before the post-submit redirect; + * the _submitting guard prevents that from incorrectly clearing the flag. + */ + resetDirtyState() { + if (this._submitting) return; + + // Wait for a frame to allow the form's DOM state to update after the reset() call, then refresh markers and update the dirty flag. + requestAnimationFrame(() => { + this._isDirty = false; + this._clearDirtyMarkers(); + }); + } + + _handleChange(event) { + const target = event?.target; + if (target?.name) { + this._updateDirtyMarker(target); + } else { + this._refreshDirtyMarkers(); + } + this._isDirty = this._form?.querySelector('[data-dirty]') !== null; + } + + /** + * Walk every named form element and update its `data-dirty` attribute. + * Un-named elements (e.g. the visible TristateCheckbox whose name was removed) are + * skipped — they are not submitted and are not the source of truth for form data. + */ + _refreshDirtyMarkers() { + if (!this._form) return; + for (const el of this._form.elements) { + if (!el.name) continue; + this._updateDirtyMarker(el); + } + } + + /** + * Set or clear `data-dirty` on a single named form element. + * Hidden inputs are not visually rendered, so special handling applies: + * - TristateCheckbox: the hidden backing input is preceded by a nameless visual checkbox — + * mark that instead. + * - Other hidden inputs (e.g. CSRF tokens): ignored. + * TomSelect hides the so the dirty-check + * controller can detect changes, and restores that value when the form is reset. + */ +export default function form_reset_handler() { + const self = this; + const input = this.input; + + // Multiple selects not yet supported + if (input.multiple) { + return; + } + + // Always capture the initial value, even empty string. + // Empty string is falsy, so the old `|| null` guard would silently skip it, + // leaving data-default-value unset and breaking the dirty check for blank defaults. + input.dataset.defaultValue = input.value; + + if (input.form) { + input.form.addEventListener('reset', () => { + input.value = input.dataset.defaultValue ?? ''; + self.sync(); + }); + } +} diff --git a/assets/translator.js b/assets/translator.js index 0d5ae86b..ad967488 100644 --- a/assets/translator.js +++ b/assets/translator.js @@ -1,5 +1,6 @@ -import { localeFallbacks } from '../var/translations/configuration'; -import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator'; +import { createTranslator } from '@symfony/ux-translator'; +import { messages, localeFallbacks } from '../var/translations/index.js'; + /* * This file is part of the Symfony UX Translator package. * @@ -9,8 +10,12 @@ import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-tra * If you use TypeScript, you can rename this file to "translator.ts" to take advantage of types checking. */ -setLocaleFallbacks(localeFallbacks); +const translator = createTranslator({ + messages, + localeFallbacks, +}); -export { trans }; - -export * from '../var/translations'; \ No newline at end of file +// Wrapper function with default domain set to 'frontend' +export const trans = (id, parameters = {}, domain = 'frontend', locale = null) => { + return translator.trans(id, parameters, domain, locale); +}; diff --git a/bin/phpunit b/bin/phpunit index 692baccb..ac5eef11 100755 --- a/bin/phpunit +++ b/bin/phpunit @@ -1,23 +1,4 @@ #!/usr/bin/env php = 80000) { - require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit'; - } else { - define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php'); - require PHPUNIT_COMPOSER_INSTALL; - PHPUnit\TextUI\Command::main(); - } -} else { - if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { - echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; - exit(1); - } - - require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; -} +require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit'; diff --git a/composer.json b/composer.json index f53130d4..a1f15834 100644 --- a/composer.json +++ b/composer.json @@ -11,12 +11,14 @@ "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", + "ext-zip": "*", "amphp/http-client": "^5.1", "api-platform/doctrine-orm": "^4.1", "api-platform/json-api": "^4.0.0", "api-platform/symfony": "^4.0.0", "beberlei/doctrineextensions": "^1.2", - "brick/math": "^0.13.1", + "brick/math": "^0.17.0", + "brick/schema": "^0.2.0", "composer/ca-bundle": "^1.5", "composer/package-versions-deprecated": "^1.11.99.5", "doctrine/data-fixtures": "^2.0.0", @@ -26,11 +28,12 @@ "doctrine/orm": "^3.2.0", "dompdf/dompdf": "^3.1.2", "gregwar/captcha-bundle": "^2.1.0", - "hshn/base64-encoded-file": "^5.0", + "hshn/base64-encoded-file": "^6.0", "jbtronics/2fa-webauthn": "^3.0.0", "jbtronics/dompdf-font-loader-bundle": "^1.0.0", "jbtronics/settings-bundle": "^3.0.0", "jfcherng/php-diff": "^6.14", + "jkphl/micrometa": "^v3.4.0", "knpuniversity/oauth2-client-bundle": "^2.15", "league/commonmark": "^2.7", "league/csv": "^9.8.0", @@ -43,12 +46,10 @@ "nelmio/security-bundle": "^3.0", "nyholm/psr7": "^1.1", "omines/datatables-bundle": "^0.10.0", - "paragonie/sodium_compat": "^1.21", "part-db/label-fonts": "^1.0", "part-db/swap-bundle": "^6.0.0", "phpoffice/phpspreadsheet": "^5.0.0", "rhukster/dom-sanitizer": "^1.0", - "runtime/frankenphp-symfony": "^0.2.0", "s9e/text-formatter": "^2.1", "scheb/2fa-backup-code": "^v7.11.0", "scheb/2fa-bundle": "^v7.11.0", @@ -56,39 +57,43 @@ "scheb/2fa-trusted-device": "^v7.11.0", "shivas/versioning-bundle": "^4.0", "spatie/db-dumper": "^3.3.1", + "symfony/ai-bundle": "^0.9.0", + "symfony/ai-lm-studio-platform": "^0.9.0", + "symfony/ai-open-router-platform": "^0.9.0", "symfony/apache-pack": "^1.0", - "symfony/asset": "7.3.*", - "symfony/console": "7.3.*", - "symfony/css-selector": "7.3.*", - "symfony/dom-crawler": "7.3.*", - "symfony/dotenv": "7.3.*", - "symfony/expression-language": "7.3.*", + "symfony/asset": "7.4.*", + "symfony/console": "7.4.*", + "symfony/css-selector": "7.4.*", + "symfony/dom-crawler": "7.4.*", + "symfony/dotenv": "7.4.*", + "symfony/expression-language": "7.4.*", "symfony/flex": "^v2.3.1", - "symfony/form": "7.3.*", - "symfony/framework-bundle": "7.3.*", - "symfony/http-client": "7.3.*", - "symfony/http-kernel": "7.3.*", - "symfony/mailer": "7.3.*", - "symfony/monolog-bundle": "^3.1", - "symfony/polyfill-php82": "^1.28", - "symfony/process": "7.3.*", - "symfony/property-access": "7.3.*", - "symfony/property-info": "7.3.*", - "symfony/rate-limiter": "7.3.*", - "symfony/runtime": "7.3.*", - "symfony/security-bundle": "7.3.*", - "symfony/serializer": "7.3.*", - "symfony/string": "7.3.*", - "symfony/translation": "7.3.*", - "symfony/twig-bundle": "7.3.*", - "symfony/ux-translator": "^2.10", + "symfony/form": "7.4.*", + "symfony/framework-bundle": "7.4.*", + "symfony/http-client": "7.4.*", + "symfony/http-kernel": "7.4.*", + "symfony/mailer": "7.4.*", + "symfony/monolog-bundle": "^4.0", + "symfony/process": "7.4.*", + "symfony/property-access": "7.4.*", + "symfony/property-info": "7.4.*", + "symfony/rate-limiter": "7.4.*", + "symfony/runtime": "7.4.*", + "symfony/security-bundle": "7.4.*", + "symfony/serializer": "7.4.*", + "symfony/string": "7.4.*", + "symfony/translation": "7.4.*", + "symfony/twig-bundle": "7.4.*", + "symfony/type-info": "7.4.*", + "symfony/ux-translator": "^2.32.0", "symfony/ux-turbo": "^2.0", - "symfony/validator": "7.3.*", - "symfony/web-link": "7.3.*", + "symfony/validator": "7.4.*", + "symfony/web-link": "7.4.*", "symfony/webpack-encore-bundle": "^v2.0.1", - "symfony/yaml": "7.3.*", - "symplify/easy-coding-standard": "^12.5.20", + "symfony/yaml": "7.4.*", + "symplify/easy-coding-standard": "^13.0", "tecnickcom/tc-lib-barcode": "^2.1.4", + "tiendanube/gtinvalidation": "^1.0", "twig/cssinliner-extra": "^3.0", "twig/extra-bundle": "^3.8", "twig/html-extra": "^3.8", @@ -111,16 +116,23 @@ "phpunit/phpunit": "^11.5.0", "rector/rector": "^2.0.4", "roave/security-advisories": "dev-latest", - "symfony/browser-kit": "7.3.*", - "symfony/debug-bundle": "7.3.*", + "symfony/browser-kit": "7.4.*", + "symfony/debug-bundle": "7.4.*", "symfony/maker-bundle": "^1.13", - "symfony/phpunit-bridge": "7.3.*", - "symfony/stopwatch": "7.3.*", - "symfony/web-profiler-bundle": "7.3.*" + "symfony/phpunit-bridge": "7.4.*", + "symfony/stopwatch": "7.4.*", + "symfony/web-profiler-bundle": "7.4.*" + }, + "replace": { + "symfony/polyfill-mbstring": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-php82": "*" }, "suggest": { "ext-bcmath": "Used to improve price calculation performance", - "ext-gmp": "Used to improve price calculation performanice" + "ext-gmp": "Used to improve price calculation performance" }, "config": { "preferred-install": { @@ -167,8 +179,13 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "7.3.*", + "require": "7.4.*", "docker": true + }, + "phpstan/extension-installer": { + "ignore" : [ + "ekino/phpstan-banned-code" + ] } } } diff --git a/composer.lock b/composer.lock index 72e83e0f..34715b7b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3b5a603cc4c289262a2e58b0f37ee42e", + "content-hash": "d6bda397c505e1e6d540c814a2368fbb", "packages": [ { "name": "amphp/amp", @@ -318,16 +318,16 @@ }, { "name": "amphp/hpack", - "version": "v3.2.1", + "version": "v3.2.2", "source": { "type": "git", "url": "https://github.com/amphp/hpack.git", - "reference": "4f293064b15682a2b178b1367ddf0b8b5feb0239" + "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/hpack/zipball/4f293064b15682a2b178b1367ddf0b8b5feb0239", - "reference": "4f293064b15682a2b178b1367ddf0b8b5feb0239", + "url": "https://api.github.com/repos/amphp/hpack/zipball/291da27078e7e149a9bad4d08ff05bf7d81c89f4", + "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4", "shasum": "" }, "require": { @@ -336,7 +336,7 @@ "require-dev": { "amphp/php-cs-fixer-config": "^2", "http2jp/hpack-test-case": "^1", - "nikic/php-fuzzer": "^0.0.10", + "nikic/php-fuzzer": "^0.0.11", "phpunit/phpunit": "^7 | ^8 | ^9" }, "type": "library", @@ -380,7 +380,7 @@ ], "support": { "issues": "https://github.com/amphp/hpack/issues", - "source": "https://github.com/amphp/hpack/tree/v3.2.1" + "source": "https://github.com/amphp/hpack/tree/v3.2.2" }, "funding": [ { @@ -388,7 +388,7 @@ "type": "github" } ], - "time": "2024-03-21T19:00:16+00:00" + "time": "2026-05-03T19:28:59+00:00" }, { "name": "amphp/http", @@ -456,16 +456,16 @@ }, { "name": "amphp/http-client", - "version": "v5.3.4", + "version": "v5.3.6", "source": { "type": "git", "url": "https://github.com/amphp/http-client.git", - "reference": "75ad21574fd632594a2dd914496647816d5106bc" + "reference": "ca155026acafa74a612d776a97202d53077fee86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/http-client/zipball/75ad21574fd632594a2dd914496647816d5106bc", - "reference": "75ad21574fd632594a2dd914496647816d5106bc", + "url": "https://api.github.com/repos/amphp/http-client/zipball/ca155026acafa74a612d776a97202d53077fee86", + "reference": "ca155026acafa74a612d776a97202d53077fee86", "shasum": "" }, "require": { @@ -493,9 +493,8 @@ "amphp/phpunit-util": "^3", "ext-json": "*", "kelunik/link-header-rfc5988": "^1", - "laminas/laminas-diactoros": "^2.3", "phpunit/phpunit": "^9", - "psalm/phar": "~5.23" + "psalm/phar": "6.16.1" }, "suggest": { "amphp/file": "Required for file request bodies and HTTP archive logging", @@ -542,7 +541,7 @@ ], "support": { "issues": "https://github.com/amphp/http-client/issues", - "source": "https://github.com/amphp/http-client/tree/v5.3.4" + "source": "https://github.com/amphp/http-client/tree/v5.3.6" }, "funding": [ { @@ -550,7 +549,7 @@ "type": "github" } ], - "time": "2025-08-16T20:41:23+00:00" + "time": "2026-05-15T23:29:38+00:00" }, { "name": "amphp/parser", @@ -616,16 +615,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.3", + "version": "v1.2.4", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "7b52598c2e9105ebcddf247fc523161581930367" + "reference": "a044733e080940d1483f56caff0c412ad6982776" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", - "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/a044733e080940d1483f56caff0c412ad6982776", + "reference": "a044733e080940d1483f56caff0c412ad6982776", "shasum": "" }, "require": { @@ -637,7 +636,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -671,7 +670,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + "source": "https://github.com/amphp/pipeline/tree/v1.2.4" }, "funding": [ { @@ -679,7 +678,7 @@ "type": "github" } ], - "time": "2025-03-16T16:33:53+00:00" + "time": "2026-05-06T05:37:57+00:00" }, { "name": "amphp/process", @@ -751,24 +750,27 @@ }, { "name": "amphp/serialization", - "version": "v1.0.0", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/amphp/serialization.git", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "phpunit/phpunit": "^9 || ^8 || ^7" + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -803,22 +805,28 @@ ], "support": { "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/master" + "source": "https://github.com/amphp/serialization/tree/v1.1.0" }, - "time": "2020-03-25T21:39:07+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-04-05T15:59:53+00:00" }, { "name": "amphp/socket", - "version": "v2.3.1", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/amphp/socket.git", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "url": "https://api.github.com/repos/amphp/socket/zipball/dadb63c5d3179fd83803e29dfeac27350e619314", + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314", "shasum": "" }, "require": { @@ -827,17 +835,17 @@ "amphp/dns": "^2", "ext-openssl": "*", "kelunik/certificate": "^1.1", - "league/uri": "^6.5 | ^7", - "league/uri-interfaces": "^2.3 | ^7", + "league/uri": "^7", + "league/uri-interfaces": "^7", "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" + "revolt/event-loop": "^1" }, "require-dev": { "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "amphp/process": "^2", "phpunit/phpunit": "^9", - "psalm/phar": "5.20" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -881,7 +889,7 @@ ], "support": { "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.3.1" + "source": "https://github.com/amphp/socket/tree/v2.4.0" }, "funding": [ { @@ -889,7 +897,7 @@ "type": "github" } ], - "time": "2024-04-21T14:33:03+00:00" + "time": "2026-04-19T15:09:56+00:00" }, { "name": "amphp/sync", @@ -968,22 +976,22 @@ }, { "name": "api-platform/doctrine-common", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", - "reference": "8acbed7c2768f7c15a5b030018132e454f895e55" + "reference": "089b196c2f8e4d14333aaa3c6db33356e8fd8be0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/8acbed7c2768f7c15a5b030018132e454f895e55", - "reference": "8acbed7c2768f7c15a5b030018132e454f895e55", + "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/089b196c2f8e4d14333aaa3c6db33356e8fd8be0", + "reference": "089b196c2f8e4d14333aaa3c6db33356e8fd8be0", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1.11", - "api-platform/state": "^4.1.11", - "doctrine/collections": "^2.1", + "api-platform/metadata": "^4.2.6", + "api-platform/state": "^4.2.4", + "doctrine/collections": "^2.1 || ^3.0", "doctrine/common": "^3.2.2", "doctrine/persistence": "^3.2 || ^4.0", "php": ">=8.2" @@ -995,8 +1003,8 @@ "doctrine/mongodb-odm": "^2.10", "doctrine/orm": "^2.17 || ^3.0", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev", - "symfony/type-info": "^7.3" + "phpunit/phpunit": "^11.5 || ^12.2", + "symfony/type-info": "^7.3 || ^8.0" }, "suggest": { "api-platform/graphql": "For GraphQl mercure subscriptions.", @@ -1013,13 +1021,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1052,46 +1060,48 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.2.2" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.3.6" }, - "time": "2025-08-27T12:34:14+00:00" + "time": "2026-05-04T13:25:58+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "d35d97423f7b399117ee033ecc886b3ed9dc2e23" + "reference": "095a4c56cdd9986208100dedd5d28be50a4830ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/d35d97423f7b399117ee033ecc886b3ed9dc2e23", - "reference": "d35d97423f7b399117ee033ecc886b3ed9dc2e23", + "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/095a4c56cdd9986208100dedd5d28be50a4830ba", + "reference": "095a4c56cdd9986208100dedd5d28be50a4830ba", "shasum": "" }, "require": { - "api-platform/doctrine-common": "^4.2.0-alpha.3@alpha", - "api-platform/metadata": "^4.1.11", - "api-platform/state": "^4.1.11", - "doctrine/orm": "^2.17 || ^3.0", - "php": ">=8.2", - "symfony/type-info": "^7.3" + "api-platform/doctrine-common": "^4.2.23", + "api-platform/metadata": "^4.2", + "api-platform/serializer": "^4.2.16", + "api-platform/state": "^4.2.4", + "composer/semver": "^3.4", + "doctrine/orm": "^2.17 || ^3.0.1", + "php": ">=8.2" }, "require-dev": { - "doctrine/doctrine-bundle": "^2.11", + "doctrine/doctrine-bundle": "^2.11 || ^3.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev", + "phpunit/phpunit": "^11.5 || ^12.2", "ramsey/uuid": "^4.7", "ramsey/uuid-doctrine": "^2.0", - "symfony/cache": "^6.4 || ^7.0", - "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/property-access": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.0", - "symfony/uid": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0" + "symfony/cache": "^6.4 || ^7.0 || ^8.0", + "symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0", + "symfony/property-access": "^6.4 || ^7.0 || ^8.0", + "symfony/property-info": "^6.4 || ^7.1 || ^8.0", + "symfony/serializer": "^6.4 || ^7.0 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0", + "symfony/uid": "^6.4 || ^7.0 || ^8.0", + "symfony/validator": "^6.4.11 || ^7.0 || ^8.0", + "symfony/yaml": "^6.4 || ^7.0 || ^8.0" }, "type": "library", "extra": { @@ -1100,13 +1110,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1139,30 +1149,30 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.2.2" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.3.6" }, - "time": "2025-10-07T13:54:25+00:00" + "time": "2026-05-07T11:45:31+00:00" }, { "name": "api-platform/documentation", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", - "reference": "c5a54336d8c51271aa5d54e57147cdee7162ab3a" + "reference": "f07b444aef1f75bb07beb9f8d799213f05070e5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/documentation/zipball/c5a54336d8c51271aa5d54e57147cdee7162ab3a", - "reference": "c5a54336d8c51271aa5d54e57147cdee7162ab3a", + "url": "https://api.github.com/repos/api-platform/documentation/zipball/f07b444aef1f75bb07beb9f8d799213f05070e5f", + "reference": "f07b444aef1f75bb07beb9f8d799213f05070e5f", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1.11", + "api-platform/metadata": "^4.3", "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "11.5.x-dev" + "phpunit/phpunit": "^11.5 || ^12.2" }, "type": "project", "extra": { @@ -1171,13 +1181,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1202,37 +1212,37 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.2.2" + "source": "https://github.com/api-platform/documentation/tree/v4.3.6" }, - "time": "2025-08-19T08:04:29+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", - "reference": "aef434b026b861ea451d814c86838b5470b8bfb4" + "reference": "dd7c092b9abee06e72fd58544fe714b6c2a61efa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/http-cache/zipball/aef434b026b861ea451d814c86838b5470b8bfb4", - "reference": "aef434b026b861ea451d814c86838b5470b8bfb4", + "url": "https://api.github.com/repos/api-platform/http-cache/zipball/dd7c092b9abee06e72fd58544fe714b6c2a61efa", + "reference": "dd7c092b9abee06e72fd58544fe714b6c2a61efa", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1.11", - "api-platform/state": "^4.1.11", + "api-platform/metadata": "^4.3", + "api-platform/state": "^4.3", "php": ">=8.2", - "symfony/http-foundation": "^6.4 || ^7.0" + "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0" }, "require-dev": { - "guzzlehttp/guzzle": "^6.0 || ^7.0", + "guzzlehttp/guzzle": "^6.0 || ^7.0 || ^8.0", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev", - "symfony/dependency-injection": "^6.4 || ^7.0", - "symfony/http-client": "^6.4 || ^7.0", - "symfony/type-info": "^7.3" + "phpunit/phpunit": "^11.5 || ^12.2", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", + "symfony/http-client": "^6.4 || ^7.0 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0" }, "type": "library", "extra": { @@ -1241,13 +1251,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1282,42 +1292,42 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.2.2" + "source": "https://github.com/api-platform/http-cache/tree/v4.3.6" }, - "time": "2025-09-16T12:51:08+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/hydra", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", - "reference": "bfbe928e6a3999433ef11afc267e591152b17cc3" + "reference": "317a696e396b80ba87de2560679c362923ef0a14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/hydra/zipball/bfbe928e6a3999433ef11afc267e591152b17cc3", - "reference": "bfbe928e6a3999433ef11afc267e591152b17cc3", + "url": "https://api.github.com/repos/api-platform/hydra/zipball/317a696e396b80ba87de2560679c362923ef0a14", + "reference": "317a696e396b80ba87de2560679c362923ef0a14", "shasum": "" }, "require": { - "api-platform/documentation": "^4.1", - "api-platform/json-schema": "^4.2@beta", - "api-platform/jsonld": "^4.1", - "api-platform/metadata": "^4.2@beta", - "api-platform/serializer": "^4.1", - "api-platform/state": "^4.1.8", + "api-platform/documentation": "^4.3", + "api-platform/json-schema": "^4.3", + "api-platform/jsonld": "^4.3", + "api-platform/metadata": "^4.3", + "api-platform/serializer": "^4.3", + "api-platform/state": "^4.3", "php": ">=8.2", - "symfony/type-info": "^7.3", - "symfony/web-link": "^6.4 || ^7.1" + "symfony/type-info": "^7.3 || ^8.0", + "symfony/web-link": "^6.4 || ^7.1 || ^8.0" }, "require-dev": { - "api-platform/doctrine-common": "^4.1", - "api-platform/doctrine-odm": "^4.1", - "api-platform/doctrine-orm": "^4.1", + "api-platform/doctrine-common": "^4.3", + "api-platform/doctrine-odm": "^4.3", + "api-platform/doctrine-orm": "^4.3", "phpspec/prophecy": "^1.19", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev" + "phpunit/phpunit": "^11.5 || ^12.2" }, "type": "library", "extra": { @@ -1326,13 +1336,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1369,40 +1379,40 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.2.2" + "source": "https://github.com/api-platform/hydra/tree/v4.3.6" }, - "time": "2025-10-07T13:39:38+00:00" + "time": "2026-05-11T11:50:19+00:00" }, { "name": "api-platform/json-api", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/json-api.git", - "reference": "e8da698d55fb1702b25c63d7c821d1760159912e" + "reference": "3a562e7f1bb1bc802e58eff674a20b78fe107275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-api/zipball/e8da698d55fb1702b25c63d7c821d1760159912e", - "reference": "e8da698d55fb1702b25c63d7c821d1760159912e", + "url": "https://api.github.com/repos/api-platform/json-api/zipball/3a562e7f1bb1bc802e58eff674a20b78fe107275", + "reference": "3a562e7f1bb1bc802e58eff674a20b78fe107275", "shasum": "" }, "require": { - "api-platform/documentation": "^4.1.11", - "api-platform/json-schema": "^4.2@beta", - "api-platform/metadata": "^4.2@beta", - "api-platform/serializer": "^4.1.11", - "api-platform/state": "^4.1.11", + "api-platform/documentation": "^4.3", + "api-platform/json-schema": "^4.3", + "api-platform/metadata": "^4.3", + "api-platform/serializer": "^4.3", + "api-platform/state": "^4.3", "php": ">=8.2", - "symfony/error-handler": "^6.4 || ^7.0", - "symfony/http-foundation": "^6.4 || ^7.0", - "symfony/type-info": "^7.3" + "symfony/error-handler": "^6.4 || ^7.0 || ^8.0", + "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0" }, "require-dev": { "phpspec/prophecy": "^1.19", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev", - "symfony/type-info": "^7.3" + "phpunit/phpunit": "^11.5 || ^12.2", + "symfony/type-info": "^7.3 || ^8.0" }, "type": "library", "extra": { @@ -1411,13 +1421,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1451,36 +1461,36 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/json-api/tree/v4.2.2" + "source": "https://github.com/api-platform/json-api/tree/v4.3.6" }, - "time": "2025-09-16T12:49:22+00:00" + "time": "2026-05-22T11:06:32+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", - "reference": "ec81bdd09375067d7d2555c10ec3dfc697874327" + "reference": "23dc2c388a08f2006b9189a0883a08f8837d7249" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-schema/zipball/ec81bdd09375067d7d2555c10ec3dfc697874327", - "reference": "ec81bdd09375067d7d2555c10ec3dfc697874327", + "url": "https://api.github.com/repos/api-platform/json-schema/zipball/23dc2c388a08f2006b9189a0883a08f8837d7249", + "reference": "23dc2c388a08f2006b9189a0883a08f8837d7249", "shasum": "" }, "require": { - "api-platform/metadata": "^4.2", + "api-platform/metadata": "^4.3", "php": ">=8.2", - "symfony/console": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.0", - "symfony/type-info": "^7.3", - "symfony/uid": "^6.4 || ^7.0" + "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/property-info": "^6.4 || ^7.1 || ^8.0", + "symfony/serializer": "^6.4 || ^7.0 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0", + "symfony/uid": "^6.4 || ^7.0 || ^8.0" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev" + "phpunit/phpunit": "^11.5 || ^12.2" }, "type": "library", "extra": { @@ -1489,13 +1499,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1532,33 +1542,33 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/json-schema/tree/v4.2.2" + "source": "https://github.com/api-platform/json-schema/tree/v4.3.6" }, - "time": "2025-10-07T09:45:59+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/jsonld", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", - "reference": "774b460273177983c52540a479ea9e9f940d7a1b" + "reference": "20ca6d7b5c11674c3046d710aaa0c9bc1795e54b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/jsonld/zipball/774b460273177983c52540a479ea9e9f940d7a1b", - "reference": "774b460273177983c52540a479ea9e9f940d7a1b", + "url": "https://api.github.com/repos/api-platform/jsonld/zipball/20ca6d7b5c11674c3046d710aaa0c9bc1795e54b", + "reference": "20ca6d7b5c11674c3046d710aaa0c9bc1795e54b", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1.11", - "api-platform/serializer": "^4.1.11", - "api-platform/state": "^4.1.11", + "api-platform/metadata": "^4.3", + "api-platform/serializer": "^4.3", + "api-platform/state": "^4.3", "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "11.5.x-dev", - "symfony/type-info": "^7.3" + "phpunit/phpunit": "^11.5 || ^12.2", + "symfony/type-info": "^7.3 || ^8.0" }, "type": "library", "extra": { @@ -1567,13 +1577,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1612,22 +1622,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.2.2" + "source": "https://github.com/api-platform/jsonld/tree/v4.3.6" }, - "time": "2025-09-25T19:30:56+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/metadata", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", - "reference": "5aeba910cb6352068664ca11cd9ee0dc208894c0" + "reference": "e9e8a7b85d2d513edff3b108072f8ab23a9d6344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/5aeba910cb6352068664ca11cd9ee0dc208894c0", - "reference": "5aeba910cb6352068664ca11cd9ee0dc208894c0", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/e9e8a7b85d2d513edff3b108072f8ab23a9d6344", + "reference": "e9e8a7b85d2d513edff3b108072f8ab23a9d6344", "shasum": "" }, "require": { @@ -1635,22 +1645,22 @@ "php": ">=8.2", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/string": "^6.4 || ^7.0", - "symfony/type-info": "^7.3" + "symfony/property-info": "^6.4 || ^7.1 || ^8.0", + "symfony/string": "^6.4 || ^7.0 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0" }, "require-dev": { - "api-platform/json-schema": "^4.1.11", - "api-platform/openapi": "^4.1.11", - "api-platform/state": "^4.1.11", + "api-platform/json-schema": "^4.3", + "api-platform/openapi": "^4.3", + "api-platform/state": "^4.3", "phpspec/prophecy-phpunit": "^2.2", "phpstan/phpdoc-parser": "^1.29 || ^2.0", - "phpunit/phpunit": "11.5.x-dev", - "symfony/config": "^6.4 || ^7.0", - "symfony/routing": "^6.4 || ^7.0", - "symfony/var-dumper": "^6.4 || ^7.0", - "symfony/web-link": "^6.4 || ^7.1", - "symfony/yaml": "^6.4 || ^7.0" + "phpunit/phpunit": "^11.5 || ^12.2", + "symfony/config": "^6.4 || ^7.0 || ^8.0", + "symfony/routing": "^6.4 || ^7.0 || ^8.0", + "symfony/var-dumper": "^6.4 || ^7.0 || ^8.0", + "symfony/web-link": "^6.4 || ^7.1 || ^8.0", + "symfony/yaml": "^6.4 || ^7.0 || ^8.0" }, "suggest": { "phpstan/phpdoc-parser": "For PHP documentation support.", @@ -1664,13 +1674,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1710,42 +1720,43 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.2.2" + "source": "https://github.com/api-platform/metadata/tree/v4.3.6" }, - "time": "2025-10-08T08:36:37+00:00" + "time": "2026-05-22T12:00:17+00:00" }, { "name": "api-platform/openapi", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", - "reference": "2545f2be06eed0f9a121d631b8f1db22212a7826" + "reference": "1562617e7500a50c2b6e6f43a0fb29a6a47e83a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/2545f2be06eed0f9a121d631b8f1db22212a7826", - "reference": "2545f2be06eed0f9a121d631b8f1db22212a7826", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/1562617e7500a50c2b6e6f43a0fb29a6a47e83a2", + "reference": "1562617e7500a50c2b6e6f43a0fb29a6a47e83a2", "shasum": "" }, "require": { - "api-platform/json-schema": "^4.2@beta", - "api-platform/metadata": "^4.2@beta", - "api-platform/state": "^4.2@beta", + "api-platform/json-schema": "^4.3", + "api-platform/metadata": "^4.3", + "api-platform/state": "^4.3", "php": ">=8.2", - "symfony/console": "^6.4 || ^7.0", - "symfony/filesystem": "^6.4 || ^7.0", - "symfony/property-access": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", - "symfony/type-info": "^7.3" + "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/filesystem": "^6.4 || ^7.0 || ^8.0", + "symfony/property-access": "^6.4 || ^7.0 || ^8.0", + "symfony/serializer": "^6.4 || ^7.0 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0" }, "require-dev": { - "api-platform/doctrine-common": "^4.1", - "api-platform/doctrine-odm": "^4.1", - "api-platform/doctrine-orm": "^4.1", + "api-platform/doctrine-common": "^4.3", + "api-platform/doctrine-odm": "^4.3", + "api-platform/doctrine-orm": "^4.3", + "api-platform/serializer": "^4.3", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev", - "symfony/type-info": "^7.3" + "phpunit/phpunit": "^11.5 || ^12.2", + "symfony/type-info": "^7.3 || ^8.0" }, "type": "library", "extra": { @@ -1754,13 +1765,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1800,46 +1811,47 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.2.2" + "source": "https://github.com/api-platform/openapi/tree/v4.3.6" }, - "time": "2025-09-30T12:06:50+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/serializer", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", - "reference": "58c1378af6429049ee62951b838f6b645706c3a3" + "reference": "2c4f996bb6e5fef49106df0c48d0c1954e10998b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/serializer/zipball/58c1378af6429049ee62951b838f6b645706c3a3", - "reference": "58c1378af6429049ee62951b838f6b645706c3a3", + "url": "https://api.github.com/repos/api-platform/serializer/zipball/2c4f996bb6e5fef49106df0c48d0c1954e10998b", + "reference": "2c4f996bb6e5fef49106df0c48d0c1954e10998b", "shasum": "" }, "require": { - "api-platform/metadata": "^4.2.0", - "api-platform/state": "^4.1.11", + "api-platform/metadata": "^4.3", + "api-platform/state": "^4.3", "php": ">=8.2", - "symfony/property-access": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0" + "symfony/property-access": "^6.4 || ^7.0 || ^8.0", + "symfony/property-info": "^6.4 || ^7.1 || ^8.0", + "symfony/serializer": "^6.4.37 || ^7.4.9 || ^8.0.9", + "symfony/validator": "^6.4.11 || ^7.0 || ^8.0" }, "require-dev": { - "api-platform/doctrine-common": "^4.1", - "api-platform/doctrine-odm": "^4.1", - "api-platform/doctrine-orm": "^4.1", - "api-platform/json-schema": "^4.1", - "api-platform/openapi": "^4.1", + "api-platform/doctrine-common": "^4.3", + "api-platform/doctrine-odm": "^4.3", + "api-platform/doctrine-orm": "^4.3", + "api-platform/json-schema": "^4.3", + "api-platform/openapi": "^4.3", "doctrine/collections": "^2.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev", + "phpunit/phpunit": "^11.5 || ^12.2", + "sebastian/exporter": "^6.3.2 || ^7.0.2", "symfony/mercure-bundle": "*", - "symfony/type-info": "^7.3", - "symfony/var-dumper": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0" + "symfony/type-info": "^7.3 || ^8.0", + "symfony/var-dumper": "^6.4 || ^7.0 || ^8.0", + "symfony/yaml": "^6.4 || ^7.0 || ^8.0" }, "suggest": { "api-platform/doctrine-odm": "To support Doctrine MongoDB ODM state options.", @@ -1852,13 +1864,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1893,40 +1905,41 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.2.2" + "source": "https://github.com/api-platform/serializer/tree/v4.3.6" }, - "time": "2025-10-03T08:13:34+00:00" + "time": "2026-05-12T10:07:44+00:00" }, { "name": "api-platform/state", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", - "reference": "7dc3dfbafa4627cc789bd8bcd4da46e6c37f6b26" + "reference": "6e3f6d75e605ba7171a7590c82da5126979a936b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/state/zipball/7dc3dfbafa4627cc789bd8bcd4da46e6c37f6b26", - "reference": "7dc3dfbafa4627cc789bd8bcd4da46e6c37f6b26", + "url": "https://api.github.com/repos/api-platform/state/zipball/6e3f6d75e605ba7171a7590c82da5126979a936b", + "reference": "6e3f6d75e605ba7171a7590c82da5126979a936b", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1.18", + "api-platform/metadata": "^4.3", "php": ">=8.2", "psr/container": "^1.0 || ^2.0", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", + "symfony/deprecation-contracts": "^3.1", + "symfony/http-kernel": "^6.4.13 || ^7.0 || ^8.0", + "symfony/serializer": "^6.4 || ^7.0 || ^8.0", "symfony/translation-contracts": "^3.0" }, "require-dev": { - "api-platform/serializer": "^4.1", - "api-platform/validator": "^4.1", - "phpunit/phpunit": "11.5.x-dev", - "symfony/http-foundation": "^6.4 || ^7.0", - "symfony/object-mapper": "^7.3", - "symfony/type-info": "^7.3", - "symfony/web-link": "^6.4 || ^7.1", + "api-platform/serializer": "^4.3", + "api-platform/validator": "^4.3.1", + "phpunit/phpunit": "^11.5 || ^12.2", + "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0", + "symfony/object-mapper": "^7.4 || ^8.0", + "symfony/type-info": "^7.4 || ^8.0", + "symfony/web-link": "^6.4 || ^7.1 || ^8.0", "willdurand/negotiation": "^3.1" }, "suggest": { @@ -1943,13 +1956,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -1989,59 +2002,61 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.2.2" + "source": "https://github.com/api-platform/state/tree/v4.3.6" }, - "time": "2025-10-09T08:33:56+00:00" + "time": "2026-05-22T12:02:28+00:00" }, { "name": "api-platform/symfony", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "44bb117df1cd5695203ec6a97999cabc85257780" + "reference": "13308ad99dd1479e70fe79c20519d8135df8e7b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/44bb117df1cd5695203ec6a97999cabc85257780", - "reference": "44bb117df1cd5695203ec6a97999cabc85257780", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/13308ad99dd1479e70fe79c20519d8135df8e7b9", + "reference": "13308ad99dd1479e70fe79c20519d8135df8e7b9", "shasum": "" }, "require": { - "api-platform/documentation": "^4.1.11", - "api-platform/http-cache": "^4.1.11", - "api-platform/hydra": "^4.1.11", - "api-platform/json-schema": "^4.1.11", - "api-platform/jsonld": "^4.1.11", - "api-platform/metadata": "^4.2@beta", - "api-platform/openapi": "^4.1.11", - "api-platform/serializer": "^4.1.11", - "api-platform/state": "^4.2@beta", - "api-platform/validator": "^4.1.11", + "api-platform/documentation": "^4.3", + "api-platform/http-cache": "^4.3", + "api-platform/hydra": "^4.3", + "api-platform/json-schema": "^4.3", + "api-platform/jsonld": "^4.3", + "api-platform/metadata": "^4.3", + "api-platform/openapi": "^4.3", + "api-platform/serializer": "^4.3", + "api-platform/state": "^4.3", + "api-platform/validator": "^4.3.1", "php": ">=8.2", - "symfony/finder": "^6.4 || ^7.0", - "symfony/property-access": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/security-core": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", + "symfony/asset": "^6.4 || ^7.0 || ^8.0", + "symfony/finder": "^6.4 || ^7.0 || ^8.0", + "symfony/http-kernel": "^6.4.13 || ^7.0 || ^8.0", + "symfony/property-access": "^6.4 || ^7.0 || ^8.0", + "symfony/property-info": "^6.4 || ^7.0 || ^8.0", + "symfony/security-core": "^6.4 || ^7.0 || ^8.0", + "symfony/serializer": "^6.4 || ^7.0 || ^8.0", "willdurand/negotiation": "^3.1" }, "require-dev": { - "api-platform/doctrine-common": "^4.1", - "api-platform/doctrine-odm": "^4.1", - "api-platform/doctrine-orm": "^4.1", - "api-platform/elasticsearch": "^4.1", - "api-platform/graphql": "^4.1", - "api-platform/parameter-validator": "^3.1", + "api-platform/doctrine-common": "^4.3", + "api-platform/doctrine-odm": "^4.3", + "api-platform/doctrine-orm": "^4.3", + "api-platform/elasticsearch": "^4.3", + "api-platform/graphql": "^4.3", + "api-platform/hal": "^4.3", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev", - "symfony/expression-language": "^6.4 || ^7.0", - "symfony/intl": "^6.4 || ^7.0", + "phpunit/phpunit": "^11.5 || ^12.2", + "symfony/expression-language": "^6.4 || ^7.0 || ^8.0", + "symfony/intl": "^6.4 || ^7.0 || ^8.0", "symfony/mercure-bundle": "*", - "symfony/object-mapper": "^7.0", - "symfony/routing": "^6.4 || ^7.0", - "symfony/type-info": "^7.3", - "symfony/validator": "^6.4 || ^7.0", + "symfony/object-mapper": "^7.0 || ^8.0", + "symfony/routing": "^6.4 || ^7.0 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0", + "symfony/validator": "^6.4.11 || ^7.0 || ^8.0", "webonyx/graphql-php": "^15.0" }, "suggest": { @@ -2050,6 +2065,7 @@ "api-platform/elasticsearch": "To support Elasticsearch.", "api-platform/graphql": "To support GraphQL.", "api-platform/hal": "to support the HAL format", + "api-platform/json-api": "to support the JSON-API format", "api-platform/ramsey-uuid": "To support Ramsey's UUID identifiers.", "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", "psr/cache-implementation": "To use metadata caching.", @@ -2071,13 +2087,10 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { - "dev-3.4": "3.4.x-dev", - "dev-4.1": "4.1.x-dev", - "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -2118,36 +2131,36 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.2.2" + "source": "https://github.com/api-platform/symfony/tree/v4.3.6" }, - "time": "2025-10-09T08:33:56+00:00" + "time": "2026-05-18T09:34:32+00:00" }, { "name": "api-platform/validator", - "version": "v4.2.2", + "version": "v4.3.6", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", - "reference": "9986e84b27e3de7f87c7c23f238430a572f87f67" + "reference": "6df6804799f8831469d2602d0845a0316e81fbab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/validator/zipball/9986e84b27e3de7f87c7c23f238430a572f87f67", - "reference": "9986e84b27e3de7f87c7c23f238430a572f87f67", + "url": "https://api.github.com/repos/api-platform/validator/zipball/6df6804799f8831469d2602d0845a0316e81fbab", + "reference": "6df6804799f8831469d2602d0845a0316e81fbab", "shasum": "" }, "require": { - "api-platform/metadata": "^4.1.11", + "api-platform/metadata": "^4.3", "php": ">=8.2", - "symfony/http-kernel": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", - "symfony/type-info": "^7.3", - "symfony/validator": "^6.4 || ^7.1", - "symfony/web-link": "^6.4 || ^7.1" + "symfony/http-kernel": "^6.4.13 || ^7.1 || ^8.0", + "symfony/serializer": "^6.4 || ^7.1 || ^8.0", + "symfony/type-info": "^7.3 || ^8.0", + "symfony/validator": "^6.4.11 || ^7.1 || ^8.0", + "symfony/web-link": "^6.4 || ^7.1 || ^8.0" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "11.5.x-dev" + "phpunit/phpunit": "^11.5 || ^12.2" }, "type": "library", "extra": { @@ -2156,13 +2169,13 @@ "name": "api-platform/api-platform" }, "symfony": { - "require": "^6.4 || ^7.0" + "require": "^6.4 || ^7.0 || ^8.0" }, "branch-alias": { "dev-3.4": "3.4.x-dev", "dev-4.1": "4.1.x-dev", "dev-4.2": "4.2.x-dev", - "dev-main": "4.3.x-dev" + "dev-main": "4.4.x-dev" } }, "autoload": { @@ -2194,9 +2207,9 @@ "validator" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v4.2.2" + "source": "https://github.com/api-platform/validator/tree/v4.3.6" }, - "time": "2025-09-29T15:36:04+00:00" + "time": "2026-05-07T11:45:31+00:00" }, { "name": "beberlei/assert", @@ -2329,25 +2342,24 @@ }, { "name": "brick/math", - "version": "0.13.1", + "version": "0.17.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" + "reference": "6aef71a9fbbd1ee7be0e313cd627f8e6f7125a5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", + "url": "https://api.github.com/repos/brick/math/zipball/6aef71a9fbbd1ee7be0e313cd627f8e6f7125a5b", + "reference": "6aef71a9fbbd1ee7be0e313cd627f8e6f7125a5b", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.1", - "vimeo/psalm": "6.8.8" + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" }, "type": "library", "autoload": { @@ -2377,7 +2389,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.13.1" + "source": "https://github.com/brick/math/tree/0.17.1" }, "funding": [ { @@ -2385,20 +2397,131 @@ "type": "github" } ], - "time": "2025-03-29T13:50:30+00:00" + "time": "2026-04-19T20:55:20+00:00" }, { - "name": "composer/ca-bundle", - "version": "1.5.8", + "name": "brick/schema", + "version": "0.2.0", "source": { "type": "git", - "url": "https://github.com/composer/ca-bundle.git", - "reference": "719026bb30813accb68271fee7e39552a58e9f65" + "url": "https://github.com/brick/schema.git", + "reference": "b5114bf5e8092430041a37efe1cfd5279ca764c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/719026bb30813accb68271fee7e39552a58e9f65", - "reference": "719026bb30813accb68271fee7e39552a58e9f65", + "url": "https://api.github.com/repos/brick/schema/zipball/b5114bf5e8092430041a37efe1cfd5279ca764c0", + "reference": "b5114bf5e8092430041a37efe1cfd5279ca764c0", + "shasum": "" + }, + "require": { + "brick/structured-data": "~0.1.0 || ~0.2.0", + "ext-dom": "*", + "php": "^8.1" + }, + "require-dev": { + "brick/varexporter": "^0.6", + "vimeo/psalm": "6.12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Schema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Schema.org library for PHP", + "keywords": [ + "JSON-LD", + "brick", + "microdata", + "rdfa lite", + "schema", + "schema.org", + "structured data" + ], + "support": { + "issues": "https://github.com/brick/schema/issues", + "source": "https://github.com/brick/schema/tree/0.2.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-06-12T07:03:20+00:00" + }, + { + "name": "brick/structured-data", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/brick/structured-data.git", + "reference": "be9b28720e2aba87f19c90500700970be85affde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/structured-data/zipball/be9b28720e2aba87f19c90500700970be85affde", + "reference": "be9b28720e2aba87f19c90500700970be85affde", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "php": "^8.1", + "sabre/uri": "^2.1 || ^3.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^8.0 || ^9.0", + "vimeo/psalm": "6.12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\StructuredData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Microdata, RDFa Lite & JSON-LD structured data reader", + "keywords": [ + "JSON-LD", + "brick", + "microdata", + "rdfa", + "structured data" + ], + "support": { + "issues": "https://github.com/brick/structured-data/issues", + "source": "https://github.com/brick/structured-data/tree/0.2.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-06-10T23:48:46+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.5.12", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", + "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", "shasum": "" }, "require": { @@ -2445,7 +2568,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.8" + "source": "https://github.com/composer/ca-bundle/tree/1.5.12" }, "funding": [ { @@ -2457,7 +2580,7 @@ "type": "github" } ], - "time": "2025-08-20T18:49:47+00:00" + "time": "2026-05-19T11:26:22+00:00" }, { "name": "composer/package-versions-deprecated", @@ -2611,6 +2734,83 @@ ], "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": "daverandom/libdns", "version": "v2.1.0", @@ -2732,16 +2932,16 @@ }, { "name": "doctrine/collections", - "version": "2.3.0", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", - "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", + "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2", "shasum": "" }, "require": { @@ -2750,11 +2950,11 @@ "symfony/polyfill-php84": "^1.30" }, "require-dev": { - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^14", "ext-json": "*", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.5" + "phpstan/phpstan": "^2.1.30", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" }, "type": "library", "autoload": { @@ -2798,7 +2998,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.3.0" + "source": "https://github.com/doctrine/collections/tree/2.6.0" }, "funding": [ { @@ -2814,7 +3014,7 @@ "type": "tidelift" } ], - "time": "2025-03-22T10:17:19+00:00" + "time": "2026-01-15T10:01:58+00:00" }, { "name": "doctrine/common", @@ -2909,16 +3109,16 @@ }, { "name": "doctrine/data-fixtures", - "version": "2.2.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/doctrine/data-fixtures.git", - "reference": "7a615ba135e45d67674bb623d90f34f6c7b6bd97" + "reference": "bf7ac3a050b54b261cedfb3d0a44733819062275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/7a615ba135e45d67674bb623d90f34f6c7b6bd97", - "reference": "7a615ba135e45d67674bb623d90f34f6c7b6bd97", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/bf7ac3a050b54b261cedfb3d0a44733819062275", + "reference": "bf7ac3a050b54b261cedfb3d0a44733819062275", "shasum": "" }, "require": { @@ -2936,12 +3136,14 @@ "doctrine/dbal": "^3.5 || ^4", "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", "doctrine/orm": "^2.14 || ^3", + "doctrine/phpcr-odm": "^1.8 || ^2.0", "ext-sqlite3": "*", "fig/log-test": "^1", - "phpstan/phpstan": "2.1.31", - "phpunit/phpunit": "10.5.45 || 12.4.0", - "symfony/cache": "^6.4 || ^7", - "symfony/var-exporter": "^6.4 || ^7" + "jackalope/jackalope-fs": "*", + "phpstan/phpstan": "2.1.46", + "phpunit/phpunit": "10.5.63 || 12.5.12", + "symfony/cache": "^6.4 || ^7 || ^8", + "symfony/var-exporter": "^6.4 || ^7 || ^8" }, "suggest": { "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", @@ -2972,7 +3174,7 @@ ], "support": { "issues": "https://github.com/doctrine/data-fixtures/issues", - "source": "https://github.com/doctrine/data-fixtures/tree/2.2.0" + "source": "https://github.com/doctrine/data-fixtures/tree/2.2.1" }, "funding": [ { @@ -2988,20 +3190,20 @@ "type": "tidelift" } ], - "time": "2025-10-17T20:06:20+00:00" + "time": "2026-04-01T13:56:01+00:00" }, { "name": "doctrine/dbal", - "version": "4.3.4", + "version": "4.4.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "1a2fbd0e93b8dec7c3d1ac2b6396a7b929b130dc" + "reference": "61e730f1658814821a85f2402c945f3883407dec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/1a2fbd0e93b8dec7c3d1ac2b6396a7b929b130dc", - "reference": "1a2fbd0e93b8dec7c3d1ac2b6396a7b929b130dc", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec", + "reference": "61e730f1658814821a85f2402c945f3883407dec", "shasum": "" }, "require": { @@ -3017,11 +3219,11 @@ "phpstan/phpstan": "2.1.30", "phpstan/phpstan-phpunit": "2.0.7", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "11.5.23", - "slevomat/coding-standard": "8.24.0", - "squizlabs/php_codesniffer": "4.0.0", - "symfony/cache": "^6.3.8|^7.0", - "symfony/console": "^5.4|^6.3|^7.0" + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -3078,7 +3280,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.3.4" + "source": "https://github.com/doctrine/dbal/tree/4.4.3" }, "funding": [ { @@ -3094,33 +3296,33 @@ "type": "tidelift" } ], - "time": "2025-10-09T09:11:36+00:00" + "time": "2026-03-20T08:52:12+00:00" }, { "name": "doctrine/deprecations", - "version": "1.1.5", + "version": "1.1.6", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=13" + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^13", - "phpstan/phpstan": "1.4.10 || 2.1.11", + "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", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -3140,22 +3342,22 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2025-04-07T20:06:18+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "doctrine/doctrine-bundle", - "version": "2.18.0", + "version": "2.18.2", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "cd5d4da6a5f7cf3d8708e17211234657b5eb4e95" + "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/cd5d4da6a5f7cf3d8708e17211234657b5eb4e95", - "reference": "cd5d4da6a5f7cf3d8708e17211234657b5eb4e95", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0ff098b29b8b3c68307c8987dcaed7fd829c6546", + "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546", "shasum": "" }, "require": { @@ -3247,7 +3449,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.0" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.2" }, "funding": [ { @@ -3263,20 +3465,20 @@ "type": "tidelift" } ], - "time": "2025-10-11T04:43:27+00:00" + "time": "2025-12-20T21:35:32+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.5.0", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "71c81279ca0e907c3edc718418b93fd63074856c" + "reference": "1e380c6dd8ac8488217f39cff6b77e367f1a644b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/71c81279ca0e907c3edc718418b93fd63074856c", - "reference": "71c81279ca0e907c3edc718418b93fd63074856c", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/1e380c6dd8ac8488217f39cff6b77e367f1a644b", + "reference": "1e380c6dd8ac8488217f39cff6b77e367f1a644b", "shasum": "" }, "require": { @@ -3284,7 +3486,7 @@ "doctrine/migrations": "^3.2", "php": "^7.2 || ^8.0", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "composer/semver": "^3.0", @@ -3296,8 +3498,8 @@ "phpstan/phpstan-strict-rules": "^1.1 || ^2", "phpstan/phpstan-symfony": "^1.3 || ^2", "phpunit/phpunit": "^8.5 || ^9.5", - "symfony/phpunit-bridge": "^6.3 || ^7", - "symfony/var-exporter": "^5.4 || ^6 || ^7" + "symfony/phpunit-bridge": "^6.3 || ^7 || ^8", + "symfony/var-exporter": "^5.4 || ^6 || ^7 || ^8" }, "type": "symfony-bundle", "autoload": { @@ -3332,7 +3534,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.5.0" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.7.0" }, "funding": [ { @@ -3348,20 +3550,20 @@ "type": "tidelift" } ], - "time": "2025-10-12T17:06:40+00:00" + "time": "2025-11-15T19:02:59+00:00" }, { "name": "doctrine/event-manager", - "version": "2.0.1", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", "shasum": "" }, "require": { @@ -3371,10 +3573,10 @@ "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -3423,7 +3625,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" }, "funding": [ { @@ -3439,7 +3641,7 @@ "type": "tidelift" } ], - "time": "2024-05-22T20:47:39+00:00" + "time": "2026-01-29T07:11:08+00:00" }, { "name": "doctrine/inflector", @@ -3680,16 +3882,16 @@ }, { "name": "doctrine/migrations", - "version": "3.9.4", + "version": "3.9.7", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "1b88fcb812f2cd6e77c83d16db60e3cf1e35c66c" + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/1b88fcb812f2cd6e77c83d16db60e3cf1e35c66c", - "reference": "1b88fcb812f2cd6e77c83d16db60e3cf1e35c66c", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", "shasum": "" }, "require": { @@ -3699,15 +3901,15 @@ "doctrine/event-manager": "^1.2 || ^2.0", "php": "^8.1", "psr/log": "^1.1.3 || ^2 || ^3", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", - "symfony/var-exporter": "^6.2 || ^7.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.2 || ^7.0 || ^8.0" }, "conflict": { "doctrine/orm": "<2.12 || >=4" }, "require-dev": { - "doctrine/coding-standard": "^13", + "doctrine/coding-standard": "^14", "doctrine/orm": "^2.13 || ^3", "doctrine/persistence": "^2 || ^3 || ^4", "doctrine/sql-formatter": "^1.0", @@ -3719,9 +3921,9 @@ "phpstan/phpstan-strict-rules": "^2", "phpstan/phpstan-symfony": "^2", "phpunit/phpunit": "^10.3 || ^11.0 || ^12.0", - "symfony/cache": "^5.4 || ^6.0 || ^7.0", - "symfony/process": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + "symfony/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "suggest": { "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", @@ -3763,7 +3965,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.9.4" + "source": "https://github.com/doctrine/migrations/tree/3.9.7" }, "funding": [ { @@ -3779,20 +3981,20 @@ "type": "tidelift" } ], - "time": "2025-08-19T06:41:07+00:00" + "time": "2026-04-23T19:33:20+00:00" }, { "name": "doctrine/orm", - "version": "3.5.2", + "version": "3.6.7", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "5a541b8b3a327ab1ea5f93b1615b4ff67a34e109" + "reference": "bc217c0e19c3a9eadfa67697143b87c9ba01272c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/5a541b8b3a327ab1ea5f93b1615b4ff67a34e109", - "reference": "5a541b8b3a327ab1ea5f93b1615b4ff67a34e109", + "url": "https://api.github.com/repos/doctrine/orm/zipball/bc217c0e19c3a9eadfa67697143b87c9ba01272c", + "reference": "bc217c0e19c3a9eadfa67697143b87c9ba01272c", "shasum": "" }, "require": { @@ -3808,20 +4010,18 @@ "ext-ctype": "*", "php": "^8.1", "psr/cache": "^1 || ^2 || ^3", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/var-exporter": "^6.3.9 || ^7.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.3.9 || ^7.0 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^13.0", + "doctrine/coding-standard": "^14.0", "phpbench/phpbench": "^1.0", - "phpdocumentor/guides-cli": "^1.4", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "2.0.3", + "phpstan/phpstan": "2.1.23", "phpstan/phpstan-deprecation-rules": "^2", - "phpunit/phpunit": "^10.4.0", + "phpunit/phpunit": "^10.5.0 || ^11.5", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.12.0", - "symfony/cache": "^5.4 || ^6.2 || ^7.0" + "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -3867,25 +4067,26 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.5.2" + "source": "https://github.com/doctrine/orm/tree/3.6.7" }, - "time": "2025-08-08T17:00:40+00:00" + "time": "2026-05-25T16:45:47+00:00" }, { "name": "doctrine/persistence", - "version": "4.1.1", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09" + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/b9c49ad3558bb77ef973f4e173f2e9c2eca9be09", - "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", "shasum": "" }, "require": { + "doctrine/deprecations": "^1", "doctrine/event-manager": "^1 || ^2", "php": "^8.1", "psr/cache": "^1.0 || ^2.0 || ^3.0" @@ -3896,13 +4097,13 @@ "phpstan/phpstan-phpunit": "^2", "phpstan/phpstan-strict-rules": "^2", "phpunit/phpunit": "^10.5.58 || ^12", - "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0", - "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0" + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Persistence\\": "src/Persistence" + "Doctrine\\Persistence\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3946,7 +4147,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/4.1.1" + "source": "https://github.com/doctrine/persistence/tree/4.2.0" }, "funding": [ { @@ -3962,30 +4163,30 @@ "type": "tidelift" } ], - "time": "2025-10-16T20:13:18+00:00" + "time": "2026-04-26T12:12:52+00:00" }, { "name": "doctrine/sql-formatter", - "version": "1.5.2", + "version": "1.5.4", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^12", - "ergebnis/phpunit-slow-test-detector": "^2.14", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5" + "doctrine/coding-standard": "^14", + "ergebnis/phpunit-slow-test-detector": "^2.20", + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^10.5.58" }, "bin": [ "bin/sql-formatter" @@ -4015,22 +4216,22 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.4" }, - "time": "2025-01-24T11:45:48+00:00" + "time": "2026-02-08T16:21:46+00:00" }, { "name": "dompdf/dompdf", - "version": "v3.1.3", + "version": "v3.1.5", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "baed300e4fb8226359c04395518059a136e2a2e2" + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/baed300e4fb8226359c04395518059a136e2a2e2", - "reference": "baed300e4fb8226359c04395518059a136e2a2e2", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", "shasum": "" }, "require": { @@ -4079,22 +4280,22 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v3.1.3" + "source": "https://github.com/dompdf/dompdf/tree/v3.1.5" }, - "time": "2025-10-14T13:10:17+00:00" + "time": "2026-03-03T13:54:37+00:00" }, { "name": "dompdf/php-font-lib", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/dompdf/php-font-lib.git", - "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", - "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a", "shasum": "" }, "require": { @@ -4102,7 +4303,7 @@ "php": "^7.1 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12" }, "type": "library", "autoload": { @@ -4124,31 +4325,31 @@ "homepage": "https://github.com/dompdf/php-font-lib", "support": { "issues": "https://github.com/dompdf/php-font-lib/issues", - "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1" + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2" }, - "time": "2024-12-02T14:37:59+00:00" + "time": "2026-01-20T14:10:26+00:00" }, { "name": "dompdf/php-svg-lib", - "version": "1.0.0", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af" + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af", - "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^7.1 || ^8.0", - "sabberworm/php-css-parser": "^8.4" + "sabberworm/php-css-parser": "^8.4 || ^9.0" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" }, "type": "library", "autoload": { @@ -4170,9 +4371,9 @@ "homepage": "https://github.com/dompdf/php-svg-lib", "support": { "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0" + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" }, - "time": "2024-04-29T13:26:35+00:00" + "time": "2026-01-02T16:01:13+00:00" }, { "name": "egulias/email-validator", @@ -4368,25 +4569,25 @@ }, { "name": "gregwar/captcha-bundle", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/Gregwar/CaptchaBundle.git", - "reference": "090a3754f02cadb7ecdb531b090322dbe5c03c75" + "reference": "b129efda562bf8361ca6eb77043036813f749de6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Gregwar/CaptchaBundle/zipball/090a3754f02cadb7ecdb531b090322dbe5c03c75", - "reference": "090a3754f02cadb7ecdb531b090322dbe5c03c75", + "url": "https://api.github.com/repos/Gregwar/CaptchaBundle/zipball/b129efda562bf8361ca6eb77043036813f749de6", + "reference": "b129efda562bf8361ca6eb77043036813f749de6", "shasum": "" }, "require": { "ext-gd": "*", "gregwar/captcha": "^1.2.1", "php": ">=8.0.2", - "symfony/form": "~6.0|~7.0", - "symfony/framework-bundle": "~6.0|~7.0", - "symfony/translation": "~6.0|^7.0", + "symfony/form": "~6.0|~7.0|~8.0", + "symfony/framework-bundle": "~6.0|~7.0|~8.0", + "symfony/translation": "~6.0|~7.0|~8.0", "twig/twig": "^3.0" }, "require-dev": { @@ -4429,22 +4630,22 @@ ], "support": { "issues": "https://github.com/Gregwar/CaptchaBundle/issues", - "source": "https://github.com/Gregwar/CaptchaBundle/tree/v2.4.0" + "source": "https://github.com/Gregwar/CaptchaBundle/tree/v2.5.0" }, - "time": "2025-06-24T10:25:23+00:00" + "time": "2026-01-08T10:51:57+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.10.0", + "version": "7.10.4", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + "reference": "aec528da477062d3af11f51e6b33402be233b21f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aec528da477062d3af11f51e6b33402be233b21f", + "reference": "aec528da477062d3af11f51e6b33402be233b21f", "shasum": "" }, "require": { @@ -4462,8 +4663,9 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.3.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -4541,7 +4743,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + "source": "https://github.com/guzzle/guzzle/tree/7.10.4" }, "funding": [ { @@ -4557,20 +4759,20 @@ "type": "tidelift" } ], - "time": "2025-08-23T22:36:01+00:00" + "time": "2026-05-22T19:00:53+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.3.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "481557b130ef3790cf82b713667b43030dc9c957" + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", - "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", "shasum": "" }, "require": { @@ -4578,7 +4780,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -4624,7 +4826,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.3.0" + "source": "https://github.com/guzzle/promises/tree/2.4.1" }, "funding": [ { @@ -4640,20 +4842,20 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:34:08+00:00" + "time": "2026-05-20T22:57:30+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.8.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "reference": "73ab136360b5dfd858006eae9795e8fe43c80361" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/73ab136360b5dfd858006eae9795e8fe43c80361", + "reference": "73ab136360b5dfd858006eae9795e8fe43c80361", "shasum": "" }, "require": { @@ -4668,8 +4870,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -4740,7 +4943,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/guzzle/psr7/tree/2.10.1" }, "funding": [ { @@ -4756,34 +4959,34 @@ "type": "tidelift" } ], - "time": "2025-08-23T21:21:41+00:00" + "time": "2026-05-20T09:27:36+00:00" }, { "name": "hshn/base64-encoded-file", - "version": "v5.0.3", + "version": "v6.0.0", "source": { "type": "git", "url": "https://github.com/hshn/base64-encoded-file.git", - "reference": "74984c7e69fbed9378dbf1d64e632522cc1b6d95" + "reference": "f379875f5582ebcda20111bb68d6a3268dddf345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hshn/base64-encoded-file/zipball/74984c7e69fbed9378dbf1d64e632522cc1b6d95", - "reference": "74984c7e69fbed9378dbf1d64e632522cc1b6d95", + "url": "https://api.github.com/repos/hshn/base64-encoded-file/zipball/f379875f5582ebcda20111bb68d6a3268dddf345", + "reference": "f379875f5582ebcda20111bb68d6a3268dddf345", "shasum": "" }, "require": { "php": "^8.1.0", - "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", - "symfony/mime": "^5.4 || ^6.0 || ^7.0" + "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/mime": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0.0", - "symfony/config": "^5.4 || ^6.0 || ^7.0", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", - "symfony/form": "^5.4 || ^6.0 || ^7.0", - "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", - "symfony/serializer": "^5.4 || ^6.0 || ^7.0" + "symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/form": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/serializer": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "suggest": { "symfony/config": "to use the bundle in a Symfony project", @@ -4795,7 +4998,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.x-dev" + "dev-master": "6.x-dev" } }, "autoload": { @@ -4816,22 +5019,22 @@ "description": "Provides handling base64 encoded files, and the integration of symfony/form", "support": { "issues": "https://github.com/hshn/base64-encoded-file/issues", - "source": "https://github.com/hshn/base64-encoded-file/tree/v5.0.3" + "source": "https://github.com/hshn/base64-encoded-file/tree/v6.0.0" }, - "time": "2025-10-06T10:34:52+00:00" + "time": "2025-12-05T15:24:18+00:00" }, { "name": "imagine/imagine", - "version": "1.5.0", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/php-imagine/Imagine.git", - "reference": "80ab21434890dee9ba54969d31c51ac8d4d551e0" + "reference": "f9ed796eefb77c2f0f2167e1d4e36bc2b5ed6b0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/80ab21434890dee9ba54969d31c51ac8d4d551e0", - "reference": "80ab21434890dee9ba54969d31c51ac8d4d551e0", + "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/f9ed796eefb77c2f0f2167e1d4e36bc2b5ed6b0c", + "reference": "f9ed796eefb77c2f0f2167e1d4e36bc2b5ed6b0c", "shasum": "" }, "require": { @@ -4878,9 +5081,9 @@ ], "support": { "issues": "https://github.com/php-imagine/Imagine/issues", - "source": "https://github.com/php-imagine/Imagine/tree/1.5.0" + "source": "https://github.com/php-imagine/Imagine/tree/1.5.2" }, - "time": "2024-12-03T14:37:55+00:00" + "time": "2026-01-09T10:45:12+00:00" }, { "name": "jbtronics/2fa-webauthn", @@ -4944,24 +5147,24 @@ }, { "name": "jbtronics/dompdf-font-loader-bundle", - "version": "v1.1.5", + "version": "v1.1.6", "source": { "type": "git", "url": "https://github.com/jbtronics/dompdf-font-loader-bundle.git", - "reference": "83a0e50ecceefea0a63915dae758e00788fd067e" + "reference": "5fb434f35544d5757292cd5471768dda3862c932" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jbtronics/dompdf-font-loader-bundle/zipball/83a0e50ecceefea0a63915dae758e00788fd067e", - "reference": "83a0e50ecceefea0a63915dae758e00788fd067e", + "url": "https://api.github.com/repos/jbtronics/dompdf-font-loader-bundle/zipball/5fb434f35544d5757292cd5471768dda3862c932", + "reference": "5fb434f35544d5757292cd5471768dda3862c932", "shasum": "" }, "require": { "dompdf/dompdf": "^1.0.0|^2.0.0|^3.0.0", "ext-json": "*", "php": "^8.1", - "symfony/finder": "^6.0|^7.0", - "symfony/framework-bundle": "^6.0|^7.0" + "symfony/finder": "^6.0|^7.0|^8.0", + "symfony/framework-bundle": "^6.0|^7.0|^8.0" }, "require-dev": { "phpunit/phpunit": "^9.5", @@ -4993,22 +5196,22 @@ ], "support": { "issues": "https://github.com/jbtronics/dompdf-font-loader-bundle/issues", - "source": "https://github.com/jbtronics/dompdf-font-loader-bundle/tree/v1.1.5" + "source": "https://github.com/jbtronics/dompdf-font-loader-bundle/tree/v1.1.6" }, - "time": "2025-07-25T20:29:05+00:00" + "time": "2025-11-30T22:19:12+00:00" }, { "name": "jbtronics/settings-bundle", - "version": "v3.1.1", + "version": "v3.3.1", "source": { "type": "git", "url": "https://github.com/jbtronics/settings-bundle.git", - "reference": "1067dd3d816cd0a6be7ac3d3989587ea05040bd4" + "reference": "ca90a8b2255482d11f10cb30c49791a7dabd5d40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jbtronics/settings-bundle/zipball/1067dd3d816cd0a6be7ac3d3989587ea05040bd4", - "reference": "1067dd3d816cd0a6be7ac3d3989587ea05040bd4", + "url": "https://api.github.com/repos/jbtronics/settings-bundle/zipball/ca90a8b2255482d11f10cb30c49791a7dabd5d40", + "reference": "ca90a8b2255482d11f10cb30c49791a7dabd5d40", "shasum": "" }, "require": { @@ -5016,12 +5219,12 @@ "ext-json": "*", "php": "^8.1", "symfony/deprecation-contracts": "^3.4", - "symfony/form": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/translation": "^7.0|^6.4", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.0|^6.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "require-dev": { "doctrine/doctrine-bundle": "^2.11", @@ -5034,14 +5237,16 @@ "phpstan/phpstan-symfony": "^1.3", "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-latest", - "symfony/console": "^6.4|^7.0", - "symfony/phpunit-bridge": "^6.4|^7.0", - "symfony/security-csrf": "^7.0|^6.4", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/phpunit-bridge": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^7.0|^6.4|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "suggest": { "doctrine/doctrine-bundle": "To use the doctrine ORM storage", - "symfony/twig-bridge": "Allows to access settings in twig templates" + "symfony/twig-bridge": "Allows to access settings in twig templates", + "symfony/yaml": "To use the YAML metadata driver for settings configuration" }, "type": "symfony-bundle", "autoload": { @@ -5056,20 +5261,30 @@ "authors": [ { "name": "Jan Böhmer", - "email": "mail@jan-boehmer.de" + "email": "mail@jan-boehmer.de", + "role": "Maintainer" + }, + { + "name": "Github Contributors", + "homepage": "https://github.com/jbtronics/settings-bundle/graphs/contributors" } ], "description": "A symfony bundle to easily create typesafe, user-configurable settings for symfony applications", "keywords": [ "Settings", "config", + "configuration", + "dynamic-settings", + "options", + "preferences", "symfony", "symfony-bundle", + "user-config", "user-configurable" ], "support": { "issues": "https://github.com/jbtronics/settings-bundle/issues", - "source": "https://github.com/jbtronics/settings-bundle/tree/v3.1.1" + "source": "https://github.com/jbtronics/settings-bundle/tree/v3.3.1" }, "funding": [ { @@ -5081,7 +5296,7 @@ "type": "github" } ], - "time": "2025-09-22T22:00:15+00:00" + "time": "2026-04-28T10:57:15+00:00" }, { "name": "jfcherng/php-color-output", @@ -5320,6 +5535,174 @@ ], "time": "2023-05-21T07:57:08+00:00" }, + { + "name": "jkphl/dom-factory", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/jkphl/dom-factory.git", + "reference": "dd32b8b2cc800f065c0eff8bb621d9f80147d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jkphl/dom-factory/zipball/dd32b8b2cc800f065c0eff8bb621d9f80147d45e", + "reference": "dd32b8b2cc800f065c0eff8bb621d9f80147d45e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^6.0||^7.0", + "masterminds/html5": "^2.7", + "php": ">=7.2" + }, + "require-dev": { + "clue/graph-composer": "^1.1", + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.0||^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jkphl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joschi Kuphal", + "email": "joschi@kuphal.net", + "homepage": "https://jkphl.is", + "role": "Developer" + } + ], + "description": "Simple HTML5/XML DOM factory", + "homepage": "https://github.com/jkphl/dom-factory", + "support": { + "email": "joschi@kuphal.net", + "issues": "https://github.com/jkphl/dom-factory/issues", + "source": "https://github.com/jkphl/dom-factory" + }, + "time": "2021-06-28T11:49:36+00:00" + }, + { + "name": "jkphl/micrometa", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/jkphl/micrometa.git", + "reference": "003583fa91eab9c62e5a47e9d4f909b2fad44de2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jkphl/micrometa/zipball/003583fa91eab9c62e5a47e9d4f909b2fad44de2", + "reference": "003583fa91eab9c62e5a47e9d4f909b2fad44de2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "jkphl/dom-factory": "^1", + "jkphl/rdfa-lite-microdata": "^0.4.4", + "league/uri": "^5.0|^6.5|^7.0", + "mf2/mf2": "^0.4", + "ml/json-ld": "^1.2", + "monolog/monolog": "^1.24 || ^2 || ^3", + "php": ">=7.1.3", + "psr/cache": "^1.0|^2|^3", + "psr/log": "^1.1|^2|^3", + "symfony/cache": "^4.0|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "clue/graph-composer": "^1.1", + "mf2/tests": "@dev", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.0 || ^8.5", + "squizlabs/php_codesniffer": "^3.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jkphl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joschi Kuphal", + "email": "joschi@tollwerk.de", + "homepage": "https://jkphl.is", + "role": "Developer" + } + ], + "description": "A meta parser for extracting micro information out of web documents, currently supporting Microformats 1+2, HTML Microdata, RDFa Lite 1.1 and JSON-LD", + "homepage": "https://jkphl.is/projects/micrometa/", + "support": { + "email": "joschi@tollwerk.de", + "issues": "https://github.com/jkphl/micrometa/issues", + "source": "https://github.com/jkphl/micrometa" + }, + "time": "2026-04-28T07:20:59+00:00" + }, + { + "name": "jkphl/rdfa-lite-microdata", + "version": "v0.4.7", + "source": { + "type": "git", + "url": "https://github.com/jkphl/rdfa-lite-microdata.git", + "reference": "ffc4940e8be55798257a03da7ed7d4506a13c3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jkphl/rdfa-lite-microdata/zipball/ffc4940e8be55798257a03da7ed7d4506a13c3e5", + "reference": "ffc4940e8be55798257a03da7ed7d4506a13c3e5", + "shasum": "" + }, + "require": { + "jkphl/dom-factory": "^1", + "php": ">=5.5" + }, + "require-dev": { + "clue/graph-composer": "dev-master", + "codeclimate/php-test-reporter": "^0.4.4", + "phpunit/phpunit": "^4.8", + "satooshi/php-coveralls": "^1.0", + "squizlabs/php_codesniffer": "^2.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jkphl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joschi Kuphal", + "email": "joschi@tollwerk.de", + "homepage": "https://jkphl.is", + "role": "Developer" + } + ], + "description": "RDFa Lite 1.1 and HTML Microdata parser for web documents (HTML, SVG, XML)", + "homepage": "https://github.com/jkphl/rdfa-lite-microdata", + "support": { + "email": "joschi@tollwerk.de", + "issues": "https://github.com/jkphl/rdfa-lite-microdata/issues", + "source": "https://github.com/jkphl/rdfa-lite-microdata" + }, + "time": "2023-01-27T13:29:45+00:00" + }, { "name": "kelunik/certificate", "version": "v1.1.3", @@ -5380,31 +5763,32 @@ }, { "name": "knpuniversity/oauth2-client-bundle", - "version": "v2.19.0", + "version": "v2.20.2", "source": { "type": "git", "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", - "reference": "cd1cb6945a46df81be6e94944872546ca4bf335c" + "reference": "9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/cd1cb6945a46df81be6e94944872546ca4bf335c", - "reference": "cd1cb6945a46df81be6e94944872546ca4bf335c", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7", + "reference": "9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7", "shasum": "" }, "require": { "league/oauth2-client": "^2.0", "php": ">=8.1", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/framework-bundle": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0" + "symfony/dependency-injection": "^6.4|^7.3|^8.0", + "symfony/framework-bundle": "^6.4|^7.3|^8.0", + "symfony/http-foundation": "^6.4|^7.3|^8.0", + "symfony/routing": "^6.4|^7.3|^8.0", + "symfony/security-core": "^6.4|^7.3|^8.0", + "symfony/security-http": "^6.4|^7.3|^8.0" }, "require-dev": { "league/oauth2-facebook": "^1.1|^2.0", - "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", - "symfony/security-guard": "^5.4", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/phpunit-bridge": "^7.3", + "symfony/yaml": "^6.4|^7.3|^8.0" }, "suggest": { "symfony/security-guard": "For integration with Symfony's Guard Security layer" @@ -5433,9 +5817,9 @@ ], "support": { "issues": "https://github.com/knpuniversity/oauth2-client-bundle/issues", - "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.19.0" + "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.20.2" }, - "time": "2025-09-17T15:00:36+00:00" + "time": "2026-02-12T17:07:18+00:00" }, { "name": "lcobucci/clock", @@ -5576,16 +5960,16 @@ }, { "name": "league/commonmark", - "version": "2.7.1", + "version": "2.8.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", "shasum": "" }, "require": { @@ -5610,9 +5994,9 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 | ^7.0", - "symfony/process": "^5.4 | ^6.0 | ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, @@ -5622,7 +6006,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -5679,7 +6063,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2026-03-19T13:16:38+00:00" }, { "name": "league/config", @@ -5765,16 +6149,16 @@ }, { "name": "league/csv", - "version": "9.27.0", + "version": "9.28.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "cb491b1ba3c42ff2bcd0113814f4256b42bae845" + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/cb491b1ba3c42ff2bcd0113814f4256b42bae845", - "reference": "cb491b1ba3c42ff2bcd0113814f4256b42bae845", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/6582ace29ae09ba5b07049d40ea13eb19c8b5073", + "reference": "6582ace29ae09ba5b07049d40ea13eb19c8b5073", "shasum": "" }, "require": { @@ -5784,14 +6168,14 @@ "require-dev": { "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^3.75.0", - "phpbench/phpbench": "^1.4.1", - "phpstan/phpstan": "^1.12.27", + "friendsofphp/php-cs-fixer": "^3.92.3", + "phpbench/phpbench": "^1.4.3", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-phpunit": "^1.4.2", "phpstan/phpstan-strict-rules": "^1.6.2", - "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.3.6", - "symfony/var-dumper": "^6.4.8 || ^7.3.0" + "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.5.4", + "symfony/var-dumper": "^6.4.8 || ^7.4.0 || ^8.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", @@ -5852,7 +6236,7 @@ "type": "github" } ], - "time": "2025-10-16T08:22:09+00:00" + "time": "2025-12-27T15:18:42+00:00" }, { "name": "league/html-to-markdown", @@ -5945,22 +6329,22 @@ }, { "name": "league/oauth2-client", - "version": "2.8.1", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-client.git", - "reference": "9df2924ca644736c835fc60466a3a60390d334f9" + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", - "reference": "9df2924ca644736c835fc60466a3a60390d334f9", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "php": "^7.1 || >=8.0.0 <8.5.0" + "php": "^7.1 || >=8.0.0 <8.6.0" }, "require-dev": { "mockery/mockery": "^1.3.5", @@ -6004,39 +6388,44 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-client/issues", - "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1" + "source": "https://github.com/thephpleague/oauth2-client/tree/2.9.0" }, - "time": "2025-02-26T04:37:30+00:00" + "time": "2025-11-25T22:17:17+00:00" }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -6064,6 +6453,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -6076,9 +6466,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -6088,7 +6480,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -6096,24 +6488,24 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-components", - "version": "7.5.1", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-components.git", - "reference": "4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f" + "reference": "848ff9db2f0be06229d6034b7c2e33d41b4fd675" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f", - "reference": "4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f", + "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/848ff9db2f0be06229d6034b7c2e33d41b4fd675", + "reference": "848ff9db2f0be06229d6034b7c2e33d41b4fd675", "shasum": "" }, "require": { - "league/uri": "^7.5", + "league/uri": "^7.8.1", "php": "^8.1" }, "suggest": { @@ -6122,8 +6514,10 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "ext-mbstring": "to use the sorting algorithm of URLSearchParams", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -6170,7 +6564,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-components/tree/7.5.1" + "source": "https://github.com/thephpleague/uri-components/tree/7.8.1" }, "funding": [ { @@ -6178,26 +6572,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -6205,6 +6598,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -6229,7 +6623,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -6254,7 +6648,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -6262,33 +6656,34 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "liip/imagine-bundle", - "version": "2.15.0", + "version": "2.17.1", "source": { "type": "git", "url": "https://github.com/liip/LiipImagineBundle.git", - "reference": "f8c98a5a962806f26571db219412b64266c763d8" + "reference": "69d2df3c6606495d1878fa190d6c3dc4bc5623b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/f8c98a5a962806f26571db219412b64266c763d8", - "reference": "f8c98a5a962806f26571db219412b64266c763d8", + "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/69d2df3c6606495d1878fa190d6c3dc4bc5623b6", + "reference": "69d2df3c6606495d1878fa190d6c3dc4bc5623b6", "shasum": "" }, "require": { "ext-mbstring": "*", "imagine/imagine": "^1.3.2", - "php": "^7.2|^8.0", + "php": "^8.0", + "symfony/dependency-injection": "^5.4|^6.4|^7.4|^8.0", "symfony/deprecation-contracts": "^2.5 || ^3", - "symfony/filesystem": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/finder": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/framework-bundle": "^3.4.23|^4.4|^5.3|^6.0|^7.0", - "symfony/mime": "^4.4|^5.3|^6.0|^7.0", - "symfony/options-resolver": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/process": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.4|^7.3|^8.0", + "symfony/finder": "^5.4|^6.4|^7.3|^8.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.3|^8.0", + "symfony/mime": "^5.4|^6.4|^7.3|^8.0", + "symfony/options-resolver": "^5.4|^6.4|^7.3|^8.0", + "symfony/process": "^5.4|^6.4|^7.3|^8.0", "twig/twig": "^1.44|^2.9|^3.0" }, "require-dev": { @@ -6302,17 +6697,17 @@ "phpstan/phpstan": "^1.10.0", "psr/cache": "^1.0|^2.0|^3.0", "psr/log": "^1.0", - "symfony/asset": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/browser-kit": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/cache": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/console": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/dependency-injection": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/form": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/messenger": "^4.4|^5.3|^6.0|^7.0", - "symfony/phpunit-bridge": "^7.0.2", - "symfony/templating": "^3.4|^4.4|^5.3|^6.0", - "symfony/validator": "^3.4|^4.4|^5.3|^6.0|^7.0", - "symfony/yaml": "^3.4|^4.4|^5.3|^6.0|^7.0" + "symfony/asset": "^5.4|^6.4|^7.3|^8.0", + "symfony/browser-kit": "^5.4|^6.4|^7.3|^8.0", + "symfony/cache": "^5.4|^6.4|^7.3|^8.0", + "symfony/console": "^5.4|^6.4|^7.3|^8.0", + "symfony/form": "^5.4|^6.4|^7.3|^8.0", + "symfony/messenger": "^5.4|^6.4|^7.3|^8.0", + "symfony/phpunit-bridge": "^7.3", + "symfony/runtime": "^5.4|^6.4|^7.3|^8.0", + "symfony/templating": "^5.4|^6.4|^7.3|^8.0", + "symfony/validator": "^5.4|^6.4|^7.3|^8.0", + "symfony/yaml": "^5.4|^6.4|^7.3|^8.0" }, "suggest": { "alcaeus/mongo-php-adapter": "required for mongodb components", @@ -6367,9 +6762,9 @@ ], "support": { "issues": "https://github.com/liip/LiipImagineBundle/issues", - "source": "https://github.com/liip/LiipImagineBundle/tree/2.15.0" + "source": "https://github.com/liip/LiipImagineBundle/tree/2.17.1" }, - "time": "2025-10-09T06:49:28+00:00" + "time": "2026-01-06T09:34:48+00:00" }, { "name": "lorenzo/pinky", @@ -6674,17 +7069,181 @@ "time": "2025-07-25T09:04:22+00:00" }, { - "name": "monolog/monolog", - "version": "3.9.0", + "name": "mf2/mf2", + "version": "0.4.6", "source": { "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "url": "https://github.com/microformats/php-mf2.git", + "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/microformats/php-mf2/zipball/00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23", + "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "mf2/tests": "@dev", + "phpdocumentor/phpdocumentor": "v2.8.4", + "phpunit/phpunit": "4.8.*" + }, + "suggest": { + "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you", + "masterminds/html5": "Alternative HTML parser for PHP, for better HTML5 support." + }, + "bin": [ + "bin/fetch-mf2", + "bin/parse-mf2" + ], + "type": "library", + "autoload": { + "files": [ + "Mf2/Parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "authors": [ + { + "name": "Barnaby Walters", + "homepage": "http://waterpigs.co.uk" + } + ], + "description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API", + "keywords": [ + "html", + "microformats", + "microformats 2", + "parser", + "semantic" + ], + "support": { + "issues": "https://github.com/microformats/php-mf2/issues", + "source": "https://github.com/microformats/php-mf2/tree/master" + }, + "time": "2018-08-24T14:47:04+00:00" + }, + { + "name": "ml/iri", + "version": "1.1.4", + "target-dir": "ML/IRI", + "source": { + "type": "git", + "url": "https://github.com/lanthaler/IRI.git", + "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lanthaler/IRI/zipball/cbd44fa913e00ea624241b38cefaa99da8d71341", + "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341", + "shasum": "" + }, + "require": { + "lib-pcre": ">=4.0", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "ML\\IRI": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Lanthaler", + "email": "mail@markus-lanthaler.com", + "homepage": "http://www.markus-lanthaler.com", + "role": "Developer" + } + ], + "description": "IRI handling for PHP", + "homepage": "http://www.markus-lanthaler.com", + "keywords": [ + "URN", + "iri", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/lanthaler/IRI/issues", + "source": "https://github.com/lanthaler/IRI/tree/master" + }, + "time": "2014-01-21T13:43:39+00:00" + }, + { + "name": "ml/json-ld", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/lanthaler/JsonLD.git", + "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lanthaler/JsonLD/zipball/537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ml/iri": "^1.1.1", + "php": ">=5.3.0" + }, + "require-dev": { + "json-ld/tests": "1.0", + "phpunit/phpunit": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ML\\JsonLD\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Lanthaler", + "email": "mail@markus-lanthaler.com", + "homepage": "http://www.markus-lanthaler.com", + "role": "Developer" + } + ], + "description": "JSON-LD Processor for PHP", + "homepage": "http://www.markus-lanthaler.com", + "keywords": [ + "JSON-LD", + "jsonld" + ], + "support": { + "issues": "https://github.com/lanthaler/JsonLD/issues", + "source": "https://github.com/lanthaler/JsonLD/tree/1.2.1" + }, + "time": "2022-09-29T08:45:17+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -6702,7 +7261,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -6762,7 +7321,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -6774,7 +7333,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "myclabs/php-enum", @@ -6977,25 +7536,28 @@ }, { "name": "nelmio/cors-bundle", - "version": "2.5.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioCorsBundle.git", - "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544" + "reference": "3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3a526fe025cd20e04a6a11370cf5ab28dbb5a544", - "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c", + "reference": "3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c", "shasum": "" }, "require": { "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "mockery/mockery": "^1.3.6", - "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + "phpstan/phpstan": "^1.11.5", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-symfony": "^1.4.4", + "phpunit/phpunit": "^8" }, "type": "symfony-bundle", "extra": { @@ -7033,47 +7595,47 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", - "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.5.0" + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.6.1" }, - "time": "2024-06-24T21:25:28+00:00" + "time": "2026-01-12T15:59:08+00:00" }, { "name": "nelmio/security-bundle", - "version": "v3.6.0", + "version": "v3.9.0", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioSecurityBundle.git", - "reference": "f3a7bf628a0873788172a0d05d20c0224080f5eb" + "reference": "86dd4d12bc729498cd6f52b95ab6b36a66c72fd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioSecurityBundle/zipball/f3a7bf628a0873788172a0d05d20c0224080f5eb", - "reference": "f3a7bf628a0873788172a0d05d20c0224080f5eb", + "url": "https://api.github.com/repos/nelmio/NelmioSecurityBundle/zipball/86dd4d12bc729498cd6f52b95ab6b36a66c72fd2", + "reference": "86dd4d12bc729498cd6f52b95ab6b36a66c72fd2", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", "symfony/deprecation-contracts": "^2.5 || ^3", - "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0", - "symfony/http-kernel": "^5.4 || ^6.3 || ^7.0", - "symfony/security-core": "^5.4 || ^6.3 || ^7.0", - "symfony/security-csrf": "^5.4 || ^6.3 || ^7.0", - "symfony/security-http": "^5.4 || ^6.3 || ^7.0", - "symfony/yaml": "^5.4 || ^6.3 || ^7.0", + "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/http-kernel": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/security-core": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/security-csrf": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/security-http": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.3 || ^7.0 || ^8.0", "ua-parser/uap-php": "^3.4.4" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpstan/phpstan-symfony": "^1.1", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpunit/phpunit": "^9.5 || ^10.1 || ^11.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/browser-kit": "^5.4 || ^6.3 || ^7.0", - "symfony/cache": "^5.4 || ^6.3 || ^7.0", - "symfony/phpunit-bridge": "^6.3 || ^7.0", - "symfony/twig-bundle": "^5.4 || ^6.3 || ^7.0", + "symfony/browser-kit": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/cache": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0 || ^8.0", + "symfony/twig-bundle": "^5.4 || ^6.3 || ^7.0 || ^8.0", "twig/twig": "^2.10 || ^3.0" }, "type": "symfony-bundle", @@ -7107,31 +7669,33 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioSecurityBundle/issues", - "source": "https://github.com/nelmio/NelmioSecurityBundle/tree/v3.6.0" + "source": "https://github.com/nelmio/NelmioSecurityBundle/tree/v3.9.0" }, - "time": "2025-09-19T08:24:46+00:00" + "time": "2026-02-23T10:58:33+00:00" }, { "name": "nette/schema", - "version": "v1.3.2", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.4" + "php": "8.1 - 8.5" }, "require-dev": { - "nette/tester": "^2.5.2", - "phpstan/phpstan-nette": "^1.0", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", "tracy/tracy": "^2.8" }, "type": "library", @@ -7141,6 +7705,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -7169,26 +7736,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.2" + "source": "https://github.com/nette/schema/tree/v1.3.5" }, - "time": "2024-10-06T23:10:23+00:00" + "time": "2026-02-23T03:47:12+00:00" }, { "name": "nette/utils", - "version": "v4.0.8", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", "shasum": "" }, "require": { - "php": "8.0 - 8.5" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", @@ -7196,8 +7763,10 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", "nette/tester": "^2.5", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -7211,7 +7780,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -7258,9 +7827,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.8" + "source": "https://github.com/nette/utils/tree/v4.1.4" }, - "time": "2025-08-06T21:43:34+00:00" + "time": "2026-05-11T20:49:54+00:00" }, { "name": "nikolaposa/version", @@ -7403,63 +7972,63 @@ }, { "name": "omines/datatables-bundle", - "version": "0.10.3", + "version": "0.10.7", "source": { "type": "git", "url": "https://github.com/omines/datatables-bundle.git", - "reference": "d64e7d5c72303995ada7365b467166f3cdf4757c" + "reference": "4cd6d27b12c79a1ed72b4953a86aedf289e8701d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/omines/datatables-bundle/zipball/d64e7d5c72303995ada7365b467166f3cdf4757c", - "reference": "d64e7d5c72303995ada7365b467166f3cdf4757c", + "url": "https://api.github.com/repos/omines/datatables-bundle/zipball/4cd6d27b12c79a1ed72b4953a86aedf289e8701d", + "reference": "4cd6d27b12c79a1ed72b4953a86aedf289e8701d", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/event-dispatcher": "^6.4|^7.1", - "symfony/framework-bundle": "^6.4|^7.1", - "symfony/options-resolver": "^6.4|^7.1", + "symfony/event-dispatcher": "^6.4|^7.3|^8.0", + "symfony/framework-bundle": "^6.4|^7.3|^8.0", + "symfony/options-resolver": "^6.4|^7.3|^8.0", "symfony/polyfill-mbstring": "^1.31.0", - "symfony/property-access": "^6.4|^7.1", - "symfony/translation": "^6.4|^7.1" + "symfony/property-access": "^6.4|^7.3|^8.0", + "symfony/translation": "^6.4|^7.3|^8.0" }, "conflict": { "doctrine/orm": "^3.0 <3.3" }, "require-dev": { "doctrine/common": "^3.5.0", - "doctrine/doctrine-bundle": "^2.15.0", - "doctrine/orm": "^2.19.3|^3.4.1", - "doctrine/persistence": "^3.4.0|^4.0.0", + "doctrine/doctrine-bundle": "^2.18.1|^3.0.0@dev", + "doctrine/orm": "^2.19.3|^3.5.7@dev", + "doctrine/persistence": "^3.4.0|^4.1.1", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", "ext-mongodb": "*", "ext-pdo_sqlite": "*", "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.75.0", - "mongodb/mongodb": "^1.20.0|^2.1.0", - "ocramius/package-versions": "^2.9", - "openspout/openspout": "^4.23", - "phpoffice/phpspreadsheet": "^2.3.3|^3.9.2|^4.4.0", + "friendsofphp/php-cs-fixer": "^3.90.0", + "mongodb/mongodb": "^1.20.0|^2.1.2", + "openspout/openspout": "^4.28.5", + "phpoffice/phpspreadsheet": "^2.3.3|^3.9.2|^4.5.0|^5.2.0", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.17", - "phpstan/phpstan-doctrine": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.6", - "phpstan/phpstan-symfony": "^2.0.6", - "phpunit/phpunit": "^10.5.38|^11.5.24", + "phpstan/phpstan": "^2.1.32", + "phpstan/phpstan-doctrine": "^2.0.11", + "phpstan/phpstan-phpunit": "^2.0.8", + "phpstan/phpstan-symfony": "^2.0.8", + "phpunit/phpunit": "^11.5.44|^12.4.4", "ruflin/elastica": "^7.3.2", - "symfony/browser-kit": "^6.4.13|^7.3", - "symfony/css-selector": "^6.4.13|^7.3", - "symfony/doctrine-bridge": "^6.4.13|^7.3", - "symfony/dom-crawler": "^6.4.13|^7.3", - "symfony/intl": "^6.4.13|^7.3", - "symfony/mime": "^6.4.13|^7.3", - "symfony/phpunit-bridge": "^7.3", - "symfony/twig-bundle": "^6.4|^7.3", - "symfony/var-dumper": "^6.4.13|^7.3", - "symfony/yaml": "^6.4.13|^7.3" + "symfony/browser-kit": "^6.4.13|^7.3|^8.0", + "symfony/css-selector": "^6.4.13|^7.3|^8.0", + "symfony/doctrine-bridge": "^6.4.13|^7.3|^8.0", + "symfony/dom-crawler": "^6.4.13|^7.3|^8.0", + "symfony/intl": "^6.4.13|^7.3|^8.0", + "symfony/mime": "^6.4.13|^7.3|^8.0", + "symfony/phpunit-bridge": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.3|^8.0", + "symfony/var-dumper": "^6.4.13|^7.3|^8.0", + "symfony/var-exporter": "^v6.4.26|^7.3", + "symfony/yaml": "^6.4.13|^7.3|^8.0" }, "suggest": { "doctrine/doctrine-bundle": "For integrated access to Doctrine object managers", @@ -7511,7 +8080,7 @@ ], "support": { "issues": "https://github.com/omines/datatables-bundle/issues", - "source": "https://github.com/omines/datatables-bundle/tree/0.10.3" + "source": "https://github.com/omines/datatables-bundle/tree/0.10.7" }, "funding": [ { @@ -7519,25 +8088,25 @@ "type": "github" } ], - "time": "2025-07-24T19:50:46+00:00" + "time": "2025-11-28T21:20:14+00:00" }, { "name": "onelogin/php-saml", - "version": "4.3.0", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/SAML-Toolkits/php-saml.git", - "reference": "bf5efce9f2df5d489d05e78c27003a0fc8bc50f0" + "reference": "26b3a47349415e5b7aa300ba4ab7fc316c65f19e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SAML-Toolkits/php-saml/zipball/bf5efce9f2df5d489d05e78c27003a0fc8bc50f0", - "reference": "bf5efce9f2df5d489d05e78c27003a0fc8bc50f0", + "url": "https://api.github.com/repos/SAML-Toolkits/php-saml/zipball/26b3a47349415e5b7aa300ba4ab7fc316c65f19e", + "reference": "26b3a47349415e5b7aa300ba4ab7fc316c65f19e", "shasum": "" }, "require": { "php": ">=7.3", - "robrichards/xmlseclibs": "^3.1" + "robrichards/xmlseclibs": "^3.1.5" }, "require-dev": { "pdepend/pdepend": "^2.8.0", @@ -7583,7 +8152,59 @@ "type": "github" } ], - "time": "2025-05-25T14:28:00+00:00" + "time": "2026-05-07T22:38:04+00:00" + }, + { + "name": "oskarstark/enum-helper", + "version": "1.8.4", + "source": { + "type": "git", + "url": "https://github.com/OskarStark/enum-helper.git", + "reference": "14e185f1cc259d7cd3f61eea17f9b174a886a6da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OskarStark/enum-helper/zipball/14e185f1cc259d7cd3f61eea17f9b174a886a6da", + "reference": "14e185f1cc259d7cd3f61eea17f9b174a886a6da", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "phpunit/phpunit": "<10" + }, + "require-dev": { + "ergebnis/php-cs-fixer-config": "^6.58", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.5", + "rector/rector": "^2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "OskarStark\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + } + ], + "description": "This library provides helpers for several enum operations", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/OskarStark/enum-helper/issues", + "source": "https://github.com/OskarStark/enum-helper/tree/1.8.4" + }, + "time": "2026-02-05T08:59:09+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -7654,142 +8275,6 @@ }, "time": "2025-09-24T15:06:41+00:00" }, - { - "name": "paragonie/random_compat", - "version": "v9.99.100", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", - "shasum": "" - }, - "require": { - "php": ">= 7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, - "time": "2020-10-15T08:29:30+00:00" - }, - { - "name": "paragonie/sodium_compat", - "version": "v1.23.0", - "source": { - "type": "git", - "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "b938a5c6844d222a26d46a6c7b80291e4cd8cfab" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/b938a5c6844d222a26d46a6c7b80291e4cd8cfab", - "reference": "b938a5c6844d222a26d46a6c7b80291e4cd8cfab", - "shasum": "" - }, - "require": { - "paragonie/random_compat": ">=1", - "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" - }, - "require-dev": { - "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" - }, - "suggest": { - "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", - "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." - }, - "type": "library", - "autoload": { - "files": [ - "autoload.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com" - }, - { - "name": "Frank Denis", - "email": "jedisct1@pureftpd.org" - } - ], - "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", - "keywords": [ - "Authentication", - "BLAKE2b", - "ChaCha20", - "ChaCha20-Poly1305", - "Chapoly", - "Curve25519", - "Ed25519", - "EdDSA", - "Edwards-curve Digital Signature Algorithm", - "Elliptic Curve Diffie-Hellman", - "Poly1305", - "Pure-PHP cryptography", - "RFC 7748", - "RFC 8032", - "Salpoly", - "Salsa20", - "X25519", - "XChaCha20-Poly1305", - "XSalsa20-Poly1305", - "Xchacha20", - "Xsalsa20", - "aead", - "cryptography", - "ecdh", - "elliptic curve", - "elliptic curve cryptography", - "encryption", - "libsodium", - "php", - "public-key cryptography", - "secret-key cryptography", - "side-channel resistant" - ], - "support": { - "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v1.23.0" - }, - "time": "2025-10-06T08:53:07+00:00" - }, { "name": "part-db/exchanger", "version": "v3.1.0", @@ -7869,16 +8354,16 @@ }, { "name": "part-db/label-fonts", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/Part-DB/label-fonts.git", - "reference": "c85aeb051d6492961a2c59bc291979f15ce60e88" + "reference": "f93cbc8885c96792ab86f42d76e58cf8825975d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Part-DB/label-fonts/zipball/c85aeb051d6492961a2c59bc291979f15ce60e88", - "reference": "c85aeb051d6492961a2c59bc291979f15ce60e88", + "url": "https://api.github.com/repos/Part-DB/label-fonts/zipball/f93cbc8885c96792ab86f42d76e58cf8825975d9", + "reference": "f93cbc8885c96792ab86f42d76e58cf8825975d9", "shasum": "" }, "type": "library", @@ -7901,9 +8386,9 @@ ], "support": { "issues": "https://github.com/Part-DB/label-fonts/issues", - "source": "https://github.com/Part-DB/label-fonts/tree/v1.2.0" + "source": "https://github.com/Part-DB/label-fonts/tree/v1.3.0" }, - "time": "2025-09-07T15:42:51+00:00" + "time": "2026-02-14T21:44:31+00:00" }, { "name": "part-db/swap", @@ -8346,16 +8831,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.3", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -8363,9 +8848,9 @@ "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" + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { "mockery/mockery": "~1.3.5 || ~1.6.0", @@ -8374,7 +8859,8 @@ "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" }, "type": "library", "extra": { @@ -8404,44 +8890,44 @@ "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.3" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2025-08-01T19:43:32+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" + "phpstan/phpdoc-parser": "^2.0" }, "require-dev": { "ext-tokenizer": "*", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "psalm/phar": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -8462,22 +8948,22 @@ "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.10.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" }, - "time": "2024-11-09T15:12:26+00:00" + "time": "2026-01-06T21:53:42+00:00" }, { "name": "phpoffice/phpspreadsheet", - "version": "5.1.0", + "version": "5.7.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0" + "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fd26e45a814e94ae2aad0df757d9d1739c4bf2e0", - "reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", + "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", "shasum": "" }, "require": { @@ -8485,6 +8971,7 @@ "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", + "ext-filter": "*", "ext-gd": "*", "ext-iconv": "*", "ext-libxml": "*", @@ -8499,15 +8986,14 @@ "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", "php": "^8.1", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", "dompdf/dompdf": "^2.0 || ^3.0", + "ext-intl": "*", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.3", + "mitoteam/jpgraph": "^10.5", "mpdf/mpdf": "^8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1 || ^2.0", @@ -8519,7 +9005,7 @@ }, "suggest": { "dompdf/dompdf": "Option for rendering PDF with PDF Writer", - "ext-intl": "PHP Internationalization Functions", + "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" @@ -8552,6 +9038,9 @@ }, { "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", @@ -8568,22 +9057,22 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.1.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.7.0" }, - "time": "2025-09-04T05:34:49+00:00" + "time": "2026-04-20T02:42:17+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.0", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -8615,9 +9104,9 @@ "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.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2025-08-30T15:50:23+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "psr/cache", @@ -9182,16 +9671,16 @@ }, { "name": "revolt/event-loop", - "version": "v1.0.7", + "version": "v1.0.9", "source": { "type": "git", "url": "https://github.com/revoltphp/event-loop.git", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + "reference": "44061cf513e53c6200372fc935ac42271566295d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", "shasum": "" }, "require": { @@ -9201,7 +9690,7 @@ "ext-json": "*", "jetbrains/phpstorm-stubs": "^2019.3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.15" + "psalm/phar": "6.16.*" }, "type": "library", "extra": { @@ -9248,22 +9737,22 @@ ], "support": { "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" }, - "time": "2025-01-25T19:27:39+00:00" + "time": "2026-05-16T17:55:38+00:00" }, { "name": "rhukster/dom-sanitizer", - "version": "1.0.7", + "version": "1.0.11", "source": { "type": "git", "url": "https://github.com/rhukster/dom-sanitizer.git", - "reference": "c2a98f27ad742668b254282ccc5581871d0fb601" + "reference": "02d08ec8b36b93b04517d74fe82b715ef06273bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/c2a98f27ad742668b254282ccc5581871d0fb601", - "reference": "c2a98f27ad742668b254282ccc5581871d0fb601", + "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/02d08ec8b36b93b04517d74fe82b715ef06273bd", + "reference": "02d08ec8b36b93b04517d74fe82b715ef06273bd", "shasum": "" }, "require": { @@ -9293,22 +9782,22 @@ "description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+", "support": { "issues": "https://github.com/rhukster/dom-sanitizer/issues", - "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.7" + "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.11" }, - "time": "2023-11-06T16:46:48+00:00" + "time": "2026-04-23T22:56:32+00:00" }, { "name": "robrichards/xmlseclibs", - "version": "3.1.3", + "version": "3.1.5", "source": { "type": "git", "url": "https://github.com/robrichards/xmlseclibs.git", - "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07" + "reference": "03062be78178cbb5e8f605cd255dc32a14981f92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/2bdfd742624d739dfadbd415f00181b4a77aaf07", - "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/03062be78178cbb5e8f605cd255dc32a14981f92", + "reference": "03062be78178cbb5e8f605cd255dc32a14981f92", "shasum": "" }, "require": { @@ -9335,61 +9824,9 @@ ], "support": { "issues": "https://github.com/robrichards/xmlseclibs/issues", - "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.3" + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.5" }, - "time": "2024-11-20T21:13:56+00:00" - }, - { - "name": "runtime/frankenphp-symfony", - "version": "0.2.0", - "source": { - "type": "git", - "url": "https://github.com/php-runtime/frankenphp-symfony.git", - "reference": "56822c3631d9522a3136a4c33082d006bdfe4bad" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-runtime/frankenphp-symfony/zipball/56822c3631d9522a3136a4c33082d006bdfe4bad", - "reference": "56822c3631d9522a3136a4c33082d006bdfe4bad", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", - "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", - "symfony/runtime": "^5.4 || ^6.0 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Runtime\\FrankenPhpSymfony\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.dev" - } - ], - "description": "FrankenPHP runtime for Symfony", - "support": { - "issues": "https://github.com/php-runtime/frankenphp-symfony/issues", - "source": "https://github.com/php-runtime/frankenphp-symfony/tree/0.2.0" - }, - "funding": [ - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2023-12-12T12:06:11+00:00" + "time": "2026-03-13T10:31:56+00:00" }, { "name": "s9e/regexp-builder", @@ -9481,16 +9918,16 @@ }, { "name": "s9e/text-formatter", - "version": "2.19.0", + "version": "2.19.3", "source": { "type": "git", "url": "https://github.com/s9e/TextFormatter.git", - "reference": "d65a4f61cbe494937afb3150dc73b6e757d400d3" + "reference": "aee579c12d05ca3053f9b9abdb8c479c0f2fbe69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/s9e/TextFormatter/zipball/d65a4f61cbe494937afb3150dc73b6e757d400d3", - "reference": "d65a4f61cbe494937afb3150dc73b6e757d400d3", + "url": "https://api.github.com/repos/s9e/TextFormatter/zipball/aee579c12d05ca3053f9b9abdb8c479c0f2fbe69", + "reference": "aee579c12d05ca3053f9b9abdb8c479c0f2fbe69", "shasum": "" }, "require": { @@ -9518,7 +9955,7 @@ }, "type": "library", "extra": { - "version": "2.19.0" + "version": "2.19.3" }, "autoload": { "psr-4": { @@ -9550,31 +9987,41 @@ ], "support": { "issues": "https://github.com/s9e/TextFormatter/issues", - "source": "https://github.com/s9e/TextFormatter/tree/2.19.0" + "source": "https://github.com/s9e/TextFormatter/tree/2.19.3" }, - "time": "2025-04-26T09:27:34+00:00" + "time": "2025-11-14T21:26:59+00:00" }, { "name": "sabberworm/php-css-parser", - "version": "v8.9.0", + "version": "v9.3.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", - "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", "shasum": "" }, "require": { "ext-iconv": "*", - "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" }, "require-dev": { - "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", - "rawr/cross-data-providers": "^2.0.0" + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.32 || 2.1.32", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.2.8", + "rector/type-perfect": "1.0.0 || 2.1.0", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -9582,10 +10029,14 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.0.x-dev" + "dev-main": "9.4.x-dev" } }, "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], "psr-4": { "Sabberworm\\CSS\\": "src/" } @@ -9616,26 +10067,87 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0" }, - "time": "2025-07-11T13:20:48+00:00" + "time": "2026-03-03T17:31:43+00:00" }, { - "name": "scheb/2fa-backup-code", - "version": "v7.11.0", + "name": "sabre/uri", + "version": "3.1.0", "source": { "type": "git", - "url": "https://github.com/scheb/2fa-backup-code.git", - "reference": "62c6099b179903db5ab03b8059068cdb28659294" + "url": "https://github.com/sabre-io/uri.git", + "reference": "a926c749dddfb289b8a9b5218d16ac06affdc631" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-backup-code/zipball/62c6099b179903db5ab03b8059068cdb28659294", - "reference": "62c6099b179903db5ab03b8059068cdb28659294", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/a926c749dddfb289b8a9b5218d16ac06affdc631", + "reference": "a926c749dddfb289b8a9b5218d16ac06affdc631", "shasum": "" }, "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "^8.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5", + "rector/rector": "^2.4" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "time": "2026-04-26T04:19:03+00:00" + }, + { + "name": "scheb/2fa-backup-code", + "version": "v7.13.1", + "source": { + "type": "git", + "url": "https://github.com/scheb/2fa-backup-code.git", + "reference": "35f1ace4be7be2c10158d2bb8284208499111db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scheb/2fa-backup-code/zipball/35f1ace4be7be2c10158d2bb8284208499111db8", + "reference": "35f1ace4be7be2c10158d2bb8284208499111db8", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "scheb/2fa-bundle": "self.version" }, "type": "library", @@ -9665,27 +10177,27 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-backup-code/tree/v7.11.0" + "source": "https://github.com/scheb/2fa-backup-code/tree/v7.13.1" }, - "time": "2025-04-20T08:27:40+00:00" + "time": "2025-11-20T13:35:24+00:00" }, { "name": "scheb/2fa-bundle", - "version": "v7.11.0", + "version": "v7.13.1", "source": { "type": "git", "url": "https://github.com/scheb/2fa-bundle.git", - "reference": "06a343d14dad8cdd1670157d384738f9cfba29e5" + "reference": "edcc14456b508aab37ec792cfc36793d04226784" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/06a343d14dad8cdd1670157d384738f9cfba29e5", - "reference": "06a343d14dad8cdd1670157d384738f9cfba29e5", + "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/edcc14456b508aab37ec792cfc36793d04226784", + "reference": "edcc14456b508aab37ec792cfc36793d04226784", "shasum": "" }, "require": { "ext-json": "*", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "symfony/config": "^6.4 || ^7.0", "symfony/dependency-injection": "^6.4 || ^7.0", "symfony/event-dispatcher": "^6.4 || ^7.0", @@ -9733,29 +10245,32 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-bundle/tree/v7.11.0" + "source": "https://github.com/scheb/2fa-bundle/tree/v7.13.1" }, - "time": "2025-06-27T12:14:20+00:00" + "time": "2025-12-18T15:29:07+00:00" }, { "name": "scheb/2fa-google-authenticator", - "version": "v7.11.0", + "version": "v7.13.1", "source": { "type": "git", "url": "https://github.com/scheb/2fa-google-authenticator.git", - "reference": "01a446eb68a76c3d0528a190029afa5e6ce5c384" + "reference": "7ad34bbde343a0770571464127ee072aacb70a58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/01a446eb68a76c3d0528a190029afa5e6ce5c384", - "reference": "01a446eb68a76c3d0528a190029afa5e6ce5c384", + "url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/7ad34bbde343a0770571464127ee072aacb70a58", + "reference": "7ad34bbde343a0770571464127ee072aacb70a58", "shasum": "" }, "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "scheb/2fa-bundle": "self.version", "spomky-labs/otphp": "^11.0" }, + "suggest": { + "symfony/validator": "Needed if you want to use the Google Authenticator TOTP validator constraint" + }, "type": "library", "autoload": { "psr-4": { @@ -9783,28 +10298,28 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-google-authenticator/tree/v7.11.0" + "source": "https://github.com/scheb/2fa-google-authenticator/tree/v7.13.1" }, - "time": "2025-04-20T08:38:44+00:00" + "time": "2025-12-04T15:55:14+00:00" }, { "name": "scheb/2fa-trusted-device", - "version": "v7.11.0", + "version": "v7.13.1", "source": { "type": "git", "url": "https://github.com/scheb/2fa-trusted-device.git", - "reference": "6ab98fdee3aa001ca6598eeb422d9abf2c85b5b3" + "reference": "ae3a5819faccbf151af078f432e4e6c97bb44ebf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-trusted-device/zipball/6ab98fdee3aa001ca6598eeb422d9abf2c85b5b3", - "reference": "6ab98fdee3aa001ca6598eeb422d9abf2c85b5b3", + "url": "https://api.github.com/repos/scheb/2fa-trusted-device/zipball/ae3a5819faccbf151af078f432e4e6c97bb44ebf", + "reference": "ae3a5819faccbf151af078f432e4e6c97bb44ebf", "shasum": "" }, "require": { "lcobucci/clock": "^3.0", "lcobucci/jwt": "^5.0", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "scheb/2fa-bundle": "self.version" }, "type": "library", @@ -9834,36 +10349,36 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-trusted-device/tree/v7.11.0" + "source": "https://github.com/scheb/2fa-trusted-device/tree/v7.13.1" }, - "time": "2025-06-27T12:14:20+00:00" + "time": "2025-12-01T15:40:59+00:00" }, { "name": "shivas/versioning-bundle", - "version": "4.1.1", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/shivas/versioning-bundle.git", - "reference": "fd89e3501ff1b0d3e6abe61eb7a878d1d4746868" + "reference": "5013ef49951cb8be3846eb77bf3f096a51ea66d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shivas/versioning-bundle/zipball/fd89e3501ff1b0d3e6abe61eb7a878d1d4746868", - "reference": "fd89e3501ff1b0d3e6abe61eb7a878d1d4746868", + "url": "https://api.github.com/repos/shivas/versioning-bundle/zipball/5013ef49951cb8be3846eb77bf3f096a51ea66d1", + "reference": "5013ef49951cb8be3846eb77bf3f096a51ea66d1", "shasum": "" }, "require": { "nikolaposa/version": "^4", "php": "^7.2.5 || ^8", - "symfony/console": "^5.4 || ^6 || ^7", - "symfony/framework-bundle": "^5.4 || ^6 || ^7", - "symfony/process": "^5.4 || ^6 || ^7" + "symfony/console": "^5.4 || ^6 || ^7 || ^8", + "symfony/framework-bundle": "^5.4 || ^6 || ^7 || ^8", + "symfony/process": "^5.4 || ^6 || ^7 || ^8" }, "require-dev": { "mikey179/vfsstream": "^2", - "nyholm/symfony-bundle-test": "^3.0", + "nyholm/symfony-bundle-test": "^3.1", "phpunit/phpunit": "^8.5.27", - "symfony/phpunit-bridge": "^5.4 || ^6 || ^7", + "symfony/phpunit-bridge": "^5.4 || ^6 || ^7 || ^8", "twig/twig": "^2 || ^3" }, "type": "symfony-bundle", @@ -9893,28 +10408,28 @@ ], "support": { "issues": "https://github.com/shivas/versioning-bundle/issues", - "source": "https://github.com/shivas/versioning-bundle/tree/4.1.1", + "source": "https://github.com/shivas/versioning-bundle/tree/4.2.0", "wiki": "https://github.com/shivas/versioning-bundle/wiki" }, - "time": "2024-08-14T19:33:15+00:00" + "time": "2026-03-30T07:38:31+00:00" }, { "name": "spatie/db-dumper", - "version": "3.8.0", + "version": "3.8.3", "source": { "type": "git", "url": "https://github.com/spatie/db-dumper.git", - "reference": "91e1fd4dc000aefc9753cda2da37069fc996baee" + "reference": "eac3221fbe27fac51f388600d27b67b1b079406e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/db-dumper/zipball/91e1fd4dc000aefc9753cda2da37069fc996baee", - "reference": "91e1fd4dc000aefc9753cda2da37069fc996baee", + "url": "https://api.github.com/repos/spatie/db-dumper/zipball/eac3221fbe27fac51f388600d27b67b1b079406e", + "reference": "eac3221fbe27fac51f388600d27b67b1b079406e", "shasum": "" }, "require": { "php": "^8.0", - "symfony/process": "^5.0|^6.0|^7.0" + "symfony/process": "^5.0|^6.0|^7.0|^8.0" }, "require-dev": { "pestphp/pest": "^1.22" @@ -9947,7 +10462,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/db-dumper/tree/3.8.0" + "source": "https://github.com/spatie/db-dumper/tree/3.8.3" }, "funding": [ { @@ -9959,44 +10474,32 @@ "type": "github" } ], - "time": "2025-02-14T15:04:22+00:00" + "time": "2026-01-05T16:26:03+00:00" }, { "name": "spomky-labs/cbor-php", - "version": "3.1.1", + "version": "3.2.3", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/cbor-php.git", - "reference": "5404f3e21cbe72f5cf612aa23db2b922fd2f43bf" + "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/5404f3e21cbe72f5cf612aa23db2b922fd2f43bf", - "reference": "5404f3e21cbe72f5cf612aa23db2b922fd2f43bf", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", + "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13", + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", "ext-mbstring": "*", "php": ">=8.0" }, "require-dev": { - "deptrac/deptrac": "^3.0", - "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", "ext-json": "*", - "infection/infection": "^0.29", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.0|^2.0", - "phpstan/phpstan-beberlei-assert": "^1.0|^2.0", - "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", - "phpstan/phpstan-phpunit": "^1.0|^2.0", - "phpstan/phpstan-strict-rules": "^1.0|^2.0", - "phpunit/phpunit": "^10.1|^11.0|^12.0", - "rector/rector": "^1.0|^2.0", "roave/security-advisories": "dev-latest", - "symfony/var-dumper": "^6.0|^7.0", - "symplify/easy-coding-standard": "^12.0" + "symfony/error-handler": "^6.4|^7.1|^8.0", + "symfony/var-dumper": "^6.4|^7.1|^8.0" }, "suggest": { "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", @@ -10030,7 +10533,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/cbor-php/issues", - "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.1.1" + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.2.3" }, "funding": [ { @@ -10042,42 +10545,30 @@ "type": "patreon" } ], - "time": "2025-06-13T11:57:55+00:00" + "time": "2026-04-01T12:15:20+00:00" }, { "name": "spomky-labs/otphp", - "version": "11.3.0", + "version": "11.4.2", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/otphp.git", - "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33" + "reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", - "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad", + "reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad", "shasum": "" }, "require": { - "ext-mbstring": "*", "paragonie/constant_time_encoding": "^2.0 || ^3.0", "php": ">=8.1", "psr/clock": "^1.0", "symfony/deprecation-contracts": "^3.2" }, "require-dev": { - "ekino/phpstan-banned-code": "^1.0", - "infection/infection": "^0.26|^0.27|^0.28|^0.29", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5.26|^10.0|^11.0", - "qossmic/deptrac-shim": "^1.0", - "rector/rector": "^1.0", - "symfony/phpunit-bridge": "^6.1|^7.0", - "symplify/easy-coding-standard": "^12.0" + "symfony/error-handler": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10112,7 +10603,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/otphp/issues", - "source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0" + "source": "https://github.com/Spomky-Labs/otphp/tree/11.4.2" }, "funding": [ { @@ -10124,44 +10615,45 @@ "type": "patreon" } ], - "time": "2024-06-12T11:22:32+00:00" + "time": "2026-01-23T10:53:01+00:00" }, { "name": "spomky-labs/pki-framework", - "version": "1.3.0", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/pki-framework.git", - "reference": "eced5b5ce70518b983ff2be486e902bbd15135ae" + "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/eced5b5ce70518b983ff2be486e902bbd15135ae", - "reference": "eced5b5ce70518b983ff2be486e902bbd15135ae", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/aa576cbd07128075bef97ac2f8af9854e67513d8", + "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8", "shasum": "" }, "require": { - "brick/math": "^0.10|^0.11|^0.12|^0.13", + "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", "ext-mbstring": "*", - "php": ">=8.1" + "php": ">=8.1", + "psr/clock": "^1.0" }, "require-dev": { "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", "ext-gmp": "*", "ext-openssl": "*", - "infection/infection": "^0.28|^0.29", + "infection/infection": "^0.28|^0.29|^0.31|^0.32", "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/extension-installer": "^1.3|^2.0", "phpstan/phpstan": "^1.8|^2.0", "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", "phpstan/phpstan-phpunit": "^1.1|^2.0", "phpstan/phpstan-strict-rules": "^1.3|^2.0", - "phpunit/phpunit": "^10.1|^11.0|^12.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0|^13.0", "rector/rector": "^1.0|^2.0", "roave/security-advisories": "dev-latest", - "symfony/string": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symplify/easy-coding-standard": "^12.0" + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symplify/easy-coding-standard": "^12.0|^13.0" }, "suggest": { "ext-bcmath": "For better performance (or GMP)", @@ -10221,7 +10713,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/pki-framework/issues", - "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.3.0" + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.2" }, "funding": [ { @@ -10233,7 +10725,556 @@ "type": "patreon" } ], - "time": "2025-06-13T08:35:04+00:00" + "time": "2026-03-23T22:56:56+00:00" + }, + { + "name": "symfony/ai-bundle", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-bundle.git", + "reference": "77fd1b513174770acf49abd68effa995fa518f7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-bundle/zipball/77fd1b513174770acf49abd68effa995fa518f7c", + "reference": "77fd1b513174770acf49abd68effa995fa518f7c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-platform": "^0.9", + "symfony/clock": "^7.3|^8.0", + "symfony/config": "^7.3|^8.0", + "symfony/console": "^7.3|^8.0", + "symfony/dependency-injection": "^7.3|^8.0", + "symfony/framework-bundle": "^7.3|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.3|^8.0" + }, + "require-dev": { + "google/auth": "^1.47", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53", + "symfony/ai-agent": "^0.9", + "symfony/ai-ai-ml-api-platform": "^0.9", + "symfony/ai-albert-platform": "^0.9", + "symfony/ai-amazee-ai-platform": "^0.9", + "symfony/ai-anthropic-platform": "^0.9", + "symfony/ai-azure-platform": "^0.9", + "symfony/ai-azure-search-store": "^0.9", + "symfony/ai-bedrock-platform": "^0.9", + "symfony/ai-cache-message-store": "^0.9", + "symfony/ai-cache-platform": "^0.9", + "symfony/ai-cache-store": "^0.9", + "symfony/ai-cartesia-platform": "^0.9", + "symfony/ai-cerebras-platform": "^0.9", + "symfony/ai-chat": "^0.9", + "symfony/ai-chroma-db-store": "^0.9", + "symfony/ai-click-house-store": "^0.9", + "symfony/ai-cloudflare-message-store": "^0.9", + "symfony/ai-cloudflare-store": "^0.9", + "symfony/ai-cohere-platform": "^0.9", + "symfony/ai-decart-platform": "^0.9", + "symfony/ai-deep-seek-platform": "^0.9", + "symfony/ai-docker-model-runner-platform": "^0.9", + "symfony/ai-doctrine-message-store": "^0.9", + "symfony/ai-elasticsearch-store": "^0.9", + "symfony/ai-eleven-labs-platform": "^0.9", + "symfony/ai-failover-platform": "^0.9", + "symfony/ai-gemini-platform": "^0.9", + "symfony/ai-generic-platform": "^0.9", + "symfony/ai-hugging-face-platform": "^0.9", + "symfony/ai-lm-studio-platform": "^0.9", + "symfony/ai-manticore-search-store": "^0.9", + "symfony/ai-maria-db-store": "^0.9", + "symfony/ai-meilisearch-message-store": "^0.9", + "symfony/ai-meilisearch-store": "^0.9", + "symfony/ai-meta-platform": "^0.9", + "symfony/ai-milvus-store": "^0.9", + "symfony/ai-mistral-platform": "^0.9", + "symfony/ai-mongo-db-message-store": "^0.9", + "symfony/ai-mongo-db-store": "^0.9", + "symfony/ai-neo4j-store": "^0.9", + "symfony/ai-ollama-platform": "^0.9", + "symfony/ai-open-ai-platform": "^0.9", + "symfony/ai-open-responses-platform": "^0.9", + "symfony/ai-open-router-platform": "^0.9", + "symfony/ai-open-search-store": "^0.9", + "symfony/ai-ovh-platform": "^0.9", + "symfony/ai-perplexity-platform": "^0.9", + "symfony/ai-pinecone-store": "^0.9", + "symfony/ai-pogocache-message-store": "^0.9", + "symfony/ai-postgres-store": "^0.9", + "symfony/ai-qdrant-store": "^0.9", + "symfony/ai-redis-message-store": "^0.9", + "symfony/ai-redis-store": "^0.9", + "symfony/ai-replicate-platform": "^0.9", + "symfony/ai-s3vectors-store": "^0.9", + "symfony/ai-scaleway-platform": "^0.9", + "symfony/ai-session-message-store": "^0.9", + "symfony/ai-sqlite-store": "^0.9", + "symfony/ai-store": "^0.9", + "symfony/ai-supabase-store": "^0.9", + "symfony/ai-surreal-db-message-store": "^0.9", + "symfony/ai-surreal-db-store": "^0.9", + "symfony/ai-transformers-php-platform": "^0.9", + "symfony/ai-typesense-store": "^0.9", + "symfony/ai-vektor-store": "^0.9", + "symfony/ai-vertex-ai-platform": "^0.9", + "symfony/ai-voyage-platform": "^0.9", + "symfony/ai-weaviate-store": "^0.9", + "symfony/expression-language": "^7.3|^8.0", + "symfony/security-core": "^7.3|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/validator": "^7.3|^8.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\AiBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration bundle for Symfony AI components", + "support": { + "source": "https://github.com/symfony/ai-bundle/tree/v0.9.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": "2026-05-16T08:40:45+00:00" + }, + { + "name": "symfony/ai-generic-platform", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-generic-platform.git", + "reference": "8887d12b8ea97d079c5c97de4aebb19f42c58dc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-generic-platform/zipball/8887d12b8ea97d079c5c97de4aebb19f42c58dc5", + "reference": "8887d12b8ea97d079c5c97de4aebb19f42c58dc5", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-platform": "^0.9", + "symfony/http-client": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53" + }, + "type": "symfony-ai-platform", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\Bridge\\Generic\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic platform bridge for Symfony AI", + "keywords": [ + "Bridge", + "ai", + "generic", + "platform" + ], + "support": { + "source": "https://github.com/symfony/ai-generic-platform/tree/v0.9.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": "2026-05-16T01:01:33+00:00" + }, + { + "name": "symfony/ai-lm-studio-platform", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-lm-studio-platform.git", + "reference": "9e53e56c8c3a04dddb955088b40904e747ec3981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-lm-studio-platform/zipball/9e53e56c8c3a04dddb955088b40904e747ec3981", + "reference": "9e53e56c8c3a04dddb955088b40904e747ec3981", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-generic-platform": "^0.9", + "symfony/ai-platform": "^0.9", + "symfony/http-client": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53" + }, + "type": "symfony-ai-platform", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\Bridge\\LmStudio\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "LM Studio platform bridge for Symfony AI", + "keywords": [ + "Bridge", + "ai", + "lmstudio", + "local", + "platform" + ], + "support": { + "source": "https://github.com/symfony/ai-lm-studio-platform/tree/v0.9.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": "2026-05-16T01:01:33+00:00" + }, + { + "name": "symfony/ai-open-router-platform", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-open-router-platform.git", + "reference": "7e2b560c86f618cd5d33f9f0c581d83bebc9802f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-open-router-platform/zipball/7e2b560c86f618cd5d33f9f0c581d83bebc9802f", + "reference": "7e2b560c86f618cd5d33f9f0c581d83bebc9802f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-generic-platform": "^0.9", + "symfony/ai-platform": "^0.9", + "symfony/http-client": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53", + "symfony/console": "^7.4|^8.0" + }, + "type": "symfony-ai-platform", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\Bridge\\OpenRouter\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "OpenRouter platform bridge for Symfony AI", + "keywords": [ + "Bridge", + "OpenRouter", + "ai", + "platform" + ], + "support": { + "source": "https://github.com/symfony/ai-open-router-platform/tree/v0.9.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": "2026-05-16T01:01:33+00:00" + }, + { + "name": "symfony/ai-platform", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-platform.git", + "reference": "fb55ebdf20bbe30af6752a0ce6a25abc56b2b625" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-platform/zipball/fb55ebdf20bbe30af6752a0ce6a25abc56b2b625", + "reference": "fb55ebdf20bbe30af6752a0ce6a25abc56b2b625", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "oskarstark/enum-helper": "^1.5", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.4|^6.0", + "phpstan/phpdoc-parser": "^2.1", + "psr/log": "^3.0", + "symfony/clock": "^7.3|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/property-access": "^7.3|^8.0", + "symfony/property-info": "^7.3|^8.0", + "symfony/serializer": "^7.3|^8.0", + "symfony/type-info": "^7.3|^8.0", + "symfony/uid": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53", + "symfony/cache": "^7.3|^8.0", + "symfony/console": "^7.3|^8.0", + "symfony/dotenv": "^7.3|^8.0", + "symfony/expression-language": "^7.3|^8.0", + "symfony/finder": "^7.3|^8.0", + "symfony/http-client": "^7.3|^8.0", + "symfony/http-client-contracts": "^3.5", + "symfony/intl": "^7.3|^8.0", + "symfony/process": "^7.3|^8.0", + "symfony/validator": "^7.3|^8.0", + "symfony/var-dumper": "^7.3|^8.0", + "symfony/yaml": "^7.3|^8.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PHP library for interacting with AI platform provider.", + "keywords": [ + "Gemini", + "OpenRouter", + "ai", + "aimlapi", + "albert", + "amazeeai", + "anthropic", + "azure", + "bedrock", + "cerebras", + "dockermodelrunner", + "elevenlabs", + "huggingface", + "inference", + "litellm", + "llama", + "lmstudio", + "meta", + "mistral", + "nova", + "ollama", + "openai", + "ovh", + "perplexity", + "replicate", + "speech", + "transformers", + "vertexai", + "voyage" + ], + "support": { + "source": "https://github.com/symfony/ai-platform/tree/v0.9.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": "2026-05-15T19:15:50+00:00" }, { "name": "symfony/apache-pack", @@ -10263,16 +11304,16 @@ }, { "name": "symfony/asset", - "version": "v7.3.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/asset.git", - "reference": "56c4d9f759247c4e07d8549e3baf7493cb9c3e4b" + "reference": "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/asset/zipball/56c4d9f759247c4e07d8549e3baf7493cb9c3e4b", - "reference": "56c4d9f759247c4e07d8549e3baf7493cb9c3e4b", + "url": "https://api.github.com/repos/symfony/asset/zipball/d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", + "reference": "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", "shasum": "" }, "require": { @@ -10282,9 +11323,9 @@ "symfony/http-foundation": "<6.4" }, "require-dev": { - "symfony/http-client": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10312,7 +11353,7 @@ "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/asset/tree/v7.3.0" + "source": "https://github.com/symfony/asset/tree/v7.4.8" }, "funding": [ { @@ -10323,25 +11364,29 @@ "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-03-05T10:15:41+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/cache", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f" + "reference": "902d621e0b6ef0ebeaa133770b5c339a19328589" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f", - "reference": "bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f", + "url": "https://api.github.com/repos/symfony/cache/zipball/902d621e0b6ef0ebeaa133770b5c339a19328589", + "reference": "902d621e0b6ef0ebeaa133770b5c339a19328589", "shasum": "" }, "require": { @@ -10349,12 +11394,14 @@ "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^3.6", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "conflict": { "doctrine/dbal": "<3.6", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1", "symfony/dependency-injection": "<6.4", "symfony/http-kernel": "<6.4", "symfony/var-dumper": "<6.4" @@ -10369,13 +11416,13 @@ "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/filesystem": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10410,7 +11457,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.3.4" + "source": "https://github.com/symfony/cache/tree/v7.4.12" }, "funding": [ { @@ -10430,20 +11477,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + "reference": "225e8a254166bd3442e370c6f50145465db63831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", - "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/225e8a254166bd3442e370c6f50145465db63831", + "reference": "225e8a254166bd3442e370c6f50145465db63831", "shasum": "" }, "require": { @@ -10457,7 +11504,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -10490,7 +11537,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.0" }, "funding": [ { @@ -10501,25 +11548,29 @@ "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-03-13T15:25:07+00:00" + "time": "2026-05-05T15:33:14+00:00" }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", "shasum": "" }, "require": { @@ -10564,7 +11615,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v7.4.8" }, "funding": [ { @@ -10575,31 +11626,35 @@ "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-25T14:21:43+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/config", - "version": "v7.3.4", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "8a09223170046d2cfda3d2e11af01df2c641e961" + "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/8a09223170046d2cfda3d2e11af01df2c641e961", - "reference": "8a09223170046d2cfda3d2e11af01df2c641e961", + "url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^7.1", + "symfony/filesystem": "^7.1|^8.0", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -10607,11 +11662,11 @@ "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10639,7 +11694,7 @@ "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/v7.3.4" + "source": "https://github.com/symfony/config/tree/v7.4.10" }, "funding": [ { @@ -10659,20 +11714,20 @@ "type": "tidelift" } ], - "time": "2025-09-22T12:46:16+00:00" + "time": "2026-05-03T14:20:49+00:00" }, { "name": "symfony/console", - "version": "v7.3.4", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" + "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "url": "https://api.github.com/repos/symfony/console/zipball/ed0107e43ab452aa77ae99e005b95e56b556e075", + "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075", "shasum": "" }, "require": { @@ -10680,7 +11735,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -10694,16 +11749,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10737,7 +11792,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.4" + "source": "https://github.com/symfony/console/tree/v7.4.11" }, "funding": [ { @@ -10757,20 +11812,20 @@ "type": "tidelift" } ], - "time": "2025-09-22T15:31:00+00:00" + "time": "2026-05-13T12:04:42+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.0", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b75663ed96cf4756e28e3105476f220f92886cc4", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4", "shasum": "" }, "require": { @@ -10806,7 +11861,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + "source": "https://github.com/symfony/css-selector/tree/v7.4.9" }, "funding": [ { @@ -10817,33 +11872,37 @@ "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-25T14:21:43+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.3.4", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "82119812ab0bf3425c1234d413efd1b19bb92ae4" + "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/82119812ab0bf3425c1234d413efd1b19bb92ae4", - "reference": "82119812ab0bf3425c1234d413efd1b19bb92ae4", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", + "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", "shasum": "" }, "require": { "php": ">=8.2", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^3.5", - "symfony/var-exporter": "^6.4.20|^7.2.5" + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -10856,9 +11915,9 @@ "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -10886,7 +11945,7 @@ "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/v7.3.4" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.10" }, "funding": [ { @@ -10906,20 +11965,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-05-06T11:55:30+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -10932,7 +11991,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -10957,7 +12016,7 @@ "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" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -10968,25 +12027,29 @@ "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-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v7.3.4", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "21cd48c34a47a0d0e303a590a67c3450fde55888" + "reference": "7a87c85853f3069e3657a823c62b02952de46b0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/21cd48c34a47a0d0e303a590a67c3450fde55888", - "reference": "21cd48c34a47a0d0e303a590a67c3450fde55888", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/7a87c85853f3069e3657a823c62b02952de46b0a", + "reference": "7a87c85853f3069e3657a823c62b02952de46b0a", "shasum": "" }, "require": { @@ -11013,7 +12076,7 @@ "symfony/property-info": "<6.4", "symfony/security-bundle": "<6.4", "symfony/security-core": "<6.4", - "symfony/validator": "<6.4" + "symfony/validator": "<7.4" }, "require-dev": { "doctrine/collections": "^1.8|^2.0", @@ -11021,24 +12084,24 @@ "doctrine/dbal": "^3.6|^4", "doctrine/orm": "^2.15|^3", "psr/log": "^1|^2|^3", - "symfony/cache": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/doctrine-messenger": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/form": "^6.4.6|^7.0.6", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", - "symfony/type-info": "^7.1.8", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/doctrine-messenger": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.2|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "symfony-bridge", "autoload": { @@ -11066,7 +12129,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v7.3.4" + "source": "https://github.com/symfony/doctrine-bridge/tree/v7.4.9" }, "funding": [ { @@ -11086,30 +12149,31 @@ "type": "tidelift" } ], - "time": "2025-09-24T09:56:23+00:00" + "time": "2026-04-29T14:19:39+00:00" }, { "name": "symfony/dom-crawler", - "version": "v7.3.3", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "efa076ea0eeff504383ff0dcf827ea5ce15690ba" + "reference": "b59b59122690976550fd142c23fab62c84738db6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/efa076ea0eeff504383ff0dcf827ea5ce15690ba", - "reference": "efa076ea0eeff504383ff0dcf827ea5ce15690ba", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b59b59122690976550fd142c23fab62c84738db6", + "reference": "b59b59122690976550fd142c23fab62c84738db6", "shasum": "" }, "require": { "masterminds/html5": "^2.6", "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { - "symfony/css-selector": "^6.4|^7.0" + "symfony/css-selector": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -11137,7 +12201,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.3.3" + "source": "https://github.com/symfony/dom-crawler/tree/v7.4.12" }, "funding": [ { @@ -11157,20 +12221,20 @@ "type": "tidelift" } ], - "time": "2025-08-06T20:13:54+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/dotenv", - "version": "v7.3.2", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "2192790a11f9e22cbcf9dc705a3ff22a5503923a" + "reference": "82e9b1355c68ef7b96397dbd34cc75a92eebae7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/2192790a11f9e22cbcf9dc705a3ff22a5503923a", - "reference": "2192790a11f9e22cbcf9dc705a3ff22a5503923a", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/82e9b1355c68ef7b96397dbd34cc75a92eebae7c", + "reference": "82e9b1355c68ef7b96397dbd34cc75a92eebae7c", "shasum": "" }, "require": { @@ -11181,8 +12245,8 @@ "symfony/process": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -11215,7 +12279,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v7.3.2" + "source": "https://github.com/symfony/dotenv/tree/v7.4.11" }, "funding": [ { @@ -11235,36 +12299,37 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:29:33+00:00" + "time": "2026-05-11T13:02:51+00:00" }, { "name": "symfony/error-handler", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4" + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -11296,7 +12361,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.4" + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" }, "funding": [ { @@ -11316,20 +12381,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", "shasum": "" }, "require": { @@ -11346,13 +12411,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -11380,7 +12446,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" }, "funding": [ { @@ -11400,20 +12466,20 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { @@ -11427,7 +12493,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -11460,7 +12526,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -11471,30 +12537,34 @@ "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-25T14:21:43+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/expression-language", - "version": "v7.3.2", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "32d2d19c62e58767e6552166c32fb259975d2b23" + "reference": "87ff95687748f4af65e4d5a6e917d448ec52aa83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/32d2d19c62e58767e6552166c32fb259975d2b23", - "reference": "32d2d19c62e58767e6552166c32fb259975d2b23", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/87ff95687748f4af65e4d5a6e917d448ec52aa83", + "reference": "87ff95687748f4af65e4d5a6e917d448ec52aa83", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/cache": "^6.4|^7.0", + "symfony/cache": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3" }, @@ -11524,7 +12594,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v7.3.2" + "source": "https://github.com/symfony/expression-language/tree/v7.4.8" }, "funding": [ { @@ -11544,20 +12614,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:29:33+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/filesystem", - "version": "v7.3.2", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", "shasum": "" }, "require": { @@ -11566,7 +12636,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -11594,7 +12664,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -11614,27 +12684,27 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:47+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "e0be088d22278583a82da281886e8c3592fbf149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -11662,7 +12732,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.2" + "source": "https://github.com/symfony/finder/tree/v7.4.8" }, "funding": [ { @@ -11682,35 +12752,36 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/flex", - "version": "v2.8.2", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "f356aa35f3cf3d2f46c31d344c1098eb2d260426" + "reference": "9cd384775973eabbf6e8b05784dda279fc67c28d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/f356aa35f3cf3d2f46c31d344c1098eb2d260426", - "reference": "f356aa35f3cf3d2f46c31d344c1098eb2d260426", + "url": "https://api.github.com/repos/symfony/flex/zipball/9cd384775973eabbf6e8b05784dda279fc67c28d", + "reference": "9cd384775973eabbf6e8b05784dda279fc67c28d", "shasum": "" }, "require": { "composer-plugin-api": "^2.1", - "php": ">=8.0" + "php": ">=8.1" }, "conflict": { - "composer/semver": "<1.7.2" + "composer/semver": "<1.7.2", + "symfony/dotenv": "<5.4" }, "require-dev": { "composer/composer": "^2.1", - "symfony/dotenv": "^5.4|^6.0", - "symfony/filesystem": "^5.4|^6.0", - "symfony/phpunit-bridge": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0" + "symfony/dotenv": "^6.4|^7.4|^8.0", + "symfony/filesystem": "^6.4|^7.4|^8.0", + "symfony/phpunit-bridge": "^6.4|^7.4|^8.0", + "symfony/process": "^6.4|^7.4|^8.0" }, "type": "composer-plugin", "extra": { @@ -11734,7 +12805,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.8.2" + "source": "https://github.com/symfony/flex/tree/v2.10.0" }, "funding": [ { @@ -11754,31 +12825,31 @@ "type": "tidelift" } ], - "time": "2025-08-22T07:17:23+00:00" + "time": "2025-11-16T09:38:19+00:00" }, { "name": "symfony/form", - "version": "v7.3.4", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "7b3eee0f4d4dfd1ff1be70a27474197330c61736" + "reference": "b6c107af659106abec1771d9d7d22da528644d3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/7b3eee0f4d4dfd1ff1be70a27474197330c61736", - "reference": "7b3eee0f4d4dfd1ff1be70a27474197330c61736", + "url": "https://api.github.com/repos/symfony/form/zipball/b6c107af659106abec1771d9d7d22da528644d3a", + "reference": "b6c107af659106abec1771d9d7d22da528644d3a", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/options-resolver": "^7.3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/options-resolver": "^7.3|^8.0", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-icu": "^1.21", "symfony/polyfill-mbstring": "~1.0", - "symfony/property-access": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -11788,26 +12859,28 @@ "symfony/error-handler": "<6.4", "symfony/framework-bundle": "<6.4", "symfony/http-kernel": "<6.4", + "symfony/intl": "<7.4", "symfony/translation": "<6.4.3|>=7.0,<7.0.3", "symfony/translation-contracts": "<2.5", "symfony/twig-bridge": "<6.4" }, "require-dev": { "doctrine/collections": "^1.0|^2.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/html-sanitizer": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/security-csrf": "^6.4|^7.0", - "symfony/translation": "^6.4.3|^7.0.3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4.12|^7.1.5|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -11835,7 +12908,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v7.3.4" + "source": "https://github.com/symfony/form/tree/v7.4.9" }, "funding": [ { @@ -11855,57 +12928,56 @@ "type": "tidelift" } ], - "time": "2025-09-22T15:31:00+00:00" + "time": "2026-04-29T14:39:19+00:00" }, { "name": "symfony/framework-bundle", - "version": "v7.3.4", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "b13e7cec5a144c8dba6f4233a2c53c00bc29e140" + "reference": "637f5cac1ac2698a012b41610215bf366004295f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/b13e7cec5a144c8dba6f4233a2c53c00bc29e140", - "reference": "b13e7cec5a144c8dba6f4233a2c53c00bc29e140", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/637f5cac1ac2698a012b41610215bf366004295f", + "reference": "637f5cac1ac2698a012b41610215bf366004295f", "shasum": "" }, "require": { "composer-runtime-api": ">=2.1", "ext-xml": "*", "php": ">=8.2", - "symfony/cache": "^6.4|^7.0", - "symfony/config": "^7.3", - "symfony/dependency-injection": "^7.2", + "symfony/cache": "^6.4.12|^7.0|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^7.4.4|^8.0.4", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^7.3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/filesystem": "^7.1", - "symfony/finder": "^6.4|^7.0", - "symfony/http-foundation": "^7.3", - "symfony/http-kernel": "^7.2", + "symfony/error-handler": "^7.3|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/routing": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/routing": "^7.4|^8.0" }, "conflict": { "doctrine/persistence": "<1.3", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/asset": "<6.4", "symfony/asset-mapper": "<6.4", "symfony/clock": "<6.4", "symfony/console": "<6.4", "symfony/dom-crawler": "<6.4", "symfony/dotenv": "<6.4", - "symfony/form": "<6.4", + "symfony/form": "<7.4", "symfony/http-client": "<6.4", - "symfony/json-streamer": ">=7.4", "symfony/lock": "<6.4", "symfony/mailer": "<6.4", - "symfony/messenger": "<6.4", - "symfony/mime": "<6.4", - "symfony/object-mapper": ">=7.4", + "symfony/messenger": "<7.4", + "symfony/mime": "<6.4.37|>=7.0,<7.4.9|>=8.0,<8.0.9", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", @@ -11920,51 +12992,52 @@ "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", "symfony/webhook": "<7.2", - "symfony/workflow": "<7.3.0-beta2" + "symfony/workflow": "<7.4" }, "require-dev": { "doctrine/persistence": "^1.3|^2|^3", "dragonmantank/cron-expression": "^3.1", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", "seld/jsonlint": "^1.10", - "symfony/asset": "^6.4|^7.0", - "symfony/asset-mapper": "^6.4|^7.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/dotenv": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", - "symfony/html-sanitizer": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/json-streamer": "7.3.*", - "symfony/lock": "^6.4|^7.0", - "symfony/mailer": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/notifier": "^6.4|^7.0", - "symfony/object-mapper": "^v7.3.0-beta2", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/json-streamer": "^7.3|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^6.4.37|^7.4.9|^8.0.9", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/object-mapper": "^7.3|^8.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/scheduler": "^6.4.4|^7.0.4", - "symfony/security-bundle": "^6.4|^7.0", - "symfony/semaphore": "^6.4|^7.0", - "symfony/serializer": "^7.2.5", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/string": "^6.4|^7.0", - "symfony/translation": "^7.3", - "symfony/twig-bundle": "^6.4|^7.0", - "symfony/type-info": "^7.1.8", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/web-link": "^6.4|^7.0", - "symfony/webhook": "^7.2", - "symfony/workflow": "^7.3", - "symfony/yaml": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/scheduler": "^6.4.4|^7.0.4|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/semaphore": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.2.5|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/webhook": "^7.2|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.3|^8.0", "twig/twig": "^3.12" }, "type": "symfony-bundle", @@ -11993,7 +13066,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v7.3.4" + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.11" }, "funding": [ { @@ -12013,20 +13086,20 @@ "type": "tidelift" } ], - "time": "2025-09-17T05:51:54+00:00" + "time": "2026-05-13T12:04:42+00:00" }, { "name": "symfony/http-client", - "version": "v7.3.4", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" + "reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", - "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "url": "https://api.github.com/repos/symfony/http-client/zipball/7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6", + "reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6", "shasum": "" }, "require": { @@ -12057,12 +13130,13 @@ "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12093,7 +13167,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.4" + "source": "https://github.com/symfony/http-client/tree/v7.4.9" }, "funding": [ { @@ -12113,20 +13187,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-04-29T13:25:15+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "75d7043853a42837e68111812f4d964b01e5101c" + "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", - "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", + "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", "shasum": "" }, "require": { @@ -12139,7 +13213,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -12175,7 +13249,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.0" }, "funding": [ { @@ -12186,32 +13260,35 @@ "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-04-29T11:18:49+00:00" + "time": "2026-03-06T13:17:50+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6" + "reference": "9381209597ec66c25be154cbf2289076e64d1eab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6", - "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -12220,13 +13297,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12254,7 +13331,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.4" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" }, "funding": [ { @@ -12274,29 +13351,29 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "b796dffea7821f035047235e076b60ca2446e3cf" + "reference": "7922b53e70d2ba2027af8bb6a59d91eb3541ea4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf", - "reference": "b796dffea7821f035047235e076b60ca2446e3cf", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7922b53e70d2ba2027af8bb6a59d91eb3541ea4d", + "reference": "7922b53e70d2ba2027af8bb6a59d91eb3541ea4d", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -12306,6 +13383,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -12323,27 +13401,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -12372,7 +13450,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.12" }, "funding": [ { @@ -12392,20 +13470,20 @@ "type": "tidelift" } ], - "time": "2025-09-27T12:32:17+00:00" + "time": "2026-05-20T09:27:11+00:00" }, { "name": "symfony/intl", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "e6db84864655885d9dac676a9d7dde0d904fda54" + "reference": "7cfb7792d580dea833647420afd5f2f98df8457b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/e6db84864655885d9dac676a9d7dde0d904fda54", - "reference": "e6db84864655885d9dac676a9d7dde0d904fda54", + "url": "https://api.github.com/repos/symfony/intl/zipball/7cfb7792d580dea833647420afd5f2f98df8457b", + "reference": "7cfb7792d580dea833647420afd5f2f98df8457b", "shasum": "" }, "require": { @@ -12416,8 +13494,8 @@ "symfony/string": "<7.1" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12462,7 +13540,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v7.3.4" + "source": "https://github.com/symfony/intl/tree/v7.4.8" }, "funding": [ { @@ -12482,20 +13560,20 @@ "type": "tidelift" } ], - "time": "2025-09-08T14:11:30+00:00" + "time": "2026-03-30T12:55:43+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "ab97ef2f7acf0216955f5845484235113047a31d" + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/ab97ef2f7acf0216955f5845484235113047a31d", - "reference": "ab97ef2f7acf0216955f5845484235113047a31d", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", "shasum": "" }, "require": { @@ -12503,8 +13581,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -12515,10 +13593,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12546,7 +13624,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.4" + "source": "https://github.com/symfony/mailer/tree/v7.4.12" }, "funding": [ { @@ -12566,43 +13644,44 @@ "type": "tidelift" } ], - "time": "2025-09-17T05:51:54+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/mime", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + "reference": "b198dd66c211c97119bcaaff7c13431dbbb5e470" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "url": "https://api.github.com/repos/symfony/mime/zipball/b198dd66c211c97119bcaaff7c13431dbbb5e470", + "reference": "b198dd66c211c97119bcaaff7c13431dbbb5e470", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -12634,7 +13713,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.4" + "source": "https://github.com/symfony/mime/tree/v7.4.12" }, "funding": [ { @@ -12654,26 +13733,27 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/monolog-bridge", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "7acf2abe23e5019451399ba69fc8ed3d61d4d8f0" + "reference": "20bb2345ac7a9dd57724b6b7ada92c6d7d67b4b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/7acf2abe23e5019451399ba69fc8ed3d61d4d8f0", - "reference": "7acf2abe23e5019451399ba69fc8ed3d61d4d8f0", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/20bb2345ac7a9dd57724b6b7ada92c6d7d67b4b8", + "reference": "20bb2345ac7a9dd57724b6b7ada92c6d7d67b4b8", "shasum": "" }, "require": { "monolog/monolog": "^3", "php": ">=8.2", - "symfony/http-kernel": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -12682,13 +13762,13 @@ "symfony/security-core": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/mailer": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "symfony-bridge", "autoload": { @@ -12716,7 +13796,7 @@ "description": "Provides integration for Monolog with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/monolog-bridge/tree/v7.3.4" + "source": "https://github.com/symfony/monolog-bridge/tree/v7.4.12" }, "funding": [ { @@ -12736,48 +13816,42 @@ "type": "tidelift" } ], - "time": "2025-09-24T16:45:39+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/monolog-bundle", - "version": "v3.10.0", + "version": "v4.0.2", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bundle.git", - "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + "reference": "c012c6aba13129eb02aa7dd61e66e720911d8598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", - "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/c012c6aba13129eb02aa7dd61e66e720911d8598", + "reference": "c012c6aba13129eb02aa7dd61e66e720911d8598", "shasum": "" }, "require": { - "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", - "php": ">=7.2.5", - "symfony/config": "^5.4 || ^6.0 || ^7.0", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", - "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", - "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + "composer-runtime-api": "^2.0", + "monolog/monolog": "^3.5", + "php": ">=8.2", + "symfony/config": "^7.3 || ^8.0", + "symfony/dependency-injection": "^7.3 || ^8.0", + "symfony/http-kernel": "^7.3 || ^8.0", + "symfony/monolog-bridge": "^7.3 || ^8.0", + "symfony/polyfill-php84": "^1.30" }, "require-dev": { - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/phpunit-bridge": "^6.3 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + "phpunit/phpunit": "^11.5.41 || ^12.3", + "symfony/console": "^7.3 || ^8.0", + "symfony/yaml": "^7.3 || ^8.0" }, "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Bundle\\MonologBundle\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Bundle\\MonologBundle\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -12801,7 +13875,7 @@ ], "support": { "issues": "https://github.com/symfony/monolog-bundle/issues", - "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + "source": "https://github.com/symfony/monolog-bundle/tree/v4.0.2" }, "funding": [ { @@ -12812,25 +13886,29 @@ "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": "2023-11-06T17:08:13+00:00" + "time": "2026-04-02T18:27:21+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", "shasum": "" }, "require": { @@ -12868,7 +13946,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v7.4.8" }, "funding": [ { @@ -12888,20 +13966,20 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/password-hasher", - "version": "v7.3.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/password-hasher.git", - "reference": "31fbe66af859582a20b803f38be96be8accdf2c3" + "reference": "18a7d92126c95962f7efbcc9e421ba710a366847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/password-hasher/zipball/31fbe66af859582a20b803f38be96be8accdf2c3", - "reference": "31fbe66af859582a20b803f38be96be8accdf2c3", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/18a7d92126c95962f7efbcc9e421ba710a366847", + "reference": "18a7d92126c95962f7efbcc9e421ba710a366847", "shasum": "" }, "require": { @@ -12911,8 +13989,8 @@ "symfony/security-core": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/security-core": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -12944,7 +14022,7 @@ "password" ], "support": { - "source": "https://github.com/symfony/password-hasher/tree/v7.3.0" + "source": "https://github.com/symfony/password-hasher/tree/v7.4.8" }, "funding": [ { @@ -12955,25 +14033,29 @@ "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-02-04T08:22:58+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -13023,7 +14105,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -13043,20 +14125,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "9c862df890f7c833b1101ac5578ec4dcf199efb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/9c862df890f7c833b1101ac5578ec4dcf199efb5", + "reference": "9c862df890f7c833b1101ac5578ec4dcf199efb5", "shasum": "" }, "require": { @@ -13105,7 +14187,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.0" }, "funding": [ { @@ -13125,20 +14207,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-05-25T12:39:52+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" + "reference": "445c90e341fccda10311019cf82ff73bb7343945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", - "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/445c90e341fccda10311019cf82ff73bb7343945", + "reference": "445c90e341fccda10311019cf82ff73bb7343945", "shasum": "" }, "require": { @@ -13193,7 +14275,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.38.0" }, "funding": [ { @@ -13213,11 +14295,11 @@ "type": "tidelift" } ], - "time": "2025-06-20T22:24:30+00:00" + "time": "2026-05-25T11:52:53+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -13280,7 +14362,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" }, "funding": [ { @@ -13304,16 +14386,16 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -13365,7 +14447,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -13385,269 +14467,20 @@ "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/polyfill-php80", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/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-01-02T08:10:11+00:00" - }, - { - "name": "symfony/polyfill-php82", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php82.git", - "reference": "5d2ed36f7734637dacc025f179698031951b1692" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", - "reference": "5d2ed36f7734637dacc025f179698031951b1692", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php82\\": "" - }, - "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 backporting some PHP 8.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php82/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" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + "reference": "c4406e07046227db8844a25f89d111da078aaa9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/c4406e07046227db8844a25f89d111da078aaa9c", + "reference": "c4406e07046227db8844a25f89d111da078aaa9c", "shasum": "" }, "require": { @@ -13694,7 +14527,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.0" }, "funding": [ { @@ -13714,20 +14547,20 @@ "type": "tidelift" } ], - "time": "2025-07-08T02:45:35+00:00" + "time": "2026-05-25T12:39:52+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "a0e0aca0368801ec79f8791dea9a7c12af527c93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/a0e0aca0368801ec79f8791dea9a7c12af527c93", + "reference": "a0e0aca0368801ec79f8791dea9a7c12af527c93", "shasum": "" }, "require": { @@ -13774,7 +14607,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.0" }, "funding": [ { @@ -13794,20 +14627,100 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-05-25T12:12:52+00:00" }, { - "name": "symfony/polyfill-uuid", - "version": "v1.33.0", + "name": "symfony/polyfill-php85", + "version": "v1.38.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "3e851ffb42624b64b3b9ec234a0e2074b0f880e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/3e851ffb42624b64b3b9ec234a0e2074b0f880e3", + "reference": "3e851ffb42624b64b3b9ec234a0e2074b0f880e3", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "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 backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.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": "2026-05-25T11:50:50+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", "shasum": "" }, "require": { @@ -13857,7 +14770,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" }, "funding": [ { @@ -13877,20 +14790,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/d9593c9efa40499eb078b81144de42cbc28a31f0", + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0", "shasum": "" }, "require": { @@ -13922,7 +14835,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.11" }, "funding": [ { @@ -13942,28 +14855,29 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-05-11T16:55:21+00:00" }, { "name": "symfony/property-access", - "version": "v7.3.3", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "4a4389e5c8bd1d0320d80a23caa6a1ac71cb81a7" + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/4a4389e5c8bd1d0320d80a23caa6a1ac71cb81a7", - "reference": "4a4389e5c8bd1d0320d80a23caa6a1ac71cb81a7", + "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/property-info": "^6.4|^7.0" + "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" }, "require-dev": { - "symfony/cache": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" }, "type": "library", "autoload": { @@ -14002,7 +14916,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.3.3" + "source": "https://github.com/symfony/property-access/tree/v7.4.8" }, "funding": [ { @@ -14022,41 +14936,41 @@ "type": "tidelift" } ], - "time": "2025-08-04T15:15:28+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/property-info", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "7b6db23f23d13ada41e1cb484748a8ec028fbace" + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/7b6db23f23d13ada41e1cb484748a8ec028fbace", - "reference": "7b6db23f23d13ada41e1cb484748a8ec028fbace", + "url": "https://api.github.com/repos/symfony/property-info/zipball/ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0", - "symfony/type-info": "~7.2.8|^7.3.1" + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" }, "conflict": { - "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/reflection-docblock": "<5.2|>=7", "phpdocumentor/type-resolver": "<1.5.1", "symfony/cache": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/serializer": "<6.4" }, "require-dev": { - "phpdocumentor/reflection-docblock": "^5.2", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", "phpstan/phpdoc-parser": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -14092,7 +15006,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.3.4" + "source": "https://github.com/symfony/property-info/tree/v7.4.8" }, "funding": [ { @@ -14112,26 +15026,26 @@ "type": "tidelift" } ], - "time": "2025-09-15T13:55:54+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v7.3.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f" + "reference": "76f1a57719a4a04c0ea18678a6c9305b5dcb9da8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", - "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/76f1a57719a4a04c0ea18678a6c9305b5dcb9da8", + "reference": "76f1a57719a4a04c0ea18678a6c9305b5dcb9da8", "shasum": "" }, "require": { "php": ">=8.2", "psr/http-message": "^1.0|^2.0", - "symfony/http-foundation": "^6.4|^7.0" + "symfony/http-foundation": "^6.4|^7.0|^8.0" }, "conflict": { "php-http/discovery": "<1.15", @@ -14141,11 +15055,12 @@ "nyholm/psr7": "^1.1", "php-http/discovery": "^1.15", "psr/log": "^1.1.4|^2|^3", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" }, "type": "symfony-bridge", "autoload": { @@ -14179,7 +15094,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.3.0" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.8" }, "funding": [ { @@ -14190,34 +15105,38 @@ "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-26T08:57:56+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/rate-limiter", - "version": "v7.3.2", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/rate-limiter.git", - "reference": "7e855541d302ba752f86fb0e97932e7969fe9c04" + "reference": "778c5239c7fd6bf9b886dedf3d84ddb156ddb888" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/7e855541d302ba752f86fb0e97932e7969fe9c04", - "reference": "7e855541d302ba752f86fb0e97932e7969fe9c04", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/778c5239c7fd6bf9b886dedf3d84ddb156ddb888", + "reference": "778c5239c7fd6bf9b886dedf3d84ddb156ddb888", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/options-resolver": "^7.3" + "symfony/options-resolver": "^7.3|^8.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/lock": "^6.4|^7.0" + "symfony/lock": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -14249,7 +15168,7 @@ "rate-limiter" ], "support": { - "source": "https://github.com/symfony/rate-limiter/tree/v7.3.2" + "source": "https://github.com/symfony/rate-limiter/tree/v7.4.10" }, "funding": [ { @@ -14269,20 +15188,20 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:57+00:00" + "time": "2026-05-04T13:25:50+00:00" }, { "name": "symfony/routing", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c" + "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c", + "url": "https://api.github.com/repos/symfony/routing/zipball/3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204", + "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204", "shasum": "" }, "require": { @@ -14296,11 +15215,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -14334,7 +15253,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.4" + "source": "https://github.com/symfony/routing/tree/v7.4.12" }, "funding": [ { @@ -14354,20 +15273,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/runtime", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "3550e2711e30bfa5d808514781cd52d1cc1d9e9f" + "reference": "0b032fa77359745db793df5aff626779180c5f3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/3550e2711e30bfa5d808514781cd52d1cc1d9e9f", - "reference": "3550e2711e30bfa5d808514781cd52d1cc1d9e9f", + "url": "https://api.github.com/repos/symfony/runtime/zipball/0b032fa77359745db793df5aff626779180c5f3b", + "reference": "0b032fa77359745db793df5aff626779180c5f3b", "shasum": "" }, "require": { @@ -14379,10 +15298,11 @@ }, "require-dev": { "composer/composer": "^2.6", - "symfony/console": "^6.4|^7.0", - "symfony/dotenv": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0" }, "type": "composer-plugin", "extra": { @@ -14417,7 +15337,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v7.3.4" + "source": "https://github.com/symfony/runtime/tree/v7.4.12" }, "funding": [ { @@ -14437,36 +15357,37 @@ "type": "tidelift" } ], - "time": "2025-09-11T15:31:28+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/security-bundle", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "f750d9abccbeaa433c56f6a4eb2073166476a75a" + "reference": "6f6f859b437fb95028addfa21b417d25daca86d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/f750d9abccbeaa433c56f6a4eb2073166476a75a", - "reference": "f750d9abccbeaa433c56f6a4eb2073166476a75a", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/6f6f859b437fb95028addfa21b417d25daca86d5", + "reference": "6f6f859b437fb95028addfa21b417d25daca86d5", "shasum": "" }, "require": { "composer-runtime-api": ">=2.1", "ext-xml": "*", "php": ">=8.2", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^7.3", - "symfony/dependency-injection": "^6.4.11|^7.1.4", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/password-hasher": "^6.4|^7.0", - "symfony/security-core": "^7.3", - "symfony/security-csrf": "^6.4|^7.0", - "symfony/security-http": "^7.3", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/password-hasher": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -14480,25 +15401,26 @@ "symfony/validator": "<6.4" }, "require-dev": { - "symfony/asset": "^6.4|^7.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/ldap": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0", - "symfony/twig-bundle": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", - "twig/twig": "^3.12", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/ldap": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/twig": "^3.15", "web-token/jwt-library": "^3.3.2|^4.0" }, "type": "symfony-bundle", @@ -14527,7 +15449,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v7.3.4" + "source": "https://github.com/symfony/security-bundle/tree/v7.4.12" }, "funding": [ { @@ -14547,27 +15469,27 @@ "type": "tidelift" } ], - "time": "2025-09-22T15:31:00+00:00" + "time": "2026-05-15T07:14:02+00:00" }, { "name": "symfony/security-core", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "68b9d3ca57615afde6152a1e1441fa035bea43f8" + "reference": "efff84605474ec682c7d9c6278088811e6f3caaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/68b9d3ca57615afde6152a1e1441fa035bea43f8", - "reference": "68b9d3ca57615afde6152a1e1441fa035bea43f8", + "url": "https://api.github.com/repos/symfony/security-core/zipball/efff84605474ec682c7d9c6278088811e6f3caaa", + "reference": "efff84605474ec682c7d9c6278088811e6f3caaa", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher-contracts": "^2.5|^3", - "symfony/password-hasher": "^6.4|^7.0", + "symfony/password-hasher": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -14582,15 +15504,15 @@ "psr/cache": "^1.0|^2.0|^3.0", "psr/container": "^1.1|^2.0", "psr/log": "^1|^2|^3", - "symfony/cache": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/ldap": "^6.4|^7.0", - "symfony/string": "^6.4|^7.0", - "symfony/translation": "^6.4.3|^7.0.3", - "symfony/validator": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/ldap": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -14618,7 +15540,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v7.3.4" + "source": "https://github.com/symfony/security-core/tree/v7.4.12" }, "funding": [ { @@ -14638,33 +15560,33 @@ "type": "tidelift" } ], - "time": "2025-09-24T14:32:13+00:00" + "time": "2026-05-15T06:48:59+00:00" }, { "name": "symfony/security-csrf", - "version": "v7.3.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-csrf.git", - "reference": "2b4b0c46c901729e4e90719eacd980381f53e0a3" + "reference": "16b3aa2f67d02fb0dbd013a8759bbe90daaa9c5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-csrf/zipball/2b4b0c46c901729e4e90719eacd980381f53e0a3", - "reference": "2b4b0c46c901729e4e90719eacd980381f53e0a3", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/16b3aa2f67d02fb0dbd013a8759bbe90daaa9c5d", + "reference": "16b3aa2f67d02fb0dbd013a8759bbe90daaa9c5d", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/security-core": "^6.4|^7.0" + "symfony/security-core": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/http-foundation": "<6.4" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -14692,7 +15614,7 @@ "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-csrf/tree/v7.3.0" + "source": "https://github.com/symfony/security-csrf/tree/v7.4.8" }, "funding": [ { @@ -14703,55 +15625,59 @@ "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-01-02T18:42:10+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/security-http", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "1cf54d0648ebab23bf9b8972617b79f1995e13a9" + "reference": "1fc7ca636cbd2cad29b42cc13c9fd0c681c6efee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/1cf54d0648ebab23bf9b8972617b79f1995e13a9", - "reference": "1cf54d0648ebab23bf9b8972617b79f1995e13a9", + "url": "https://api.github.com/repos/symfony/security-http/zipball/1fc7ca636cbd2cad29b42cc13c9fd0c681c6efee", + "reference": "1fc7ca636cbd2cad29b42cc13c9fd0c681c6efee", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/security-core": "^7.3", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.3|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { "symfony/clock": "<6.4", - "symfony/event-dispatcher": "<6.4", "symfony/http-client-contracts": "<3.0", "symfony/security-bundle": "<6.4", "symfony/security-csrf": "<6.4" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/cache": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^3.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/routing": "^6.4|^7.0", - "symfony/security-csrf": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "web-token/jwt-library": "^3.3.2|^4.0" }, "type": "library", @@ -14780,7 +15706,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v7.3.4" + "source": "https://github.com/symfony/security-http/tree/v7.4.12" }, "funding": [ { @@ -14800,20 +15726,20 @@ "type": "tidelift" } ], - "time": "2025-09-09T17:06:44+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symfony/serializer", - "version": "v7.3.4", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "0df5af266c6fe9a855af7db4fea86e13b9ca3ab1" + "reference": "268c5aa6c4bd675eddd89348e7ecac292a843ddd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/0df5af266c6fe9a855af7db4fea86e13b9ca3ab1", - "reference": "0df5af266c6fe9a855af7db4fea86e13b9ca3ab1", + "url": "https://api.github.com/repos/symfony/serializer/zipball/268c5aa6c4bd675eddd89348e7ecac292a843ddd", + "reference": "268c5aa6c4bd675eddd89348e7ecac292a843ddd", "shasum": "" }, "require": { @@ -14823,39 +15749,40 @@ "symfony/polyfill-php84": "^1.30" }, "conflict": { - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/dependency-injection": "<6.4", - "symfony/property-access": "<6.4", + "symfony/property-access": "<6.4.31|>=7.0,<7.4.2|>=8.0,<8.0.2", "symfony/property-info": "<6.4", + "symfony/type-info": "<7.2.5", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4" }, "require-dev": { - "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", "phpstan/phpdoc-parser": "^1.0|^2.0", "seld/jsonlint": "^1.10", - "symfony/cache": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^7.2", - "symfony/error-handler": "^6.4|^7.0", - "symfony/filesystem": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^7.2|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4.31|^7.4.2|^8.0.2", + "symfony/property-info": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.1.8", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/type-info": "^7.2.5|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -14883,7 +15810,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.3.4" + "source": "https://github.com/symfony/serializer/tree/v7.4.10" }, "funding": [ { @@ -14903,20 +15830,20 @@ "type": "tidelift" } ], - "time": "2025-09-15T13:39:02+00:00" + "time": "2026-05-03T13:03:28+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -14934,7 +15861,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -14970,7 +15897,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -14981,25 +15908,29 @@ "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-04-25T09:37:31+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/stimulus-bundle", - "version": "v2.30.0", + "version": "v2.35.0", "source": { "type": "git", "url": "https://github.com/symfony/stimulus-bundle.git", - "reference": "668b9efe9d0ab8b4e50091263171609e0459c0c8" + "reference": "05af0259f201dbbd15c103bea289989a4b483b5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/668b9efe9d0ab8b4e50091263171609e0459c0c8", - "reference": "668b9efe9d0ab8b4e50091263171609e0459c0c8", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/05af0259f201dbbd15c103bea289989a4b483b5b", + "reference": "05af0259f201dbbd15c103bea289989a4b483b5b", "shasum": "" }, "require": { @@ -15039,7 +15970,7 @@ "symfony-ux" ], "support": { - "source": "https://github.com/symfony/stimulus-bundle/tree/v2.30.0" + "source": "https://github.com/symfony/stimulus-bundle/tree/v2.35.0" }, "funding": [ { @@ -15059,20 +15990,20 @@ "type": "tidelift" } ], - "time": "2025-08-27T15:25:48+00:00" + "time": "2026-03-22T22:21:50+00:00" }, { "name": "symfony/stopwatch", - "version": "v7.3.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89", "shasum": "" }, "require": { @@ -15105,7 +16036,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" + "source": "https://github.com/symfony/stopwatch/tree/v7.4.8" }, "funding": [ { @@ -15116,31 +16047,36 @@ "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-02-24T10:49:57+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", + "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -15148,11 +16084,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -15191,7 +16127,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v7.4.11" }, "funding": [ { @@ -15211,27 +16147,27 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2026-05-13T12:04:42+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", @@ -15250,17 +16186,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -15291,7 +16227,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v7.4.10" }, "funding": [ { @@ -15311,20 +16247,20 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2026-05-06T11:19:24+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", "shasum": "" }, "require": { @@ -15337,7 +16273,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -15373,7 +16309,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" }, "funding": [ { @@ -15384,25 +16320,29 @@ "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-27T08:32:26+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/twig-bridge", - "version": "v7.3.3", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "33558f013b7f6ed72805527c8405cae0062e47c5" + "reference": "81663873d946531129c76c65e80b681ce99c0e89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/33558f013b7f6ed72805527c8405cae0062e47c5", - "reference": "33558f013b7f6ed72805527c8405cae0062e47c5", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/81663873d946531129c76c65e80b681ce99c0e89", + "reference": "81663873d946531129c76c65e80b681ce99c0e89", "shasum": "" }, "require": { @@ -15412,13 +16352,13 @@ "twig/twig": "^3.21" }, "conflict": { - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/console": "<6.4", - "symfony/form": "<6.4", + "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4", "symfony/http-foundation": "<6.4", "symfony/http-kernel": "<6.4", - "symfony/mime": "<6.4", + "symfony/mime": "<6.4.37|>7,<7.4.9|>8.0,<8.0.9", "symfony/serializer": "<6.4", "symfony/translation": "<6.4", "symfony/workflow": "<6.4" @@ -15426,34 +16366,34 @@ "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/asset": "^6.4|^7.0", - "symfony/asset-mapper": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/emoji": "^7.1", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/form": "^6.4.20|^7.2.5", - "symfony/html-sanitizer": "^6.4|^7.0", - "symfony/http-foundation": "^7.3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.3|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4.37|^7.4.9|^8.0.9", "symfony/polyfill-intl-icu": "~1.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/routing": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/security-acl": "^2.8|^3.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/security-csrf": "^6.4|^7.0", - "symfony/security-http": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/web-link": "^6.4|^7.0", - "symfony/workflow": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/workflow": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", "twig/cssinliner-extra": "^3", "twig/inky-extra": "^3", "twig/markdown-extra": "^3" @@ -15484,7 +16424,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v7.3.3" + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.12" }, "funding": [ { @@ -15504,30 +16444,31 @@ "type": "tidelift" } ], - "time": "2025-08-18T13:10:53+00:00" + "time": "2026-04-29T17:13:54+00:00" }, { "name": "symfony/twig-bundle", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "da5c778a8416fcce5318737c4d944f6fa2bb3f81" + "reference": "ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/da5c778a8416fcce5318737c4d944f6fa2bb3f81", - "reference": "da5c778a8416fcce5318737c4d944f6fa2bb3f81", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95", + "reference": "ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95", "shasum": "" }, "require": { "composer-runtime-api": ">=2.1", "php": ">=8.2", - "symfony/config": "^7.3", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/twig-bridge": "^7.3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/twig-bridge": "^7.3|^8.0", "twig/twig": "^3.12" }, "conflict": { @@ -15535,16 +16476,17 @@ "symfony/translation": "<6.4" }, "require-dev": { - "symfony/asset": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/form": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/routing": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", - "symfony/web-link": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "symfony-bundle", "autoload": { @@ -15572,7 +16514,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v7.3.4" + "source": "https://github.com/symfony/twig-bundle/tree/v7.4.8" }, "funding": [ { @@ -15592,20 +16534,20 @@ "type": "tidelift" } ], - "time": "2025-09-10T12:00:31+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/type-info", - "version": "v7.3.4", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "d34eaeb57f39c8a9c97eb72a977c423207dfa35b" + "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/d34eaeb57f39c8a9c97eb72a977c423207dfa35b", - "reference": "d34eaeb57f39c8a9c97eb72a977c423207dfa35b", + "url": "https://api.github.com/repos/symfony/type-info/zipball/cafeedbf157b890e94ac5b83eaed85595106d5d6", + "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6", "shasum": "" }, "require": { @@ -15655,7 +16597,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.3.4" + "source": "https://github.com/symfony/type-info/tree/v7.4.9" }, "funding": [ { @@ -15675,20 +16617,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T15:33:27+00:00" + "time": "2026-04-22T15:21:55+00:00" }, { "name": "symfony/uid", - "version": "v7.3.1", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "reference": "2676b524340abcfe4d6151ec698463cebafee439" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", + "reference": "2676b524340abcfe4d6151ec698463cebafee439", "shasum": "" }, "require": { @@ -15696,7 +16638,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -15733,7 +16675,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.9" }, "funding": [ { @@ -15744,25 +16686,29 @@ "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-27T19:55:54+00:00" + "time": "2026-04-30T15:19:22+00:00" }, { "name": "symfony/ux-translator", - "version": "v2.30.0", + "version": "v2.35.0", "source": { "type": "git", "url": "https://github.com/symfony/ux-translator.git", - "reference": "9616091db206df4caa7d8dce2e48941512b1a94a" + "reference": "5a56d25237393e865e3df94a39d2c8f0ce94b50c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-translator/zipball/9616091db206df4caa7d8dce2e48941512b1a94a", - "reference": "9616091db206df4caa7d8dce2e48941512b1a94a", + "url": "https://api.github.com/repos/symfony/ux-translator/zipball/5a56d25237393e865e3df94a39d2c8f0ce94b50c", + "reference": "5a56d25237393e865e3df94a39d2c8f0ce94b50c", "shasum": "" }, "require": { @@ -15810,7 +16756,7 @@ "symfony-ux" ], "support": { - "source": "https://github.com/symfony/ux-translator/tree/v2.30.0" + "source": "https://github.com/symfony/ux-translator/tree/v2.35.0" }, "funding": [ { @@ -15830,33 +16776,33 @@ "type": "tidelift" } ], - "time": "2025-08-27T15:25:48+00:00" + "time": "2026-03-22T22:21:50+00:00" }, { "name": "symfony/ux-turbo", - "version": "v2.30.0", + "version": "v2.35.0", "source": { "type": "git", "url": "https://github.com/symfony/ux-turbo.git", - "reference": "c5e88c7e16713e84a2a35f36276ccdb05c2c78d8" + "reference": "4309a4299f5f1b9b7ce4c13ed6d1b77a5472c216" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/c5e88c7e16713e84a2a35f36276ccdb05c2c78d8", - "reference": "c5e88c7e16713e84a2a35f36276ccdb05c2c78d8", + "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/4309a4299f5f1b9b7ce4c13ed6d1b77a5472c216", + "reference": "4309a4299f5f1b9b7ce4c13ed6d1b77a5472c216", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/stimulus-bundle": "^2.9.1" + "symfony/stimulus-bundle": "^2.9.1|^3.0" }, "conflict": { "symfony/flex": "<1.13" }, "require-dev": { "dbrekelmans/bdi": "dev-main", - "doctrine/doctrine-bundle": "^2.4.3", - "doctrine/orm": "^2.8 | 3.0", + "doctrine/doctrine-bundle": "^2.4.3|^3.0|^4.0", + "doctrine/orm": "^2.8|^3.0", "php-webdriver/webdriver": "^1.15", "phpstan/phpstan": "^2.1.17", "symfony/asset-mapper": "^6.4|^7.0|^8.0", @@ -15864,7 +16810,7 @@ "symfony/expression-language": "^5.4|^6.0|^7.0|^8.0", "symfony/form": "^5.4|^6.0|^7.0|^8.0", "symfony/framework-bundle": "^6.4|^7.0|^8.0", - "symfony/mercure-bundle": "^0.3.7", + "symfony/mercure-bundle": "^0.3.7|^0.4.1", "symfony/messenger": "^5.4|^6.0|^7.0|^8.0", "symfony/panther": "^2.2", "symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0", @@ -15873,7 +16819,7 @@ "symfony/security-core": "^5.4|^6.0|^7.0|^8.0", "symfony/stopwatch": "^5.4|^6.0|^7.0|^8.0", "symfony/twig-bundle": "^6.4|^7.0|^8.0", - "symfony/ux-twig-component": "^2.21", + "symfony/ux-twig-component": "^2.21|^3.0", "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0|^8.0" }, "type": "symfony-bundle", @@ -15913,7 +16859,7 @@ "turbo-stream" ], "support": { - "source": "https://github.com/symfony/ux-turbo/tree/v2.30.0" + "source": "https://github.com/symfony/ux-turbo/tree/v2.35.0" }, "funding": [ { @@ -15933,20 +16879,20 @@ "type": "tidelift" } ], - "time": "2025-08-27T15:25:48+00:00" + "time": "2026-04-03T05:13:59+00:00" }, { "name": "symfony/validator", - "version": "v7.3.4", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "5e29a348b5fac2227b6938a54db006d673bb813a" + "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/5e29a348b5fac2227b6938a54db006d673bb813a", - "reference": "5e29a348b5fac2227b6938a54db006d673bb813a", + "url": "https://api.github.com/repos/symfony/validator/zipball/c76458623af9a3fe3b2e5b09b36453f334c2a361", + "reference": "c76458623af9a3fe3b2e5b09b36453f334c2a361", "shasum": "" }, "require": { @@ -15966,27 +16912,29 @@ "symfony/intl": "<6.4", "symfony/property-info": "<6.4", "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/var-exporter": "<6.4.25|>=7.0,<7.3.3", "symfony/yaml": "<6.4" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", - "symfony/cache": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/string": "^6.4|^7.0", - "symfony/translation": "^6.4.3|^7.0.3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", "symfony/type-info": "^7.1.8", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -16015,7 +16963,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.3.4" + "source": "https://github.com/symfony/validator/tree/v7.4.10" }, "funding": [ { @@ -16035,20 +16983,20 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:32:27+00:00" + "time": "2026-05-05T15:30:56+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", - "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", "shasum": "" }, "require": { @@ -16060,10 +17008,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -16102,7 +17050,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" }, "funding": [ { @@ -16122,20 +17070,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-03-30T13:44:50+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.4", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + "reference": "22e03a49c95ef054a43601cd159b222bfab1c701" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/22e03a49c95ef054a43601cd159b222bfab1c701", + "reference": "22e03a49c95ef054a43601cd159b222bfab1c701", "shasum": "" }, "require": { @@ -16143,9 +17091,9 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -16183,7 +17131,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.9" }, "funding": [ { @@ -16203,20 +17151,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/web-link", - "version": "v7.3.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/web-link.git", - "reference": "7697f74fce67555665339423ce453cc8216a98ff" + "reference": "0711009963009e7d6d59149327f3ad633ee3fe25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-link/zipball/7697f74fce67555665339423ce453cc8216a98ff", - "reference": "7697f74fce67555665339423ce453cc8216a98ff", + "url": "https://api.github.com/repos/symfony/web-link/zipball/0711009963009e7d6d59149327f3ad633ee3fe25", + "reference": "0711009963009e7d6d59149327f3ad633ee3fe25", "shasum": "" }, "require": { @@ -16230,7 +17178,7 @@ "psr/link-implementation": "1.0|2.0" }, "require-dev": { - "symfony/http-kernel": "^6.4|^7.0" + "symfony/http-kernel": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -16270,7 +17218,7 @@ "push" ], "support": { - "source": "https://github.com/symfony/web-link/tree/v7.3.0" + "source": "https://github.com/symfony/web-link/tree/v7.4.8" }, "funding": [ { @@ -16281,25 +17229,29 @@ "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-05-19T13:28:18+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/webpack-encore-bundle", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/webpack-encore-bundle.git", - "reference": "7ae70d44c24c3b913f308af8396169b5c6d9e0f5" + "reference": "5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/7ae70d44c24c3b913f308af8396169b5c6d9e0f5", - "reference": "7ae70d44c24c3b913f308af8396169b5c6d9e0f5", + "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e", + "reference": "5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e", "shasum": "" }, "require": { @@ -16342,7 +17294,7 @@ "description": "Integration of your Symfony app with Webpack Encore", "support": { "issues": "https://github.com/symfony/webpack-encore-bundle/issues", - "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.3.0" + "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.4.0" }, "funding": [ { @@ -16362,32 +17314,32 @@ "type": "tidelift" } ], - "time": "2025-08-05T11:43:32+00:00" + "time": "2025-11-27T13:41:46+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.3", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" + "reference": "8b6952b56ca6417f25f7a65758cadd0ce02edc51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8b6952b56ca6417f25f7a65758cadd0ce02edc51", + "reference": "8b6952b56ca6417f25f7a65758cadd0ce02edc51", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -16418,7 +17370,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.3" + "source": "https://github.com/symfony/yaml/tree/v7.4.12" }, "funding": [ { @@ -16438,29 +17390,29 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:34:33+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "symplify/easy-coding-standard", - "version": "12.6.0", + "version": "13.1.3", "source": { "type": "git", - "url": "https://github.com/easy-coding-standard/easy-coding-standard.git", - "reference": "781e6124dc7e14768ae999a8f5309566bbe62004" + "url": "https://github.com/easy-coding-standard/ecs.git", + "reference": "d894d088d7ebb9326f9eed28bf251481c813b89f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/781e6124dc7e14768ae999a8f5309566bbe62004", - "reference": "781e6124dc7e14768ae999a8f5309566bbe62004", + "url": "https://api.github.com/repos/easy-coding-standard/ecs/zipball/d894d088d7ebb9326f9eed28bf251481c813b89f", + "reference": "d894d088d7ebb9326f9eed28bf251481c813b89f", "shasum": "" }, "require": { "php": ">=7.2" }, "conflict": { - "friendsofphp/php-cs-fixer": "<3.46", - "phpcsstandards/php_codesniffer": "<3.8", - "symplify/coding-standard": "<12.1" + "friendsofphp/php-cs-fixer": "<3.95.1", + "phpcsstandards/php_codesniffer": "<4.0.1", + "symplify/coding-standard": "<13.0" }, "suggest": { "ext-dom": "Needed to support checkstyle output format in class CheckstyleOutputFormatter" @@ -16486,33 +17438,22 @@ "static analysis" ], "support": { - "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues", - "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.6.0" + "source": "https://github.com/easy-coding-standard/ecs/tree/13.1.3" }, - "funding": [ - { - "url": "https://www.paypal.me/rectorphp", - "type": "custom" - }, - { - "url": "https://github.com/tomasvotruba", - "type": "github" - } - ], - "time": "2025-09-10T14:21:58+00:00" + "time": "2026-05-04T21:45:57+00:00" }, { "name": "tecnickcom/tc-lib-barcode", - "version": "2.4.8", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-barcode.git", - "reference": "f238ffd120d98a34df6573590e7ed02f766a91c4" + "reference": "4e53047a4ba4ed592ae677b3729ce9bfeae1cfbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/f238ffd120d98a34df6573590e7ed02f766a91c4", - "reference": "f238ffd120d98a34df6573590e7ed02f766a91c4", + "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/4e53047a4ba4ed592ae677b3729ce9bfeae1cfbb", + "reference": "4e53047a4ba4ed592ae677b3729ce9bfeae1cfbb", "shasum": "" }, "require": { @@ -16520,14 +17461,13 @@ "ext-date": "*", "ext-gd": "*", "ext-pcre": "*", - "php": ">=8.1", - "tecnickcom/tc-lib-color": "^2.2" + "php": ">=8.2", + "tecnickcom/tc-lib-color": "^2.7" }, "require-dev": { - "pdepend/pdepend": "2.16.2", - "phpmd/phpmd": "2.15.0", - "phpunit/phpunit": "12.2.0 || 11.5.7 || 10.5.40", - "squizlabs/php_codesniffer": "3.13.0" + "pdepend/pdepend": "^2.16", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpunit/phpunit": "^13.1 || ^12.5 || ^11.5" }, "type": "library", "autoload": { @@ -16547,7 +17487,7 @@ } ], "description": "PHP library to generate linear and bidimensional barcodes", - "homepage": "http://www.tecnick.com", + "homepage": "https://tcpdf.org", "keywords": [ "3 of 9", "ANSI MH10.8M-1983", @@ -16591,39 +17531,38 @@ ], "support": { "issues": "https://github.com/tecnickcom/tc-lib-barcode/issues", - "source": "https://github.com/tecnickcom/tc-lib-barcode/tree/2.4.8" + "source": "https://github.com/tecnickcom/tc-lib-barcode" }, "funding": [ { - "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", - "type": "custom" + "url": "https://github.com/sponsors/tecnickcom", + "type": "github" } ], - "time": "2025-06-06T11:35:02+00:00" + "time": "2026-05-22T07:09:18+00:00" }, { "name": "tecnickcom/tc-lib-color", - "version": "2.2.13", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-color.git", - "reference": "85d1366fb33813aa521d30e3d7c7d7d82a8103a6" + "reference": "6947cc9fffe23a21642279b8ab73a43f3311c5f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/85d1366fb33813aa521d30e3d7c7d7d82a8103a6", - "reference": "85d1366fb33813aa521d30e3d7c7d7d82a8103a6", + "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/6947cc9fffe23a21642279b8ab73a43f3311c5f9", + "reference": "6947cc9fffe23a21642279b8ab73a43f3311c5f9", "shasum": "" }, "require": { "ext-pcre": "*", - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "pdepend/pdepend": "2.16.2", - "phpmd/phpmd": "2.15.0", - "phpunit/phpunit": "12.2.0 || 11.5.7 || 10.5.40", - "squizlabs/php_codesniffer": "3.13.0" + "pdepend/pdepend": "^2.16", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpunit/phpunit": "^13.1 || ^12.5 || ^11.5" }, "type": "library", "autoload": { @@ -16643,7 +17582,7 @@ } ], "description": "PHP library to manipulate various color representations", - "homepage": "http://www.tecnick.com", + "homepage": "https://tcpdf.org", "keywords": [ "cmyk", "color", @@ -16660,35 +17599,226 @@ ], "support": { "issues": "https://github.com/tecnickcom/tc-lib-color/issues", - "source": "https://github.com/tecnickcom/tc-lib-color/tree/2.2.13" + "source": "https://github.com/tecnickcom/tc-lib-color" }, "funding": [ { - "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", - "type": "custom" + "url": "https://github.com/sponsors/tecnickcom", + "type": "github" } ], - "time": "2025-06-06T11:33:19+00:00" + "time": "2026-05-22T06:55:57+00:00" }, { - "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "name": "thecodingmachine/safe", + "version": "v3.4.0", "source": { "type": "git", - "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, + { + "name": "tiendanube/gtinvalidation", + "version": "v1.0", + "source": { + "type": "git", + "url": "https://github.com/TiendaNube/GtinValidator.git", + "reference": "7ff5794b6293eb748bf1efcddf4e20a657c31855" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TiendaNube/GtinValidator/zipball/7ff5794b6293eb748bf1efcddf4e20a657c31855", + "reference": "7ff5794b6293eb748bf1efcddf4e20a657c31855", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "4.6.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "GtinValidation\\": "src/GtinValidation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Blakemore", + "homepage": "https://github.com/ryanblak", + "role": "developer" + } + ], + "description": "Validates GTIN product codes.", + "keywords": [ + "gtin", + "product codes" + ], + "support": { + "issues": "https://github.com/TiendaNube/GtinValidator/issues", + "source": "https://github.com/TiendaNube/GtinValidator/tree/master" + }, + "time": "2018-07-25T22:31:29+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -16721,22 +17851,22 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "twig/cssinliner-extra", - "version": "v3.21.0", + "version": "v3.26.0", "source": { "type": "git", "url": "https://github.com/twigphp/cssinliner-extra.git", - "reference": "378d29b61d6406c456e3a4afbd15bbeea0b72ea8" + "reference": "1b0dc906bbad7226c967bd325e99cccb1a850c4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/378d29b61d6406c456e3a4afbd15bbeea0b72ea8", - "reference": "378d29b61d6406c456e3a4afbd15bbeea0b72ea8", + "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/1b0dc906bbad7226c967bd325e99cccb1a850c4b", + "reference": "1b0dc906bbad7226c967bd325e99cccb1a850c4b", "shasum": "" }, "require": { @@ -16780,7 +17910,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.26.0" }, "funding": [ { @@ -16792,30 +17922,30 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2026-05-15T13:14:14+00:00" }, { "name": "twig/extra-bundle", - "version": "v3.21.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896" + "reference": "6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/62d1cf47a1aa009cbd07b21045b97d3d5cb79896", - "reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9", + "reference": "6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9", "shasum": "" }, "require": { "php": ">=8.1.0", - "symfony/framework-bundle": "^5.4|^6.4|^7.0", - "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.2|^4.0" }, "require-dev": { - "league/commonmark": "^1.0|^2.0", + "league/commonmark": "^2.7", "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", "twig/cssinliner-extra": "^3.0", @@ -16854,7 +17984,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.21.0" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.24.0" }, "funding": [ { @@ -16866,26 +17996,26 @@ "type": "tidelift" } ], - "time": "2025-02-19T14:29:33+00:00" + "time": "2026-02-07T08:07:38+00:00" }, { "name": "twig/html-extra", - "version": "v3.21.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/html-extra.git", - "reference": "5442dd707601c83b8cd4233e37bb10ab8489a90f" + "reference": "313900fb98b371b006a55b1a29241a192634be13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/html-extra/zipball/5442dd707601c83b8cd4233e37bb10ab8489a90f", - "reference": "5442dd707601c83b8cd4233e37bb10ab8489a90f", + "url": "https://api.github.com/repos/twigphp/html-extra/zipball/313900fb98b371b006a55b1a29241a192634be13", + "reference": "313900fb98b371b006a55b1a29241a192634be13", "shasum": "" }, "require": { "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/mime": "^5.4|^6.4|^7.0", + "symfony/mime": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.13|^4.0" }, "require-dev": { @@ -16922,7 +18052,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/html-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/html-extra/tree/v3.24.0" }, "funding": [ { @@ -16934,20 +18064,20 @@ "type": "tidelift" } ], - "time": "2025-02-19T14:29:33+00:00" + "time": "2026-03-17T07:24:08+00:00" }, { "name": "twig/inky-extra", - "version": "v3.21.0", + "version": "v3.26.0", "source": { "type": "git", "url": "https://github.com/twigphp/inky-extra.git", - "reference": "aacd79d94534b4a7fd6533cb5c33c4ee97239a0d" + "reference": "0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/aacd79d94534b4a7fd6533cb5c33c4ee97239a0d", - "reference": "aacd79d94534b4a7fd6533cb5c33c4ee97239a0d", + "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e", + "reference": "0a8b24f0d0247bf6dc5e2af0c0ab09c0c4e5343e", "shasum": "" }, "require": { @@ -16992,7 +18122,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/inky-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/inky-extra/tree/v3.26.0" }, "funding": [ { @@ -17004,25 +18134,25 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2026-05-15T13:14:14+00:00" }, { "name": "twig/intl-extra", - "version": "v3.21.0", + "version": "v3.26.0", "source": { "type": "git", "url": "https://github.com/twigphp/intl-extra.git", - "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146" + "reference": "98f5ad5bff13230fcd2d834d9e79b50adf3ccda9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/05bc5d46b9df9e62399eae53e7c0b0633298b146", - "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/98f5ad5bff13230fcd2d834d9e79b50adf3ccda9", + "reference": "98f5ad5bff13230fcd2d834d9e79b50adf3ccda9", "shasum": "" }, "require": { "php": ">=8.1.0", - "symfony/intl": "^5.4|^6.4|^7.0", + "symfony/intl": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.13|^4.0" }, "require-dev": { @@ -17056,7 +18186,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/intl-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/intl-extra/tree/v3.26.0" }, "funding": [ { @@ -17068,20 +18198,20 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2026-05-19T20:44:48+00:00" }, { "name": "twig/markdown-extra", - "version": "v3.21.0", + "version": "v3.26.0", "source": { "type": "git", "url": "https://github.com/twigphp/markdown-extra.git", - "reference": "f4616e1dd375209dacf6026f846e6b537d036ce4" + "reference": "e3f3fd0836eb6c39457da22c8a76abaac62692b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/f4616e1dd375209dacf6026f846e6b537d036ce4", - "reference": "f4616e1dd375209dacf6026f846e6b537d036ce4", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/e3f3fd0836eb6c39457da22c8a76abaac62692b9", + "reference": "e3f3fd0836eb6c39457da22c8a76abaac62692b9", "shasum": "" }, "require": { @@ -17091,7 +18221,7 @@ }, "require-dev": { "erusev/parsedown": "dev-master as 1.x-dev", - "league/commonmark": "^1.0|^2.0", + "league/commonmark": "^2.7", "league/html-to-markdown": "^4.8|^5.0", "michelf/php-markdown": "^1.8|^2.0", "symfony/phpunit-bridge": "^6.4|^7.0" @@ -17128,7 +18258,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/markdown-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/markdown-extra/tree/v3.26.0" }, "funding": [ { @@ -17140,25 +18270,25 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2026-05-15T13:14:02+00:00" }, { "name": "twig/string-extra", - "version": "v3.21.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", - "reference": "4b3337544ac8f76c280def94e32b53acfaec0589" + "reference": "6ec8f2e8ca9b2193221a02cb599dc92c36384368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/string-extra/zipball/4b3337544ac8f76c280def94e32b53acfaec0589", - "reference": "4b3337544ac8f76c280def94e32b53acfaec0589", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/6ec8f2e8ca9b2193221a02cb599dc92c36384368", + "reference": "6ec8f2e8ca9b2193221a02cb599dc92c36384368", "shasum": "" }, "require": { "php": ">=8.1.0", - "symfony/string": "^5.4|^6.4|^7.0", + "symfony/string": "^5.4|^6.4|^7.0|^8.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^3.13|^4.0" }, @@ -17195,7 +18325,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/string-extra/tree/v3.24.0" }, "funding": [ { @@ -17207,20 +18337,20 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2025-12-02T14:45:16+00:00" }, { "name": "twig/twig", - "version": "v3.21.1", + "version": "v3.26.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + "reference": "1fcae487b180d78e6351f4e0afa91f9eab96a2bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/1fcae487b180d78e6351f4e0afa91f9eab96a2bc", + "reference": "1fcae487b180d78e6351f4e0afa91f9eab96a2bc", "shasum": "" }, "require": { @@ -17230,7 +18360,8 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "phpstan/phpstan": "^2.0", + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -17274,7 +18405,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.21.1" + "source": "https://github.com/twigphp/Twig/tree/v3.26.0" }, "funding": [ { @@ -17286,7 +18417,7 @@ "type": "tidelift" } ], - "time": "2025-05-03T07:21:55+00:00" + "time": "2026-05-20T07:31:59+00:00" }, { "name": "ua-parser/uap-php", @@ -17353,43 +18484,32 @@ }, { "name": "web-auth/cose-lib", - "version": "4.4.2", + "version": "4.5.2", "source": { "type": "git", "url": "https://github.com/web-auth/cose-lib.git", - "reference": "a93b61c48fb587855f64a9ec11ad7b60e867cb15" + "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/a93b61c48fb587855f64a9ec11ad7b60e867cb15", - "reference": "a93b61c48fb587855f64a9ec11ad7b60e867cb15", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13", + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", "ext-json": "*", "ext-openssl": "*", "php": ">=8.1", "spomky-labs/pki-framework": "^1.0" }, "require-dev": { - "deptrac/deptrac": "^3.0", - "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", - "infection/infection": "^0.29", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpstan/extension-installer": "^1.3", - "phpstan/phpstan": "^1.7|^2.0", - "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", - "phpstan/phpstan-phpunit": "^1.1|^2.0", - "phpstan/phpstan-strict-rules": "^1.0|^2.0", - "phpunit/phpunit": "^10.1|^11.0|^12.0", - "rector/rector": "^2.0", - "symfony/phpunit-bridge": "^6.4|^7.0", - "symplify/easy-coding-standard": "^12.0" + "spomky-labs/cbor-php": "^3.2.2" }, "suggest": { "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", - "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension" + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension", + "spomky-labs/cbor-php": "For COSE Signature support" }, "type": "library", "autoload": { @@ -17419,7 +18539,7 @@ ], "support": { "issues": "https://github.com/web-auth/cose-lib/issues", - "source": "https://github.com/web-auth/cose-lib/tree/4.4.2" + "source": "https://github.com/web-auth/cose-lib/tree/4.5.2" }, "funding": [ { @@ -17431,20 +18551,20 @@ "type": "patreon" } ], - "time": "2025-08-14T20:33:29+00:00" + "time": "2026-05-03T09:49:50+00:00" }, { "name": "web-auth/webauthn-lib", - "version": "5.2.2", + "version": "5.3.4", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "8937c397c8ae91b5af422ca8aa915c756062da74" + "reference": "dbb2d7a03db5893da2ef1f2898063ab8f7792838" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/8937c397c8ae91b5af422ca8aa915c756062da74", - "reference": "8937c397c8ae91b5af422ca8aa915c756062da74", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/dbb2d7a03db5893da2ef1f2898063ab8f7792838", + "reference": "dbb2d7a03db5893da2ef1f2898063ab8f7792838", "shasum": "" }, "require": { @@ -17452,18 +18572,18 @@ "ext-openssl": "*", "paragonie/constant_time_encoding": "^2.6|^3.0", "php": ">=8.2", - "phpdocumentor/reflection-docblock": "^5.3", + "phpdocumentor/reflection-docblock": "^5.3|^6.0", "psr/clock": "^1.0", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0|^2.0|^3.0", "spomky-labs/cbor-php": "^3.0", "spomky-labs/pki-framework": "^1.0", - "symfony/clock": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^3.2", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "web-auth/cose-lib": "^4.2.3" }, "suggest": { @@ -17505,7 +18625,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/5.2.2" + "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.4" }, "funding": [ { @@ -17517,34 +18637,35 @@ "type": "patreon" } ], - "time": "2025-03-16T14:38:43+00:00" + "time": "2026-05-18T11:59:46+00:00" }, { "name": "web-auth/webauthn-symfony-bundle", - "version": "5.2.2", + "version": "5.3.4", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-symfony-bundle.git", - "reference": "aebb0315b43728a92973cc3d4d471cbe414baa54" + "reference": "7bf9d0e5e1f6d6bcad97c6bd93dc11a61d6fbd83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/aebb0315b43728a92973cc3d4d471cbe414baa54", - "reference": "aebb0315b43728a92973cc3d4d471cbe414baa54", + "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/7bf9d0e5e1f6d6bcad97c6bd93dc11a61d6fbd83", + "reference": "7bf9d0e5e1f6d6bcad97c6bd93dc11a61d6fbd83", "shasum": "" }, "require": { "php": ">=8.2", "psr/event-dispatcher": "^1.0", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/security-bundle": "^6.4|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/security-http": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^3.0", + "symfony/validator": "^6.4|^7.0|^8.0", "web-auth/webauthn-lib": "self.version" }, "suggest": { @@ -17587,7 +18708,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.2.2" + "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.3.4" }, "funding": [ { @@ -17599,37 +18720,41 @@ "type": "patreon" } ], - "time": "2025-03-24T12:00:00+00:00" + "time": "2026-05-24T09:55:30+00:00" }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", "shasum": "" }, "require": { "ext-ctype": "*", - "php": "^7.2 || ^8.0" + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, "branch-alias": { - "dev-master": "1.10-dev" + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" } }, "autoload": { @@ -17645,6 +18770,10 @@ { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], "description": "Assertions to validate method input/output with nice error messages.", @@ -17655,9 +18784,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/2.4.0" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2026-05-20T13:07:01+00:00" }, { "name": "willdurand/negotiation", @@ -17719,34 +18848,34 @@ "packages-dev": [ { "name": "dama/doctrine-test-bundle", - "version": "v8.4.0", + "version": "v8.6.0", "source": { "type": "git", "url": "https://github.com/dmaicher/doctrine-test-bundle.git", - "reference": "ce7cd44126c36694e2f2d92c4aedd4fc5b0874f2" + "reference": "f7e3487643e685432f7e27c50cac64e9f8c515a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/ce7cd44126c36694e2f2d92c4aedd4fc5b0874f2", - "reference": "ce7cd44126c36694e2f2d92c4aedd4fc5b0874f2", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/f7e3487643e685432f7e27c50cac64e9f8c515a4", + "reference": "f7e3487643e685432f7e27c50cac64e9f8c515a4", "shasum": "" }, "require": { "doctrine/dbal": "^3.3 || ^4.0", "doctrine/doctrine-bundle": "^2.11.0 || ^3.0", - "php": ">= 8.1", + "php": ">= 8.2", "psr/cache": "^2.0 || ^3.0", "symfony/cache": "^6.4 || ^7.3 || ^8.0", "symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<10.0" + "phpunit/phpunit": "<11.0" }, "require-dev": { "behat/behat": "^3.0", "friendsofphp/php-cs-fixer": "^3.27", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.5.57 || ^11.5.41|| ^12.3.14", + "phpunit/phpunit": "^11.5.41|| ^12.3.14", "symfony/dotenv": "^6.4 || ^7.3 || ^8.0", "symfony/process": "^6.4 || ^7.3 || ^8.0" }, @@ -17782,37 +18911,37 @@ ], "support": { "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", - "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.4.0" + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.6.0" }, - "time": "2025-10-11T15:24:02+00:00" + "time": "2026-01-21T07:39:44+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "4.2.0", + "version": "4.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "cd58d7738fe1fea1dbfd3e3f3bb421ee92d45e10" + "reference": "9e013ed10d49bf7746b07204d336384a7d9b5a4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/cd58d7738fe1fea1dbfd3e3f3bb421ee92d45e10", - "reference": "cd58d7738fe1fea1dbfd3e3f3bb421ee92d45e10", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/9e013ed10d49bf7746b07204d336384a7d9b5a4d", + "reference": "9e013ed10d49bf7746b07204d336384a7d9b5a4d", "shasum": "" }, "require": { - "doctrine/data-fixtures": "^2.0", + "doctrine/data-fixtures": "^2.2", "doctrine/doctrine-bundle": "^2.2 || ^3.0", "doctrine/orm": "^2.14.0 || ^3.0", "doctrine/persistence": "^2.4 || ^3.0 || ^4.0", "php": "^8.1", "psr/log": "^2 || ^3", - "symfony/config": "^6.4 || ^7.0", - "symfony/console": "^6.4 || ^7.0", - "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0 || ^8.0", + "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/doctrine-bridge": "^6.4.16 || ^7.1.9", - "symfony/http-kernel": "^6.4 || ^7.0" + "symfony/doctrine-bridge": "^6.4.16 || ^7.1.9 || ^8.0", + "symfony/http-kernel": "^6.4 || ^7.0 || ^8.0" }, "conflict": { "doctrine/dbal": "< 3" @@ -17854,7 +18983,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.2.0" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.3.1" }, "funding": [ { @@ -17870,33 +18999,33 @@ "type": "tidelift" } ], - "time": "2025-10-12T16:50:54+00:00" + "time": "2025-12-03T16:05:42+00:00" }, { "name": "ekino/phpstan-banned-code", - "version": "v3.0.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/ekino/phpstan-banned-code.git", - "reference": "27122aa1783d6521e500c0c397c53244cfbde26f" + "reference": "3356fb9dae03c8759a61fee39dab4728dcc16d74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ekino/phpstan-banned-code/zipball/27122aa1783d6521e500c0c397c53244cfbde26f", - "reference": "27122aa1783d6521e500c0c397c53244cfbde26f", + "url": "https://api.github.com/repos/ekino/phpstan-banned-code/zipball/3356fb9dae03c8759a61fee39dab4728dcc16d74", + "reference": "3356fb9dae03c8759a61fee39dab4728dcc16d74", "shasum": "" }, "require": { - "php": "^8.1", + "php": "^8.2", "phpstan/phpstan": "^2.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.6", "friendsofphp/php-cs-fixer": "^3.0", - "nikic/php-parser": "^4.3", + "nikic/php-parser": "^5.4", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "symfony/var-dumper": "^5.0" + "phpunit/phpunit": "^10.5", + "symfony/var-dumper": "^6.4" }, "type": "phpstan-extension", "extra": { @@ -17934,33 +19063,33 @@ ], "support": { "issues": "https://github.com/ekino/phpstan-banned-code/issues", - "source": "https://github.com/ekino/phpstan-banned-code/tree/v3.0.0" + "source": "https://github.com/ekino/phpstan-banned-code/tree/v3.2.0" }, - "time": "2024-11-13T09:57:22+00:00" + "time": "2026-03-13T12:47:55+00:00" }, { "name": "jbtronics/translation-editor-bundle", - "version": "v1.1.2", + "version": "v1.1.3", "source": { "type": "git", "url": "https://github.com/jbtronics/translation-editor-bundle.git", - "reference": "bab5dd6ef41e87ba3d60c6363793e1cdf5cb6249" + "reference": "36bfb256e11d231d185bc2491323b041ba731257" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jbtronics/translation-editor-bundle/zipball/bab5dd6ef41e87ba3d60c6363793e1cdf5cb6249", - "reference": "bab5dd6ef41e87ba3d60c6363793e1cdf5cb6249", + "url": "https://api.github.com/repos/jbtronics/translation-editor-bundle/zipball/36bfb256e11d231d185bc2491323b041ba731257", + "reference": "36bfb256e11d231d185bc2491323b041ba731257", "shasum": "" }, "require": { "ext-json": "*", "php": "^8.1", "symfony/deprecation-contracts": "^3.4", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/translation": "^7.0|^6.4", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.0|^6.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/twig-bundle": "^7.0|^6.4", - "symfony/web-profiler-bundle": "^7.0|^6.4" + "symfony/twig-bundle": "^7.0|^6.4|^8.0", + "symfony/web-profiler-bundle": "^7.0|^6.4|^8.0" }, "require-dev": { "ekino/phpstan-banned-code": "^1.0", @@ -17994,7 +19123,7 @@ ], "support": { "issues": "https://github.com/jbtronics/translation-editor-bundle/issues", - "source": "https://github.com/jbtronics/translation-editor-bundle/tree/v1.1.2" + "source": "https://github.com/jbtronics/translation-editor-bundle/tree/v1.1.3" }, "funding": [ { @@ -18006,7 +19135,7 @@ "type": "github" } ], - "time": "2025-07-28T09:19:13+00:00" + "time": "2025-11-30T22:23:47+00:00" }, { "name": "myclabs/deep-copy", @@ -18070,16 +19199,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -18122,9 +19251,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -18294,11 +19423,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.31", + "version": "2.1.55", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", - "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9eaac3826ed5e9b8427350a43cac825eeca3f566", + "reference": "9eaac3826ed5e9b8427350a43cac825eeca3f566", "shasum": "" }, "require": { @@ -18343,25 +19472,25 @@ "type": "github" } ], - "time": "2025-10-10T14:14:11+00:00" + "time": "2026-05-18T11:57:34+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.10", + "version": "2.0.22", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "5eaf37b87288474051469aee9f937fc9d862f330" + "reference": "e87516b034749432d51653c0147e053e476e8c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/5eaf37b87288474051469aee9f937fc9d862f330", - "reference": "5eaf37b87288474051469aee9f937fc9d862f330", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/e87516b034749432d51653c0147e053e476e8c53", + "reference": "e87516b034749432d51653c0147e053e476e8c53", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.13" + "phpstan/phpstan": "^2.1.34" }, "conflict": { "doctrine/collections": "<1.0", @@ -18381,15 +19510,16 @@ "doctrine/lexer": "^2.0 || ^3.0", "doctrine/mongodb-odm": "^2.4.3", "doctrine/orm": "^2.16.0", - "doctrine/persistence": "^2.2.1 || ^3.2", + "doctrine/persistence": "^2.2.1 || ^3.4.3", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0.2", - "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-phpunit": "^2.0.8", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", + "shipmonk/name-collision-detector": "^2.1", "symfony/cache": "^5.4", "symfony/uid": "^5.4 || ^6.4 || ^7.3" }, @@ -18412,29 +19542,32 @@ "MIT" ], "description": "Doctrine extensions for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.10" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.22" }, - "time": "2025-10-06T10:01:02+00:00" + "time": "2026-05-09T08:10:48+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.7", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538" + "reference": "9b000a578b85b32945b358b172c7b20e91189024" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538", - "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024", + "reference": "9b000a578b85b32945b358b172c7b20e91189024", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.29" + "phpstan/phpstan": "^2.1.39" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -18460,24 +19593,27 @@ "MIT" ], "description": "Extra strict and opinionated rules for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11" }, - "time": "2025-09-26T11:19:08+00:00" + "time": "2026-05-02T06:54:10+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "2.0.8", + "version": "2.0.18", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "8820c22d785c235f69bb48da3d41e688bc8a1796" + "reference": "a12176b639dec54e8bfd0a5ebf5fc36ffe003b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/8820c22d785c235f69bb48da3d41e688bc8a1796", - "reference": "8820c22d785c235f69bb48da3d41e688bc8a1796", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a12176b639dec54e8bfd0a5ebf5fc36ffe003b5d", + "reference": "a12176b639dec54e8bfd0a5ebf5fc36ffe003b5d", "shasum": "" }, "require": { @@ -18490,7 +19626,7 @@ }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-phpunit": "^2.0.8", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6", "psr/container": "1.1.2", @@ -18531,43 +19667,46 @@ } ], "description": "Symfony Framework extensions and rules for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.8" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.18" }, - "time": "2025-09-07T06:55:50+00:00" + "time": "2026-05-18T14:51:49+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.11", + "version": "11.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", "sebastian/code-unit-reverse-lookup": "^4.0.1", "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/lines-of-code": "^3.0.1", "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^11.5.46" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -18605,7 +19744,7 @@ "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/11.0.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" }, "funding": [ { @@ -18625,32 +19764,32 @@ "type": "tidelift" } ], - "time": "2025-08-27T14:37:49+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -18678,15 +19817,27 @@ "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/5.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.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/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2026-02-02T13:52:54+00:00" }, { "name": "phpunit/php-invoker", @@ -18874,16 +20025,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.42", + "version": "11.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c" + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c", - "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", "shasum": "" }, "require": { @@ -18897,19 +20048,20 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.11", - "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.2", + "sebastian/comparator": "^6.3.3", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.1", "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" @@ -18955,7 +20107,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.42" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" }, "funding": [ { @@ -18979,25 +20131,25 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:09:13+00:00" + "time": "2026-02-18T12:37:06+00:00" }, { "name": "rector/rector", - "version": "2.2.3", + "version": "2.4.4", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "d27f976a332a87b5d03553c2e6f04adbe5da034f" + "reference": "4661c582a20f03df585d2e3fdc4af1b83d67a091" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/d27f976a332a87b5d03553c2e6f04adbe5da034f", - "reference": "d27f976a332a87b5d03553c2e6f04adbe5da034f", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/4661c582a20f03df585d2e3fdc4af1b83d67a091", + "reference": "4661c582a20f03df585d2e3fdc4af1b83d67a091", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.26" + "phpstan/phpstan": "^2.1.48" }, "conflict": { "rector/rector-doctrine": "*", @@ -19031,7 +20183,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.3" + "source": "https://github.com/rectorphp/rector/tree/2.4.4" }, "funding": [ { @@ -19039,7 +20191,7 @@ "type": "github" } ], - "time": "2025-10-11T21:50:23+00:00" + "time": "2026-05-20T19:30:21+00:00" }, { "name": "roave/security-advisories", @@ -19047,37 +20199,44 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "7a8f128281289412092c450a5eb3df5cabbc89e1" + "reference": "f9f1a88a11437cacd4d26b4953416af5c5425389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7a8f128281289412092c450a5eb3df5cabbc89e1", - "reference": "7a8f128281289412092c450a5eb3df5cabbc89e1", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/f9f1a88a11437cacd4d26b4953416af5c5425389", + "reference": "f9f1a88a11437cacd4d26b4953416af5c5425389", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", "adaptcms/adaptcms": "<=1.3", - "admidio/admidio": "<4.3.12", + "admidio/admidio": "<=5.0.8", "adodb/adodb-php": "<=5.22.9", "aheinze/cockpit": "<2.2", "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1", "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7", + "aimeos/ai-cms-grapesjs": ">=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.9|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.10.8|>=2025.04.1,<2025.10.2", "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1", "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", + "aimeos/aimeos-laravel": "==2021.10", "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", "airesvsg/acf-to-rest-api": "<=3.1", "akaunting/akaunting": "<2.1.13", "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", - "alextselegidis/easyappointments": "<1.5.2.0-beta1", + "alextselegidis/easyappointments": "<=1.5.2", + "alexusmai/laravel-file-manager": "<=3.3.1", + "algolia/algoliasearch-magento-2": "<=3.16.1|>=3.17.0.0-beta1,<=3.17.1", + "almirhodzic/nova-toggle-5": "<1.3", "alt-design/alt-redirect": "<1.6.4", + "altcha-org/altcha": "<1.3.1", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "amazing/media2click": ">=1,<1.3.3", "ameos/ameos_tarteaucitron": "<1.2.23", "amphp/artax": "<1.0.6|>=2,<2.0.6", "amphp/http": "<=1.7.2|>=2,<=2.1", "amphp/http-client": ">=4,<4.4", + "amphp/http-server": ">=2.0.0.0-RC1-dev,<2.1.10|>=3.0.0.0-beta1,<3.4.4", "anchorcms/anchor-cms": "<=0.12.7", "andreapollastri/cipi": "<=3.1.15", "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", @@ -19094,28 +20253,30 @@ "athlon1600/php-proxy": "<=5.1", "athlon1600/php-proxy-app": "<=3", "athlon1600/youtube-downloader": "<=4", + "aureuserp/aureuserp": "<1.3.0.0-beta1", "austintoddj/canvas": "<=3.4.2", - "auth0/auth0-php": ">=3.3,<=8.16", - "auth0/login": "<=7.18", - "auth0/symfony": "<=5.4.1", - "auth0/wordpress": "<=5.3", + "auth0/auth0-php": ">=3.3,<=8.18", + "auth0/login": "<=7.20", + "auth0/symfony": "<=5.7", + "auth0/wordpress": "<=5.5", "automad/automad": "<2.0.0.0-alpha5", "automattic/jetpack": "<9.8", "awesome-support/awesome-support": "<=6.0.7", - "aws/aws-sdk-php": "<3.288.1", - "azuracast/azuracast": "<0.18.3", + "aws/aws-sdk-php": "<=3.371.3", + "ayacoo/redirect-tab": "<2.1.2|>=3,<3.1.7|>=4,<4.0.5", + "azuracast/azuracast": "<=0.23.5", "b13/seo_basics": "<0.8.2", - "backdrop/backdrop": "<1.27.3|>=1.28,<1.28.2", + "backdrop/backdrop": "<=1.32", "backpack/crud": "<3.4.9", "backpack/filemanager": "<2.0.2|>=3,<3.0.9", "bacula-web/bacula-web": "<9.7.1", "badaso/core": "<=2.9.11", - "bagisto/bagisto": "<=2.3.7", + "bagisto/bagisto": "<=2.3.15", "barrelstrength/sprout-base-email": "<1.2.7", "barrelstrength/sprout-forms": "<3.9", "barryvdh/laravel-translation-manager": "<0.6.8", "barzahlen/barzahlen-php": "<2.0.1", - "baserproject/basercms": "<=5.1.1", + "baserproject/basercms": "<=5.2.2", "bassjobsen/bootstrap-3-typeahead": ">4.0.2", "bbpress/bbpress": "<2.6.5", "bcit-ci/codeigniter": "<3.1.3", @@ -19142,7 +20303,8 @@ "bvbmedia/multishop": "<2.0.39", "bytefury/crater": "<6.0.2", "cachethq/cachet": "<2.5.1", - "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cadmium-org/cadmium-cms": "<=0.4.9", + "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10|>=5.2.10,<5.2.12|==5.3", "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", "cardgate/magento2": "<2.0.33", "cardgate/woocommerce": "<=3.1.15", @@ -19153,37 +20315,50 @@ "causal/oidc": "<4", "cecil/cecil": "<7.47.1", "centreon/centreon": "<22.10.15", + "cesargb/laravel-magiclink": ">=2,<2.25.1", "cesnet/simplesamlphp-module-proxystatistics": "<3.1", "chriskacerguis/codeigniter-restserver": "<=2.7.1", "chrome-php/chrome": "<1.14", + "ci4-cms-erp/ci4ms": "<=0.31.8", "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", "ckeditor/ckeditor": "<4.25", "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3", "co-stack/fal_sftp": "<0.2.6", - "cockpit-hq/cockpit": "<2.11.4", + "cockpit-hq/cockpit": "<=2.14", + "code16/sharp": "<9.22", "codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeigniter/framework": "<3.1.10", "codeigniter4/framework": "<4.6.2", "codeigniter4/shield": "<1.0.0.0-beta8", "codiad/codiad": "<=2.8.4", "codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9", + "codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5", "commerceteam/commerce": ">=0.9.6,<0.9.9", "components/jquery": ">=1.0.3,<3.5", - "composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7", - "concrete5/concrete5": "<9.4.3", + "composer/composer": "<2.2.28|>=2.3,<2.9.8", + "concrete5/concrete5": "<9.4.8", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.13.56|>=5,<5.3.38|>=5.4.0.0-RC1-dev,<5.6.1", "contao/core": "<3.5.39", - "contao/core-bundle": "<4.13.56|>=5,<5.3.38|>=5.4,<5.6.1", + "contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5", "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", "contao/managed-edition": "<=1.5", + "coreshop/core-shop": "<4.1.9|==5", "corveda/phpsandbox": "<1.3.5", "cosenary/instagram": "<=2.3", "couleurcitron/tarteaucitron-wp": "<0.3", - "craftcms/cms": "<=4.16.5|>=5,<=5.8.6", - "croogo/croogo": "<4", + "cpsit/typo3-mailqueue": "<0.4.5|>=0.5,<0.5.2", + "craftcms/aws-s3": ">=2.0.2,<=2.2.4", + "craftcms/azure-blob": ">=2.0.0.0-beta1,<=2.1", + "craftcms/cms": "<4.17.12|>=5,<5.9.18", + "craftcms/commerce": ">=4,<4.11|>=5,<5.6", + "craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1", + "craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21", + "craftcms/google-cloud": ">=2.0.0.0-beta1,<=2.2", + "craftcms/webhooks": ">=3,<3.2", + "croogo/croogo": "<=4.0.7", "cuyz/valinor": "<0.12", "czim/file-handling": "<1.5|>=2,<2.3", "czproject/git-php": "<4.0.3", @@ -19196,13 +20371,16 @@ "david-garcia/phpwhois": "<=4.3.1", "dbrisinajumi/d2files": "<1", "dcat/laravel-admin": "<=2.1.3|==2.2.0.0-beta|==2.2.2.0-beta", + "dedoc/scramble": ">=0.13.2,<0.13.22", "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", "desperado/xml-bundle": "<=0.1.7", "dev-lancer/minecraft-motd-parser": "<=1.0.5", + "devcode-it/openstamanager": "<=2.10.1", "devgroup/dotplant": "<2020.09.14-dev", "digimix/wp-svg-upload": "<=1", "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", + "directorytree/imapengine": "<1.22.3", "dl/yag": "<3.0.1", "dmk/webkitpdf": "<1.1.4", "dnadesign/silverstripe-elemental": "<5.3.12", @@ -19215,40 +20393,52 @@ "doctrine/mongodb-odm": "<1.0.2", "doctrine/mongodb-odm-bundle": "<3.0.1", "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", - "dolibarr/dolibarr": "<21.0.3", + "dolibarr/dolibarr": "<=23.0.2", "dompdf/dompdf": "<2.0.4", "doublethreedigital/guest-entries": "<3.1.2", + "dreamfactory/df-core": "<1.0.4", "drupal-pattern-lab/unified-twig-extensions": "<=0.1", + "drupal/access_code": "<2.0.5", + "drupal/acquia_dam": "<1.1.5", "drupal/admin_audit_trail": "<1.0.5", "drupal/ai": "<1.0.5", "drupal/alogin": "<2.0.6", "drupal/cache_utility": "<1.2.1", + "drupal/civictheme": "<1.12", "drupal/commerce_alphabank_redirect": "<1.0.3", "drupal/commerce_eurobank_redirect": "<2.1.1", "drupal/config_split": "<1.10|>=2,<2.0.2", - "drupal/core": ">=6,<6.38|>=7,<7.102|>=8,<10.3.14|>=10.4,<10.4.5|>=11,<11.0.13|>=11.1,<11.1.5", + "drupal/core": ">=6,<6.38|>=7,<7.103|>=8,<10.4.9|>=10.5,<10.5.6|>=11,<11.1.9|>=11.2,<11.2.8", "drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", + "drupal/currency": "<3.5", "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", + "drupal/email_tfa": "<2.0.6", "drupal/formatter_suite": "<2.1", "drupal/gdpr": "<3.0.1|>=3.1,<3.1.2", "drupal/google_tag": "<1.8|>=2,<2.0.8", "drupal/ignition": "<1.0.4", + "drupal/json_field": "<1.5", "drupal/lightgallery": "<1.6", "drupal/link_field_display_mode_formatter": "<1.6", "drupal/matomo": "<1.24", "drupal/oauth2_client": "<4.1.3", "drupal/oauth2_server": "<2.1", "drupal/obfuscate": "<2.0.1", + "drupal/plausible_tracking": "<1.0.2", "drupal/quick_node_block": "<2", "drupal/rapidoc_elements_field_formatter": "<1.0.1", + "drupal/reverse_proxy_header": "<1.1.2", + "drupal/simple_multistep": "<2", + "drupal/simple_oauth": ">=6,<6.0.7", "drupal/spamspan": "<3.2.1", "drupal/tfa": "<1.10", + "drupal/umami_analytics": "<1.0.1", "duncanmcclean/guest-entries": "<3.1.2", "dweeves/magmi": "<=0.7.24", - "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", + "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.3.1", "ecodev/newsletter": "<=4", "ectouch/ectouch": "<=2.7.2", - "egroupware/egroupware": "<23.1.20240624", + "egroupware/egroupware": "<23.1.20260113|>=26.0.20251208,<26.0.20260113", "elefant/cms": "<2.0.7", "elgg/elgg": "<3.3.24|>=4,<4.0.5", "elijaa/phpmemcacheadmin": "<=1.3", @@ -19260,6 +20450,7 @@ "erusev/parsedown": "<1.7.2", "ether/logs": "<3.0.4", "evolutioncms/evolution": "<=3.2.3", + "evoweb/sf-register": "<13.2.4|>=14,<14.0.2", "exceedone/exment": "<4.4.3|>=5,<5.0.3", "exceedone/laravel-admin": "<2.2.3|==3", "ezsystems/demobundle": ">=5.4,<5.4.6.1-dev", @@ -19271,41 +20462,45 @@ "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1|>=5.3.0.0-beta1,<5.3.5", "ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12", "ezsystems/ezplatform-http-cache": "<2.3.16", - "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35", + "ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<1.3.35", "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", "ezsystems/ezplatform-richtext": ">=2.3,<2.3.26|>=3.3,<3.3.40", "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", "ezsystems/ezplatform-user": ">=1,<1.0.1", - "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", + "ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<7.5.31", "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1", "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", "ezyang/htmlpurifier": "<=4.2", "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", - "facturascripts/facturascripts": "<=2022.08", + "facturascripts/facturascripts": "<=2025.92|>=2026,<=2026.1", "fastly/magento2": "<1.2.26", "feehi/cms": "<=2.1.1", "feehi/feehicms": "<=2.1.1", "fenom/fenom": "<=2.12.1", "filament/actions": ">=3.2,<3.2.123", + "filament/filament": ">=4,<4.3.1", "filament/infolists": ">=3,<3.2.115", - "filament/tables": ">=3,<3.2.115", + "filament/tables": ">=3,<3.2.115|>=4,<4.8.5|>=5,<5.3.5", "filegator/filegator": "<7.8", "filp/whoops": "<2.1.13", "fineuploader/php-traditional-server": "<=1.2.2", - "firebase/php-jwt": "<6", + "firebase/php-jwt": "<7", "fisharebest/webtrees": "<=2.1.18", "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", "fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6", - "flarum/core": "<1.8.10", + "flarum/core": "<=1.8.15|>=2.0.0.0-beta1,<=2.0.0.0-beta8", "flarum/flarum": "<0.1.0.0-beta8", "flarum/framework": "<1.8.10", "flarum/mentions": "<1.6.3", + "flarum/nicknames": "<1.8.3", "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", "flarum/tags": "<=0.1.0.0-beta13", + "flightphp/core": "<3.18.1", "floriangaerber/magnesium": "<0.3.1", "fluidtypo3/vhs": "<5.1.1", "fof/byobu": ">=0.3.0.0-beta2,<1.1.7", + "fof/pretty-mail": "<=1.1.2", "fof/upload": "<1.2.3", "foodcoopshop/foodcoopshop": ">=3.2,<3.6.1", "fooman/tcpdf": "<6.2.22", @@ -19320,18 +20515,22 @@ "friendsofsymfony1/symfony1": ">=1.1,<1.5.19", "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", + "friendsoftypo3/tt-address": "<8.1.2|>=9,<9.1.1|>=10,<10.0.1", "froala/wysiwyg-editor": "<=4.3", - "froxlor/froxlor": "<=2.2.5", + "frosh/adminer-platform": "<2.2.1", + "froxlor/froxlor": "<2.3.6", "frozennode/administrator": "<=5.0.12", "fuel/core": "<1.8.1", - "funadmin/funadmin": "<=5.0.2", + "funadmin/funadmin": "<=7.1.0.0-RC6", "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", - "georgringer/news": "<1.3.3", + "georgringer/news": "<11.4.4|>=12,<12.3.2|>=13,<13.0.2|>=14,<14.0.3", "geshi/geshi": "<=1.0.9.1", - "getformwork/formwork": "<1.13.1|>=2.0.0.0-beta1,<2.0.0.0-beta4", - "getgrav/grav": "<1.7.46", - "getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", + "getformwork/formwork": "<=2.3.3", + "getgrav/grav": "<=2.0.0.0-RC1", + "getgrav/grav-plugin-api": "<1.0.0.0-beta15", + "getgrav/grav-plugin-form": "<9.1", + "getkirby/cms": "<4.9|>=5,<5.4", "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", @@ -19340,12 +20539,13 @@ "globalpayments/php-sdk": "<2", "goalgorilla/open_social": "<12.3.11|>=12.4,<12.4.10|>=13.0.0.0-alpha1,<13.0.0.0-alpha11", "gogentooss/samlbase": "<1.2.7", - "google/protobuf": "<3.4", + "goodoneuz/pay-uz": "<=2.2.24", + "google/protobuf": "<4.33.6", "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", "gp247/core": "<1.1.24", "gree/jose": "<2.2.1", "gregwar/rst": "<1.0.3", - "grumpydictator/firefly-iii": "<6.1.17", + "grumpydictator/firefly-iii": "<6.1.17|>=6.4.23,<=6.5", "gugoan/economizzer": "<=0.9.0.0-beta1", "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", "guzzlehttp/oauth-subscriber": "<0.8.1", @@ -19360,6 +20560,7 @@ "hjue/justwriting": "<=1", "hov/jobfair": "<1.0.13|>=2,<2.0.2", "httpsoft/http-message": "<1.0.12", + "hybridauth/hybridauth": "<=3.12.2", "hyn/multi-tenant": ">=5.6,<5.7.2", "ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6,<4.6.25|>=5,<5.0.3", "ibexa/admin-ui-assets": ">=4.6.0.0-alpha1,<4.6.21", @@ -19369,9 +20570,9 @@ "ibexa/http-cache": ">=4.6,<4.6.14", "ibexa/post-install": "<1.0.16|>=4.6,<4.6.14", "ibexa/solr": ">=4.5,<4.5.4", - "ibexa/user": ">=4,<4.4.3|>=5,<5.0.3", + "ibexa/user": ">=4,<4.4.3|>=5,<5.0.4", "icecoder/icecoder": "<=8.1", - "idno/known": "<=1.3.1", + "idno/known": "<1.6.4", "ilicmiljan/secure-props": ">=1.2,<1.2.2", "illuminate/auth": "<5.5.10", "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4", @@ -19388,10 +20589,13 @@ "innologi/typo3-appointments": "<2.0.6", "intelliants/subrion": "<4.2.2", "inter-mediator/inter-mediator": "==5.5", - "ipl/web": "<0.10.1", + "intercom/intercom-php": "==5.0.2", + "invoiceninja/invoiceninja": "<5.13.4", + "ipl/web": "<=0.13", "islandora/crayfish": "<4.1", "islandora/islandora": ">=2,<2.4.1", "ivankristianto/phpwhois": "<=4.3", + "j0k3r/graby": "<=2.5", "jackalope/jackalope-doctrine-dbal": "<1.7.4", "jambagecom/div2007": "<0.10.2", "james-heinrich/getid3": "<1.9.21", @@ -19399,7 +20603,9 @@ "jasig/phpcas": "<1.3.3", "jbartels/wec-map": "<3.0.3", "jcbrand/converse.js": "<3.3.3", + "joedolson/my-calendar": "<3.7.7", "joelbutcher/socialstream": "<5.6|>=6,<6.2", + "johnbillion/query-monitor": "<3.20.4", "johnbillion/wp-crontrol": "<1.16.2|>=1.17,<1.19.2", "joomla/application": "<1.0.13", "joomla/archive": "<1.1.12|>=2,<2.0.1", @@ -19417,20 +20623,23 @@ "juzaweb/cms": "<=3.4.2", "jweiland/events2": "<8.3.8|>=9,<9.0.6", "jweiland/kk-downloader": "<1.2.2", + "kantorge/yaffa": "<=2", "kazist/phpwhois": "<=4.2.6", + "kelvinmo/simplejwt": "<=1.1", "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", - "khodakhah/nodcms": "<=3", - "kimai/kimai": "<=2.20.1", + "khodakhah/nodcms": "<=3.4.1", + "kimai/kimai": "<=2.55", "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "klaviyo/magento2-extension": ">=1,<3", - "knplabs/knp-snappy": "<=1.4.2", + "knplabs/knp-snappy": "<=1.7", "kohana/core": "<3.3.3", "koillection/koillection": "<1.6.12", - "krayin/laravel-crm": "<=1.3", + "krayin/laravel-crm": "<=2.2", "kreait/firebase-php": ">=3.2,<3.8.1", "kumbiaphp/kumbiapp": "<=1.1.1", "la-haute-societe/tcpdf": "<6.2.22", + "laktak/hjson": "<2.3", "laminas/laminas-diactoros": "<2.18.1|==2.19|==2.20|==2.21|==2.22|==2.23|>=2.24,<2.24.2|>=2.25,<2.25.2", "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1", "laminas/laminas-http": "<2.14.2", @@ -19439,24 +20648,26 @@ "laravel/fortify": "<1.11.1", "laravel/framework": "<10.48.29|>=11,<11.44.1|>=12,<12.1.1", "laravel/laravel": ">=5.4,<5.4.22", + "laravel/passport": ">=13,<13.7.1", "laravel/pulse": "<1.3.1", - "laravel/reverb": "<1.4", + "laravel/reverb": "<1.7", "laravel/socialite": ">=1,<2.0.10", "latte/latte": "<2.10.8", - "lavalite/cms": "<=9|==10.1", + "lavalite/cms": "<=10.1", "lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2", "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", - "league/commonmark": "<2.7", + "league/commonmark": "<=2.8.1", "league/flysystem": "<1.1.4|>=2,<2.1.1", "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", "leantime/leantime": "<3.3", "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", "libreform/libreform": ">=2,<=2.0.8", - "librenms/librenms": "<2017.08.18", + "librenms/librenms": "<26.3", "liftkit/database": "<2.13.2", "lightsaml/lightsaml": "<1.3.5", - "limesurvey/limesurvey": "<6.5.12", + "limesurvey/limesurvey": "<6.15.4", "livehelperchat/livehelperchat": "<=3.91", + "livewire-filemanager/filemanager": "<=1.0.4", "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.6.4", "livewire/volt": "<1.7", "lms/routes": "<2.1.1", @@ -19466,7 +20677,7 @@ "luyadev/yii-helpers": "<1.2.1", "macropay-solutions/laravel-crud-wizard-free": "<3.4.17", "maestroerror/php-heic-to-jpg": "<1.0.5", - "magento/community-edition": "<=2.4.5.0-patch14|==2.4.6|>=2.4.6.0-patch1,<=2.4.6.0-patch12|>=2.4.7.0-beta1,<=2.4.7.0-patch7|>=2.4.8.0-beta1,<=2.4.8.0-patch2|>=2.4.9.0-alpha1,<=2.4.9.0-alpha2|==2.4.9", + "magento/community-edition": "<2.4.6.0-patch13|>=2.4.7.0-beta1,<2.4.7.0-patch8|>=2.4.8.0-beta1,<2.4.8.0-patch3|>=2.4.9.0-alpha1,<2.4.9.0-alpha3|==2.4.9", "magento/core": "<=1.9.4.5", "magento/magento1ce": "<1.9.4.3-dev", "magento/magento1ee": ">=1,<1.14.4.3-dev", @@ -19477,17 +20688,20 @@ "maikuolan/phpmussel": ">=1,<1.6", "mainwp/mainwp": "<=4.4.3.3", "manogi/nova-tiptap": "<=3.2.6", - "mantisbt/mantisbt": "<=2.26.3", + "mantisbt/mantisbt": "<2.28.2", "marcwillmann/turn": "<0.3.3", + "markhuot/craftql": "<=1.3.7", "marshmallow/nova-tiptap": "<5.7", "matomo/matomo": "<1.11", "matyhtf/framework": "<3.0.6", - "mautic/core": "<5.2.8|>=6.0.0.0-alpha,<6.0.5", + "mautic/core": "<5.2.10|>=6,<6.0.8|>=7.0.0.0-alpha,<7.0.1", "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", + "mautic/grapes-js-builder-bundle": ">=4,<4.4.18|>=5,<5.2.9|>=6,<6.0.7", "maximebf/debugbar": "<1.19", + "mckenziearts/livewire-markdown-editor": "<1.3", "mdanter/ecc": "<2", "mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2", - "mediawiki/cargo": "<3.6.1", + "mediawiki/cargo": "<3.8.3", "mediawiki/core": "<1.39.5|==1.40", "mediawiki/data-transfer": ">=1.39,<1.39.11|>=1.41,<1.41.3|>=1.42,<1.42.2", "mediawiki/matomo": "<2.4.3", @@ -19503,29 +20717,34 @@ "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1", "microsoft/microsoft-graph-beta": "<2.0.1", "microsoft/microsoft-graph-core": "<2.0.2", - "microweber/microweber": "<=2.0.19", + "microweber/microweber": "<2.0.20", "mikehaertl/php-shellcommand": "<1.6.1", + "mineadmin/mineadmin": "<=3.0.9", "miniorange/miniorange-saml": "<1.4.3", + "miraheze/ts-portal": "<=33", "mittwald/typo3_forum": "<1.2.1", + "mix/mix": ">=2,<=2.2.17", + "mmc/ceselector": "<3.0.3|>=4,<4.0.2|>=5,<5.0.1|>=6,<6.0.1", "mobiledetect/mobiledetectlib": "<2.8.32", "modx/revolution": "<=3.1", "mojo42/jirafeau": "<4.4", "mongodb/mongodb": ">=1,<1.9.2", + "mongodb/mongodb-extension": "<1.21.2", "monolog/monolog": ">=1.8,<1.12", - "moodle/moodle": "<4.3.12|>=4.4,<4.4.8|>=4.5.0.0-beta,<4.5.4", + "moodle/moodle": "<4.5.9|>=5.0.0.0-beta,<5.0.5|>=5.1.0.0-beta,<5.1.2", "moonshine/moonshine": "<=3.12.5", "mos/cimage": "<0.7.19", "movim/moxl": ">=0.8,<=0.10", "movingbytes/social-network": "<=1.2.1", "mpdf/mpdf": "<=7.1.7", - "munkireport/comment": "<4.1", + "munkireport/comment": "<4", "munkireport/managedinstalls": "<2.6", "munkireport/munki_facts": "<1.5", - "munkireport/munkireport": ">=2.5.3,<5.6.3", "munkireport/reportdata": "<3.5", "munkireport/softwareupdate": "<1.6", "mustache/mustache": ">=2,<2.14.1", "mwdelaney/wp-enable-svg": "<=0.2", + "nabeel/phpvms": "<7.0.6", "namshi/jose": "<2.2", "nasirkhan/laravel-starter": "<11.11", "nategood/httpful": "<1", @@ -19541,6 +20760,7 @@ "netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15", "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", + "neuron-core/neuron-ai": "<=2.8.11", "nilsteampassnet/teampass": "<3.1.3.1-dev", "nitsan/ns-backup": "<13.0.1", "nonfiction/nterchange": "<4.1.1", @@ -19555,19 +20775,19 @@ "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", "october/backend": "<1.1.2", "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", - "october/october": "<3.7.5", - "october/rain": "<1.0.472|>=1.1,<1.1.2", - "october/system": "<3.7.5", + "october/october": "<3.7.14|>=4,<4.1.10", + "october/rain": "<=3.7.13|>=4,<=4.1.9", + "october/system": "<3.7.16|>=4,<4.1.16", "oliverklee/phpunit": "<3.5.15", "omeka/omeka-s": "<4.0.3", - "onelogin/php-saml": "<2.10.4", + "onelogin/php-saml": "<2.21.1|>=3,<3.8.1|>=4,<4.3.1", "oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5", "open-web-analytics/open-web-analytics": "<1.8.1", "opencart/opencart": ">=0", "openid/php-openid": "<2.3", - "openmage/magento-lts": "<20.12.3", + "openmage/magento-lts": "<=20.17", "opensolutions/vimbadmin": "<=3.0.15", - "opensource-workshop/connect-cms": "<1.8.7|>=2,<2.4.7", + "opensource-workshop/connect-cms": "<1.41.1|>=2,<2.41.1", "orchid/platform": ">=8,<14.43", "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", "oro/commerce": ">=4.1,<5.0.11|>=5.1,<5.1.1", @@ -19584,6 +20804,7 @@ "pagekit/pagekit": "<=1.0.18", "paragonie/ecc": "<2.0.1", "paragonie/random_compat": "<2", + "paragonie/sodium_compat": "<1.24|>=2,<2.5", "passbolt/passbolt_api": "<4.6.2", "paypal/adaptivepayments-sdk-php": "<=3.9.2", "paypal/invoice-sdk-php": "<=3.9", @@ -19596,6 +20817,7 @@ "pear/pear": "<=1.10.1", "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", "personnummer/personnummer": "<3.0.2", + "ph7software/ph7builder": "<=17.9.1", "phanan/koel": "<5.1.4", "phenx/php-svg-lib": "<0.5.2", "php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5", @@ -19606,32 +20828,35 @@ "phpmailer/phpmailer": "<6.5", "phpmussel/phpmussel": ">=1,<1.6", "phpmyadmin/phpmyadmin": "<5.2.2", - "phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5|>=3.2.10,<=4.0.1", + "phpmyfaq/phpmyfaq": "<4.1.3", "phpoffice/common": "<0.2.9", "phpoffice/math": "<=0.2", "phpoffice/phpexcel": "<=1.8.2", - "phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5", - "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", + "phpoffice/phpspreadsheet": "<=1.30.3|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6", + "phppgadmin/phppgadmin": "<=7.13", + "phpseclib/phpseclib": "<=2.0.53|>=3,<=3.0.51", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", - "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpunit/phpunit": "<8.5.52|>=9,<9.6.33|>=10,<10.5.62|>=11,<11.5.50|>=12,<12.5.8|>=12.5.21,<12.5.22|>=13.1.5,<13.1.6", "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", + "phraseanet/phraseanet": "==4.0.3", "pi/pi": "<=2.5", - "pimcore/admin-ui-classic-bundle": "<1.7.6", + "pimcore/admin-ui-classic-bundle": "<=1.7.15|>=2.0.0.0-RC1-dev,<=2.2.2", "pimcore/customer-management-framework-bundle": "<4.2.1", "pimcore/data-hub": "<1.2.4", "pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3", "pimcore/demo": "<10.3", "pimcore/ecommerce-framework-bundle": "<1.0.10", "pimcore/perspective-editor": "<1.5.1", - "pimcore/pimcore": "<11.5.4", + "pimcore/pimcore": "<=11.5.14.1|>=12,<12.3.3|==12.3.3", + "pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1", "piwik/piwik": "<1.11", "pixelfed/pixelfed": "<0.12.5", "plotly/plotly.js": "<2.25.2", "pocketmine/bedrock-protocol": "<8.0.2", - "pocketmine/pocketmine-mp": "<5.32.1", + "pocketmine/pocketmine-mp": "<5.42.1", "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", @@ -19639,23 +20864,25 @@ "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">=1.0.1,<4.3", "prestashop/gamification": "<2.3.2", - "prestashop/prestashop": "<8.2.3", + "prestashop/prestashop": "<8.2.6|>=9,<9.1.1", "prestashop/productcomments": "<5.0.2", - "prestashop/ps_checkout": "<4.4.1|>=5,<5.0.5", + "prestashop/ps_checkout": "<5.3", "prestashop/ps_contactinfo": "<=3.3.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", "prestashop/ps_linklist": "<3.1", - "privatebin/privatebin": "<1.4|>=1.5,<1.7.4", - "processwire/processwire": "<=3.0.229", + "privatebin/privatebin": "<1.4|>=1.5,<1.7.4|>=1.7.7,<2.0.3", + "processwire/processwire": "<=3.0.255", "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", "propel/propel1": ">=1,<=1.7.1", - "pterodactyl/panel": "<=1.11.10", + "psy/psysh": "<=0.11.22|>=0.12,<=0.12.18", + "pterodactyl/panel": "<1.12.1", "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", "ptrofimov/beanstalk_console": "<1.7.14", "pubnub/pubnub": "<6.1", "punktde/pt_extbase": "<1.5.1", "pusher/pusher-php-server": "<2.2.1", + "putyourlightson/craft-sprig": ">=2,<2.15.2|>=3,<3.7.2", "pwweb/laravel-core": "<=0.3.6.0-beta", "pxlrbt/filament-excel": "<1.1.14|>=2.0.0.0-alpha,<2.3.3", "pyrocms/pyrocms": "<=3.9.1", @@ -19664,43 +20891,50 @@ "rainlab/blog-plugin": "<1.4.1", "rainlab/debugbar-plugin": "<3.1", "rainlab/user-plugin": "<=1.4.5", + "ralffreit/mfa-email": "<1.0.7|==2", "rankmath/seo-by-rank-math": "<=1.0.95", "rap2hpoutre/laravel-log-viewer": "<0.13", "react/http": ">=0.7,<1.9", "really-simple-plugins/complianz-gdpr": "<6.4.2", - "redaxo/source": "<5.18.3", + "redaxo/source": "<5.21", "remdex/livehelperchat": "<4.29", "renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1", "reportico-web/reportico": "<=8.1", - "rhukster/dom-sanitizer": "<1.0.7", + "rhukster/dom-sanitizer": "<1.0.10", "rmccue/requests": ">=1.6,<1.8", - "robrichards/xmlseclibs": ">=1,<3.0.4", + "roadiz/documents": "<2.3.42|>=2.4,<2.5.44|>=2.6,<2.6.28|>=2.7,<2.7.9", + "roadiz/openid": "<2.3.43|>=2.5,<2.5.45|>=2.6,<2.6.31|>=2.7,<2.7.18", + "robrichards/xmlseclibs": "<3.1.5", "roots/soil": "<4.1", - "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11", + "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11|>=1.7.0.0-beta,<1.7.0.0-RC5-dev", "rudloff/alltube": "<3.0.3", "rudloff/rtmpdump-bin": "<=2.3.1", "s-cart/core": "<=9.0.5", "s-cart/s-cart": "<6.9", + "s9y/serendipity": "<2.6", "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", + "saloonphp/saloon": "<4", "samwilson/unlinked-wikibase": "<1.42", "scheb/two-factor-bundle": "<3.26|>=4,<4.11", "sensiolabs/connect": "<4.2.3", "serluck/phpwhois": "<=4.2.6", - "setasign/fpdi": "<2.6.4", + "setasign/fpdi": "<2.6.7", "sfroemken/url_redirect": "<=1.2.1", "sheng/yiicms": "<1.2.1", - "shopware/core": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7,<6.7.2.1-dev", - "shopware/platform": "<=6.6.10.4|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", + "shopper/cart": "<2.8", + "shopper/framework": "<2.8", + "shopware/core": "<6.6.10.15-dev|>=6.7,<6.7.8.1-dev", + "shopware/platform": "<6.6.10.15-dev|>=6.7,<6.7.8.1-dev", "shopware/production": "<=6.3.5.2", - "shopware/shopware": "<=5.7.17|>=6.7,<6.7.2.1-dev", - "shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev", + "shopware/shopware": "<=5.7.17|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.6.1-dev", + "shopware/storefront": "<6.6.10.10-dev|>=6.7,<6.7.5.1-dev", "shopxo/shopxo": "<=6.4", - "showdoc/showdoc": "<2.10.4", + "showdoc/showdoc": "<3.8.1", "shuchkin/simplexlsx": ">=1.0.12,<1.1.13", "silverstripe-australia/advancedreports": ">=1,<=2", "silverstripe/admin": "<1.13.19|>=2,<2.1.8", - "silverstripe/assets": ">=1,<1.11.1", + "silverstripe/assets": "<2.4.5|>=3,<3.1.3", "silverstripe/cms": "<4.11.3", "silverstripe/comments": ">=1.3,<3.1.1", "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", @@ -19721,11 +20955,12 @@ "simplesamlphp/saml2": "<=4.16.15|>=5.0.0.0-alpha1,<=5.0.0.0-alpha19", "simplesamlphp/saml2-legacy": "<=4.16.15", "simplesamlphp/simplesamlphp": "<1.18.6", + "simplesamlphp/simplesamlphp-module-casserver": "<=7.0.2", "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", "simplesamlphp/simplesamlphp-module-openid": "<1", "simplesamlphp/simplesamlphp-module-openidprovider": "<0.9", "simplesamlphp/xml-common": "<1.20", - "simplesamlphp/xml-security": "==1.6.11", + "simplesamlphp/xml-security": "<1.13.9|>=2,<2.3.1", "simplito/elliptic-php": "<1.0.6", "sitegeist/fluid-components": "<3.5", "sjbr/sr-feuser-register": "<2.6.2|>=5.1,<12.5", @@ -19735,31 +20970,32 @@ "slim/slim": "<2.6", "slub/slub-events": "<3.0.3", "smarty/smarty": "<4.5.3|>=5,<5.1.1", - "snipe/snipe-it": "<8.1.18", + "snipe/snipe-it": "<8.4.1", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", - "solspace/craft-freeform": ">=5,<5.10.16", + "solspace/craft-freeform": "<4.1.29|>=5,<=5.14.6", "soosyze/soosyze": "<=2", "spatie/browsershot": "<5.0.5", "spatie/image-optimizer": "<1.7.3", "spencer14420/sp-php-email-handler": "<1", "spipu/html2pdf": "<5.2.8", + "spiral/roadrunner": "<2025.1", "spoon/library": "<1.4.1", "spoonity/tcpdf": "<6.2.22", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", "ssddanbrown/bookstack": "<24.05.1", - "starcitizentools/citizen-skin": ">=1.9.4,<3.4", + "starcitizentools/citizen-skin": ">=1.9.4,<3.9", "starcitizentools/short-description": ">=4,<4.0.1", "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", "starcitizenwiki/embedvideo": "<=4", - "statamic/cms": "<=5.16", + "statamic/cms": "<5.73.22|>=6,<6.18.1", "stormpath/sdk": "<9.9.99", - "studio-42/elfinder": "<=2.1.64", + "studio-42/elfinder": "<=2.1.67", "studiomitte/friendlycaptcha": "<0.1.4", "subhh/libconnect": "<7.0.8|>=8,<8.1", "sukohi/surpass": "<1", "sulu/form-bundle": ">=2,<2.5.3", - "sulu/sulu": "<1.6.44|>=2,<2.5.25|>=2.6,<2.6.9|>=3.0.0.0-alpha1,<3.0.0.0-alpha3", + "sulu/sulu": "<=2.6.22|>=3,<=3.0.5", "sumocoders/framework-user-bundle": "<1.4", "superbig/craft-audit": "<3.0.2", "svewap/a21glossary": "<=0.4.10", @@ -19771,48 +21007,57 @@ "sylius/grid-bundle": "<1.10.1", "sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2", "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", - "sylius/sylius": "<1.12.19|>=1.13.0.0-alpha1,<1.13.4", + "sylius/sylius": "<1.9.12|>=1.10,<1.10.16|>=1.11,<1.11.17|>=1.12,<=1.12.22|>=1.13,<=1.13.14|>=1.14,<=1.14.17|>=2,<=2.0.15|>=2.1,<=2.1.11|>=2.2,<=2.2.2", "symbiote/silverstripe-multivaluefield": ">=3,<3.1", "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", "symbiote/silverstripe-seed": "<6.0.3", "symbiote/silverstripe-versionedfiles": "<=2.0.3", "symfont/process": ">=0", - "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", + "symfony/cache": ">=2,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/dom-crawler": ">=2,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4", "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4", + "symfony/html-sanitizer": ">=6.1,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/http-client": ">=4.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", - "symfony/http-foundation": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", - "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/http-foundation": "<5.4.50|>=6,<6.4.29|>=7,<7.3.7", + "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6|>=7.4,<7.4.12|>=8,<8.0.12", "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/json-path": ">=7.3,<7.4.12|>=8,<8.0.12", + "symfony/lox24-notifier": ">=7.1,<7.4.12|>=8,<8.0.12", + "symfony/mailer": ">=2,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", + "symfony/mailjet-mailer": ">=6.4,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", + "symfony/mailtrap-mailer": ">=7.2,<7.4.12|>=8,<8.0.12", "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1", - "symfony/mime": ">=4.3,<4.3.8", + "symfony/mime": ">=2,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", + "symfony/monolog-bridge": ">=2,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/polyfill": ">=1,<1.10", "symfony/polyfill-php55": ">=1,<1.10", - "symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/process": "<5.4.51|>=6,<6.4.33|>=7,<7.1.7|>=7.3,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5", "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", - "symfony/routing": ">=2,<2.0.19", - "symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/routing": ">=2,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", + "symfony/runtime": ">=5.3,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8", "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.4.10|>=7,<7.0.10|>=7.1,<7.1.3", "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9", "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", - "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", + "symfony/security-http": ">=2,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", - "symfony/symfony": "<5.4.47|>=6,<6.4.15|>=7,<7.1.8", + "symfony/symfony": "<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/translation": ">=2,<2.0.17", - "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8|>=6.4.24,<6.4.40", + "symfony/twilio-notifier": ">=6.4,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symfony/ux-autocomplete": "<2.11.2", "symfony/ux-live-component": "<2.25.1", "symfony/ux-twig-component": "<2.25.1", "symfony/validator": "<5.4.43|>=6,<6.4.11|>=7,<7.1.4", "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", - "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", + "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4|>=7.2.9,<7.4.12|>=8,<8.0.12", "symfony/webhook": ">=6.3,<6.3.8", - "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7|>=2.2.0.0-beta1,<2.2.0.0-beta2", + "symfony/yaml": ">=2,<5.4.52|>=6,<6.4.40|>=7,<7.4.12|>=8,<8.0.12", "symphonycms/symphony-2": "<2.6.4", "t3/dce": "<0.11.5|>=2.2,<2.6.2", "t3g/svg-sanitizer": "<1.0.3", @@ -19826,7 +21071,7 @@ "thelia/thelia": ">=2.1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", "thinkcmf/thinkcmf": "<6.0.8", - "thorsten/phpmyfaq": "<=4.0.1|>=4.0.7,<4.0.13", + "thorsten/phpmyfaq": "<4.1.3", "tikiwiki/tiki-manager": "<=17.1", "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", "tinymce/tinymce": "<7.2", @@ -19834,21 +21079,26 @@ "titon/framework": "<9.9.99", "tltneon/lgsl": "<7", "tobiasbg/tablepress": "<=2.0.0.0-RC1", + "tomasnorre/crawler": "<11.0.13|>=12,<12.0.11", "topthink/framework": "<6.0.17|>=6.1,<=8.0.4", "topthink/think": "<=6.1.1", "topthink/thinkphp": "<=3.2.3|>=6.1.3,<=8.0.4", - "torrentpier/torrentpier": "<=2.4.3", - "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", + "torrentpier/torrentpier": "<=2.8.8", + "tpwd/ke_search": "<5.6.2|>=6,<6.6.1|>=7,<7.0.1", "tribalsystems/zenario": "<=9.7.61188", "truckersmp/phpwhois": "<=4.3.1", "ttskch/pagination-service-provider": "<1", "twbs/bootstrap": "<3.4.1|>=4,<4.3.1", - "twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19", + "twig/cssinliner-extra": "<3.26", + "twig/intl-extra": "<3.26", + "twig/markdown-extra": "<3.26", + "twig/twig": "<3.26", + "typicms/core": "<16.1.7", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1|==14.2", "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", - "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", + "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", "typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", "typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", @@ -19860,7 +21110,8 @@ "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2", "typo3/cms-lowlevel": ">=11,<=11.5.41", "typo3/cms-recordlist": ">=11,<11.5.48", - "typo3/cms-recycler": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", + "typo3/cms-recycler": ">=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-redirects": ">=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", "typo3/cms-scheduler": ">=11,<=11.5.41", "typo3/cms-setup": ">=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11", @@ -19883,35 +21134,37 @@ "uvdesk/core-framework": "<=1.1.1", "vanilla/safecurl": "<0.9.2", "verbb/comments": "<1.5.5", - "verbb/formie": "<=2.1.43", + "verbb/formie": "<2.2.20|>=3.0.0.0-beta1,<3.1.24", "verbb/image-resizer": "<2.0.9", "verbb/knock-knock": "<1.2.8", "verot/class.upload.php": "<=2.1.6", "vertexvaar/falsftp": "<0.2.6", "villagedefrance/opencart-overclocked": "<=1.11.1", "vova07/yii2-fileapi-widget": "<0.1.9", - "vrana/adminer": "<=4.8.1", + "vrana/adminer": "<5.4.2", "vufind/vufind": ">=2,<9.1.1", "waldhacker/hcaptcha": "<2.1.2", "wallabag/tcpdf": "<6.2.22", "wallabag/wallabag": "<2.6.11", "wanglelecc/laracms": "<=1.0.3", "wapplersystems/a21glossary": "<=0.4.10", - "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9", - "web-auth/webauthn-lib": ">=4.5,<4.9", + "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9|>=5.2,<5.2.4|>=5.3,<5.3.1", + "web-auth/webauthn-lib": ">=4.5,<4.9|>=5.2,<5.2.4", + "web-auth/webauthn-symfony-bundle": ">=5.2,<5.2.4", "web-feet/coastercms": "==5.5", "web-tp3/wec_map": "<3.0.3", "webbuilders-group/silverstripe-kapost-bridge": "<0.4", "webcoast/deferred-image-processing": "<1.0.2", "webklex/laravel-imap": "<5.3", "webklex/php-imap": "<5.3", + "webonyx/graphql-php": "<=15.32.2", "webpa/webpa": "<3.1.2", "webreinvent/vaahcms": "<=2.3.1", "wikibase/wikibase": "<=1.39.3", "wikimedia/parsoid": "<0.12.2", "willdurand/js-translation-bundle": "<2.1.1", - "winter/wn-backend-module": "<1.2.4", - "winter/wn-cms-module": "<1.0.476|>=1.1,<1.1.11|>=1.2,<1.2.7", + "winter/wn-backend-module": "<1.2.12", + "winter/wn-cms-module": "<=1.2.9", "winter/wn-dusk-plugin": "<2.1", "winter/wn-system-module": "<1.2.4", "wintercms/winter": "<=1.2.3", @@ -19923,16 +21176,18 @@ "wpanel/wpanel4-cms": "<=4.3.1", "wpcloud/wp-stateless": "<3.2", "wpglobus/wpglobus": "<=1.9.6", - "wwbn/avideo": "<14.3", + "wpmetabox/meta-box": "<5.11.2", + "wwbn/avideo": "<=29", "xataface/xataface": "<3", "xpressengine/xpressengine": "<3.0.15", "yab/quarx": "<2.4.5", - "yeswiki/yeswiki": "<=4.5.4", + "yansongda/pay": "<=3.7.19", + "yeswiki/yeswiki": "<4.6.4", "yetiforce/yetiforce-crm": "<6.5", "yidashi/yii2cmf": "<=2", "yii2mod/yii2-cms": "<1.9.2", "yiisoft/yii": "<1.1.31", - "yiisoft/yii2": "<2.0.52", + "yiisoft/yii2": "<2.0.55", "yiisoft/yii2-authclient": "<2.2.15", "yiisoft/yii2-bootstrap": "<2.0.4", "yiisoft/yii2-dev": "<=2.0.45", @@ -19942,8 +21197,10 @@ "yiisoft/yii2-redis": "<2.0.20", "yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6", "yoast-seo-for-typo3/yoast_seo": "<7.2.3", - "yourls/yourls": "<=1.8.2", + "yoast/duplicate-post": "<=4.5", + "yourls/yourls": "<=1.10.2", "yuan1994/tpadmin": "<=1.3.12", + "yungifez/skuul": "<=2.6.5", "z-push/z-push-dev": "<2.7.6", "zencart/zencart": "<=1.5.7.0-beta", "zendesk/zendesk_api_client_php": "<2.2.11", @@ -19981,7 +21238,8 @@ "zf-commons/zfc-user": "<1.2.2", "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3", "zfr/zfr-oauth2-server-module": "<0.1.2", - "zoujingli/thinkadmin": "<=6.1.53" + "zoujingli/thinkadmin": "<=6.1.53", + "zumba/json-serializer": "<3.2.3" }, "default-branch": true, "type": "metapackage", @@ -20019,7 +21277,7 @@ "type": "tidelift" } ], - "time": "2025-10-17T18:06:27+00:00" + "time": "2026-05-24T20:23:40+00:00" }, { "name": "sebastian/cli-parser", @@ -20133,6 +21391,7 @@ "type": "github" } ], + "abandoned": true, "time": "2025-03-19T07:56:08+00:00" }, { @@ -20189,20 +21448,21 @@ "type": "github" } ], + "abandoned": true, "time": "2024-07-03T04:45:54+00:00" }, { "name": "sebastian/comparator", - "version": "6.3.2", + "version": "6.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", "shasum": "" }, "require": { @@ -20261,7 +21521,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" }, "funding": [ { @@ -20281,7 +21541,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T08:07:46+00:00" + "time": "2026-01-24T09:26:40+00:00" }, { "name": "sebastian/complexity", @@ -21061,27 +22321,28 @@ }, { "name": "symfony/browser-kit", - "version": "v7.3.2", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "f0b889b73a845cddef1d25fe207b37fd04cb5419" + "reference": "41850d8f8ddef9a9cd7314fa9f4902cf48885521" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/f0b889b73a845cddef1d25fe207b37fd04cb5419", - "reference": "f0b889b73a845cddef1d25fe207b37fd04cb5419", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/41850d8f8ddef9a9cd7314fa9f4902cf48885521", + "reference": "41850d8f8ddef9a9cd7314fa9f4902cf48885521", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/dom-crawler": "^6.4|^7.0" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/dom-crawler": "^6.4|^7.0|^8.0" }, "require-dev": { - "symfony/css-selector": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -21109,7 +22370,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v7.3.2" + "source": "https://github.com/symfony/browser-kit/tree/v7.4.8" }, "funding": [ { @@ -21129,34 +22390,34 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/debug-bundle", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", - "reference": "30f922edd53dd85238f1f26dbb68a044109f8f0e" + "reference": "3eb18c1e6cd16da2cea1f1b5162e442af4afee44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/30f922edd53dd85238f1f26dbb68a044109f8f0e", - "reference": "30f922edd53dd85238f1f26dbb68a044109f8f0e", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/3eb18c1e6cd16da2cea1f1b5162e442af4afee44", + "reference": "3eb18c1e6cd16da2cea1f1b5162e442af4afee44", "shasum": "" }, "require": { "composer-runtime-api": ">=2.1", "ext-xml": "*", "php": ">=8.2", - "symfony/config": "^7.3", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^7.3|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "require-dev": { - "symfony/web-profiler-bundle": "^6.4|^7.0" + "symfony/web-profiler-bundle": "^6.4|^7.0|^8.0" }, "type": "symfony-bundle", "autoload": { @@ -21184,7 +22445,7 @@ "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug-bundle/tree/v7.3.4" + "source": "https://github.com/symfony/debug-bundle/tree/v7.4.8" }, "funding": [ { @@ -21204,35 +22465,36 @@ "type": "tidelift" } ], - "time": "2025-09-10T12:00:31+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/maker-bundle", - "version": "v1.64.0", + "version": "v1.67.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "c86da84640b0586e92aee2b276ee3638ef2f425a" + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/c86da84640b0586e92aee2b276ee3638ef2f425a", - "reference": "c86da84640b0586e92aee2b276ee3638ef2f425a", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/6ce8b313845f16bcf385ee3cb31d8b24e30d5516", + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516", "shasum": "" }, "require": { + "composer-runtime-api": "^2.1", "doctrine/inflector": "^2.0", "nikic/php-parser": "^5.0", "php": ">=8.1", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" }, "conflict": { "doctrine/doctrine-bundle": "<2.10", @@ -21240,13 +22502,14 @@ }, "require-dev": { "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/doctrine-bundle": "^2.10|^3.0", "doctrine/orm": "^2.15|^3", - "symfony/http-client": "^6.4|^7.0", - "symfony/phpunit-bridge": "^6.4.1|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/security-http": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", + "doctrine/persistence": "^3.1|^4.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", "twig/twig": "^3.0|^4.x-dev" }, "type": "symfony-bundle", @@ -21281,7 +22544,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.64.0" + "source": "https://github.com/symfony/maker-bundle/tree/v1.67.0" }, "funding": [ { @@ -21292,37 +22555,37 @@ "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-23T16:12:08+00:00" + "time": "2026-03-18T13:39:06+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v7.3.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "ed77a629c13979e051b7000a317966474d566398" + "reference": "140bbbe1cd1c21a084494ccddeee33f3c3381d7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/ed77a629c13979e051b7000a317966474d566398", - "reference": "ed77a629c13979e051b7000a317966474d566398", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/140bbbe1cd1c21a084494ccddeee33f3c3381d7d", + "reference": "140bbbe1cd1c21a084494ccddeee33f3c3381d7d", "shasum": "" }, "require": { - "php": ">=7.2.5" - }, - "conflict": { - "phpunit/phpunit": "<7.5|9.1.2" + "php": ">=8.1.0" }, "require-dev": { - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/error-handler": "^5.4|^6.4|^7.0", - "symfony/polyfill-php81": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4.3|^7.0.3|^8.0" }, "bin": [ "bin/simple-phpunit" @@ -21366,7 +22629,7 @@ "testing" ], "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.3.4" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.4.8" }, "funding": [ { @@ -21386,32 +22649,32 @@ "type": "tidelift" } ], - "time": "2025-09-12T12:18:52+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v7.3.4", + "version": "v7.4.12", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "f305fa4add690bb7d6b14ab61f37c3bd061a3dd7" + "reference": "558fe81a383302318d9b92f7661deb731153c86e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/f305fa4add690bb7d6b14ab61f37c3bd061a3dd7", - "reference": "f305fa4add690bb7d6b14ab61f37c3bd061a3dd7", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/558fe81a383302318d9b92f7661deb731153c86e", + "reference": "558fe81a383302318d9b92f7661deb731153c86e", "shasum": "" }, "require": { "composer-runtime-api": ">=2.1", "php": ">=8.2", - "symfony/config": "^7.3", + "symfony/config": "^7.3|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/routing": "^6.4|^7.0", - "symfony/twig-bundle": "^6.4|^7.0", - "twig/twig": "^3.12" + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "twig/twig": "^3.15" }, "conflict": { "symfony/form": "<6.4", @@ -21421,10 +22684,11 @@ "symfony/workflow": "<7.3" }, "require-dev": { - "symfony/browser-kit": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "symfony-bundle", "autoload": { @@ -21455,7 +22719,7 @@ "dev" ], "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.3.4" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.4.12" }, "funding": [ { @@ -21475,20 +22739,20 @@ "type": "tidelift" } ], - "time": "2025-09-25T08:03:55+00:00" + "time": "2026-05-20T07:20:23+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -21517,7 +22781,7 @@ "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.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -21525,7 +22789,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], @@ -21543,11 +22807,12 @@ "ext-iconv": "*", "ext-intl": "*", "ext-json": "*", - "ext-mbstring": "*" + "ext-mbstring": "*", + "ext-zip": "*" }, "platform-dev": {}, "platform-overrides": { "php": "8.2.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/bundles.php b/config/bundles.php index ae7dc9cc..000c58a1 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -33,4 +33,5 @@ return [ Jbtronics\SettingsBundle\JbtronicsSettingsBundle::class => ['all' => true], Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true], ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], + Symfony\AI\AiBundle\AiBundle::class => ['all' => true], ]; diff --git a/config/packages/ai.yaml b/config/packages/ai.yaml new file mode 100644 index 00000000..89f8e7ae --- /dev/null +++ b/config/packages/ai.yaml @@ -0,0 +1,27 @@ +ai: + platform: + # Inference Platform configuration + # see https://github.com/symfony/ai/tree/main/src/platform#platform-bridges + + # openai: + # api_key: '%env(OPENAI_API_KEY)%' + + agent: + # Agent configuration + # see https://symfony.com/doc/current/ai/bundles/ai-bundle.html + + # default: + # platform: 'ai.platform.openai' + # model: 'gpt-5-mini' + # prompt: | + # You are a pirate and you write funny. + # tools: + # - 'Symfony\AI\Agent\Bridge\Clock\Clock' + + store: + # Store configuration + + # chromadb: + # default: + # client: 'client.service.id' + # collection: 'my_collection' diff --git a/config/packages/ai_generic_platform.yaml b/config/packages/ai_generic_platform.yaml new file mode 100644 index 00000000..c2c2e133 --- /dev/null +++ b/config/packages/ai_generic_platform.yaml @@ -0,0 +1,5 @@ +ai: + platform: + generic: + default: + base_url: '%env(GENERIC_BASE_URL)%' diff --git a/config/packages/ai_lm_studio_platform.yaml b/config/packages/ai_lm_studio_platform.yaml new file mode 100644 index 00000000..0e4287e0 --- /dev/null +++ b/config/packages/ai_lm_studio_platform.yaml @@ -0,0 +1,4 @@ +ai: + platform: + lmstudio: + host_url: '%env(string:settings:ai_lmstudio:hostURL)%' diff --git a/config/packages/ai_open_router_platform.yaml b/config/packages/ai_open_router_platform.yaml new file mode 100644 index 00000000..d34de592 --- /dev/null +++ b/config/packages/ai_open_router_platform.yaml @@ -0,0 +1,4 @@ +ai: + platform: + openrouter: + api_key: '%env(string:settings:ai_openrouter:apiKey)%' diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml index 6adea442..c1816aa2 100644 --- a/config/packages/cache.yaml +++ b/config/packages/cache.yaml @@ -23,3 +23,7 @@ framework: info_provider.cache: adapter: cache.app + + cache.settings: + adapter: cache.system + tags: true diff --git a/config/packages/datatables.yaml b/config/packages/datatables.yaml index f1ea4715..fe238a1e 100644 --- a/config/packages/datatables.yaml +++ b/config/packages/datatables.yaml @@ -8,7 +8,7 @@ datatables: # Set options, as documented at https://datatables.net/reference/option/ options: - lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated + lengthMenu : [[10, 25, 50, 100, 250, 500], [10, 25, 50, 100, 250, 500]] # We add the "All" option, when part tables are generated #pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default pageLength: 50 #TODO dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>> diff --git a/config/packages/doctrine.php b/config/packages/doctrine.php index 47584ed7..e5be011f 100644 --- a/config/packages/doctrine.php +++ b/config/packages/doctrine.php @@ -20,12 +20,14 @@ declare(strict_types=1); +use Symfony\Config\DoctrineConfig; + /** * This class extends the default doctrine ORM configuration to enable native lazy objects on PHP 8.4+. * We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version. */ -return static function(\Symfony\Config\DoctrineConfig $doctrine) { +return static function(DoctrineConfig $doctrine) { //On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies. if (PHP_VERSION_ID >= 80400) { $doctrine->orm()->enableNativeLazyObjects(true); diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 5261c295..164ac717 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -56,6 +56,7 @@ doctrine: natsort: App\Doctrine\Functions\Natsort array_position: App\Doctrine\Functions\ArrayPosition ilike: App\Doctrine\Functions\ILike + si_value_sort: App\Doctrine\Functions\SiValueSort when@test: doctrine: diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 6843a177..dd8f30a5 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=../../vendor/symfony/dependency-injection/Loader/schema/services.schema.json # see https://symfony.com/doc/current/reference/configuration/framework.html framework: secret: '%env(APP_SECRET)%' @@ -8,6 +9,7 @@ framework: # Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore # TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278) http_method_override: true + allowed_http_method_override: ['DELETE'] # Allow users to configure trusted hosts via .env variables # see https://symfony.com/doc/current/reference/configuration/framework.html#trusted-hosts diff --git a/config/packages/knpu_oauth2_client.yaml b/config/packages/knpu_oauth2_client.yaml index 5e56d5c5..4967684e 100644 --- a/config/packages/knpu_oauth2_client.yaml +++ b/config/packages/knpu_oauth2_client.yaml @@ -35,4 +35,4 @@ knpu_oauth2_client: provider_options: urlAuthorize: 'https://identity.nexar.com/connect/authorize' urlAccessToken: 'https://identity.nexar.com/connect/token' - urlResourceOwnerDetails: '' \ No newline at end of file + urlResourceOwnerDetails: '' diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 725ebd7c..387d71ad 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -10,14 +10,6 @@ when@dev: path: "%kernel.logs_dir%/%kernel.environment%.log" level: debug channels: ["!event"] - # uncomment to get logging in your browser - # you may have to allow bigger header sizes in your Web server configuration - #firephp: - # type: firephp - # level: info - #chromephp: - # type: chromephp - # level: info console: type: console process_psr_3_messages: false @@ -45,6 +37,7 @@ when@prod: action_level: error handler: nested excluded_http_codes: [404, 405] + channels: ["!deprecation"] buffer_size: 50 # How many messages should be saved? Prevent memory leaks nested: type: stream diff --git a/config/packages/settings.yaml b/config/packages/settings.yaml index c16d1804..b3d209f6 100644 --- a/config/packages/settings.yaml +++ b/config/packages/settings.yaml @@ -3,6 +3,7 @@ jbtronics_settings: cache: default_cacheable: true + service: 'cache.settings' orm_storage: default_entity_class: App\Entity\SettingsEntry diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 674aa317..860cef42 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,6 +1,6 @@ twig: default_path: '%kernel.project_dir%/templates' - form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig'] + form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig', 'form/synonyms_collection.html.twig'] paths: '%kernel.project_dir%/assets/css': css @@ -18,6 +18,11 @@ twig: saml_enabled: '%partdb.saml.enabled%' part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator' + # Bootstrap grid classes used for horizontal form layouts + col_label: 'col-sm-3 col-lg-2' # The column classes for form labels + col_input: 'col-sm-9 col-lg-10' # The column classes for form input fields + offset_label: 'offset-sm-3 offset-lg-2' # Offset classes for elements that should be aligned with the input fields (e.g., submit buttons) + when@test: twig: - strict_variables: true \ No newline at end of file + strict_variables: true diff --git a/config/packages/ux_translator.yaml b/config/packages/ux_translator.yaml index 1c1c7060..46d37964 100644 --- a/config/packages/ux_translator.yaml +++ b/config/packages/ux_translator.yaml @@ -1,3 +1,12 @@ ux_translator: # The directory where the JavaScript translations are dumped dump_directory: '%kernel.project_dir%/var/translations' + # Only include the frontend translation domain in the JavaScript bundle + domains: + - 'frontend' + +when@prod: + ux_translator: + # Control whether TypeScript types are dumped alongside translations. + # Disable this if you do not use TypeScript (e.g. in production when using AssetMapper), to speed up cache warmup. + # dump_typescript: false diff --git a/config/parameters.yaml b/config/parameters.yaml index d4fe7581..b1aa5314 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -10,7 +10,7 @@ parameters: partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage) partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl', 'hu'] # The languages that are shown in user drop down menu - partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails + partdb.default_uri: '%env(addSlash:string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails partdb.db.emulate_natural_sort: '%env(bool:DATABASE_EMULATE_NATURAL_SORT)%' # If this is set to true, natural sorting is emulated on platforms that do not support it natively. This can be slow on large datasets. @@ -105,6 +105,8 @@ parameters: env(DATABASE_EMULATE_NATURAL_SORT): 0 + env(ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK): 0 + ###################################################################################################################### # Bulk Info Provider Import Configuration ###################################################################################################################### diff --git a/config/permissions.yaml b/config/permissions.yaml index 8cbd60c3..39e91b57 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -18,13 +18,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co parts: # e.g. this maps to perms_parts in User/Group database group: "data" - label: "perm.parts" + label: "[[Part]]" operations: # Here are all possible operations are listed => the op name is mapped to bit value read: label: "perm.read" # If a part can be read by a user, he can also see all the datastructures (except devices) alsoSet: ['storelocations.read', 'footprints.read', 'categories.read', 'suppliers.read', 'manufacturers.read', - 'currencies.read', 'attachment_types.read', 'measurement_units.read'] + 'currencies.read', 'attachment_types.read', 'measurement_units.read', 'part_custom_states.read'] apiTokenRole: ROLE_API_READ_ONLY edit: label: "perm.edit" @@ -68,10 +68,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co move: label: "perm.parts_stock.move" apiTokenRole: ROLE_API_EDIT + stocktake: + label: "perm.parts_stock.stocktake" + apiTokenRole: ROLE_API_EDIT storelocations: &PART_CONTAINING - label: "perm.storelocations" + label: "[[Storage_location]]" group: "data" operations: read: @@ -103,35 +106,39 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co footprints: <<: *PART_CONTAINING - label: "perm.part.footprints" + label: "[[Footprint]]" categories: <<: *PART_CONTAINING - label: "perm.part.categories" + label: "[[Category]]" suppliers: <<: *PART_CONTAINING - label: "perm.part.supplier" + label: "[[Supplier]]" manufacturers: <<: *PART_CONTAINING - label: "perm.part.manufacturers" + label: "[[Manufacturer]]" projects: <<: *PART_CONTAINING - label: "perm.projects" + label: "[[Project]]" attachment_types: <<: *PART_CONTAINING - label: "perm.part.attachment_types" + label: "[[Attachment_type]]" currencies: <<: *PART_CONTAINING - label: "perm.currencies" + label: "[[Currency]]" measurement_units: <<: *PART_CONTAINING - label: "perm.measurement_units" + label: "[[Measurement_unit]]" + + part_custom_states: + <<: *PART_CONTAINING + label: "[[Part_custom_state]]" tools: label: "perm.part.tools" @@ -293,6 +300,10 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co show_updates: label: "perm.system.show_available_updates" apiTokenRole: ROLE_API_ADMIN + manage_updates: + label: "perm.system.manage_updates" + alsoSet: ['show_updates', 'server_infos'] + apiTokenRole: ROLE_API_ADMIN attachments: @@ -373,4 +384,4 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co manage_tokens: label: "perm.api.manage_tokens" alsoSet: ['access_api'] - apiTokenRole: ROLE_API_FULL \ No newline at end of file + apiTokenRole: ROLE_API_FULL diff --git a/config/reference.php b/config/reference.php new file mode 100644 index 00000000..38a275d6 --- /dev/null +++ b/config/reference.php @@ -0,0 +1,3448 @@ + [ + * 'App\\' => [ + * 'resource' => '../src/', + * ], + * ], + * ]); + * ``` + * + * @psalm-type ImportsConfig = list + * @psalm-type ParametersConfig = array|Param|null>|Param|null> + * @psalm-type ArgumentsType = list|array + * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} + * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key + * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator|ExpressionConfigurator + * @psalm-type DeprecationType = array{package: string, version: string, message?: string} + * @psalm-type DefaultsType = array{ + * public?: bool, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type InstanceofType = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type DefinitionType = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * configurator?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * decorates?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: CallbackType, + * } + * @psalm-type AliasType = string|array{ + * alias: string, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type PrototypeType = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type StackType = array{ + * stack: list>, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type ServicesConfig = array{ + * _defaults?: DefaultsType, + * _instanceof?: InstanceofType, + * ... + * } + * @psalm-type ExtensionType = array + * @psalm-type FrameworkConfig = array{ + * secret?: scalar|Param|null, + * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false + * allowed_http_method_override?: null|list, + * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" + * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%" + * test?: bool|Param, + * default_locale?: scalar|Param|null, // Default: "en" + * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false + * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false + * enabled_locales?: list, + * trusted_hosts?: string|list, + * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] + * trusted_headers?: string|list, + * error_controller?: scalar|Param|null, // Default: "error_controller" + * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * stateless_token_ids?: list, + * check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false + * cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token" + * }, + * form?: bool|array{ // Form configuration + * enabled?: bool|Param, // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * token_id?: scalar|Param|null, // Default: null + * field_name?: scalar|Param|null, // Default: "_token" + * field_attr?: array, + * }, + * }, + * http_cache?: bool|array{ // HTTP cache configuration + * enabled?: bool|Param, // Default: false + * debug?: bool|Param, // Default: "%kernel.debug%" + * trace_level?: "none"|"short"|"full"|Param, + * trace_header?: scalar|Param|null, + * default_ttl?: int|Param, + * private_headers?: list, + * skip_response_headers?: list, + * allow_reload?: bool|Param, + * allow_revalidate?: bool|Param, + * stale_while_revalidate?: int|Param, + * stale_if_error?: int|Param, + * terminate_on_cache_hit?: bool|Param, + * }, + * esi?: bool|array{ // ESI configuration + * enabled?: bool|Param, // Default: false + * }, + * ssi?: bool|array{ // SSI configuration + * enabled?: bool|Param, // Default: false + * }, + * fragments?: bool|array{ // Fragments configuration + * enabled?: bool|Param, // Default: false + * hinclude_default_template?: scalar|Param|null, // Default: null + * path?: scalar|Param|null, // Default: "/_fragment" + * }, + * profiler?: bool|array{ // Profiler configuration + * enabled?: bool|Param, // Default: false + * collect?: bool|Param, // Default: true + * collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null + * only_exceptions?: bool|Param, // Default: false + * only_main_requests?: bool|Param, // Default: false + * dsn?: scalar|Param|null, // Default: "file:%kernel.cache_dir%/profiler" + * collect_serializer_data?: bool|Param, // Enables the serializer data collector and profiler panel. // Default: false + * }, + * workflows?: bool|array{ + * enabled?: bool|Param, // Default: false + * workflows?: array, + * definition_validators?: list, + * support_strategy?: scalar|Param|null, + * initial_marking?: \BackedEnum|string|list, + * events_to_dispatch?: null|list, + * places?: string|list, + * }>, + * transitions?: list, + * to?: \BackedEnum|string|list, + * weight?: int|Param, // Default: 1 + * metadata?: array, + * }>, + * metadata?: array, + * }>, + * }, + * router?: bool|array{ // Router configuration + * enabled?: bool|Param, // Default: false + * resource?: scalar|Param|null, + * type?: scalar|Param|null, + * cache_dir?: scalar|Param|null, // Deprecated: Setting the "framework.router.cache_dir.cache_dir" configuration option is deprecated. It will be removed in version 8.0. // Default: "%kernel.build_dir%" + * default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null + * http_port?: scalar|Param|null, // Default: 80 + * https_port?: scalar|Param|null, // Default: 443 + * strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true + * utf8?: bool|Param, // Default: true + * }, + * session?: bool|array{ // Session configuration + * enabled?: bool|Param, // Default: false + * storage_factory_id?: scalar|Param|null, // Default: "session.storage.factory.native" + * handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null. + * name?: scalar|Param|null, + * cookie_lifetime?: scalar|Param|null, + * cookie_path?: scalar|Param|null, + * cookie_domain?: scalar|Param|null, + * cookie_secure?: true|false|"auto"|Param, // Default: "auto" + * cookie_httponly?: bool|Param, // Default: true + * cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * use_cookies?: bool|Param, + * gc_divisor?: scalar|Param|null, + * gc_probability?: scalar|Param|null, + * gc_maxlifetime?: scalar|Param|null, + * save_path?: scalar|Param|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null. + * metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0 + * sid_length?: int|Param, // Deprecated: Setting the "framework.session.sid_length.sid_length" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option. + * sid_bits_per_character?: int|Param, // Deprecated: Setting the "framework.session.sid_bits_per_character.sid_bits_per_character" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option. + * }, + * request?: bool|array{ // Request configuration + * enabled?: bool|Param, // Default: false + * formats?: array>, + * }, + * assets?: bool|array{ // Assets configuration + * enabled?: bool|Param, // Default: true + * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false + * version_strategy?: scalar|Param|null, // Default: null + * version?: scalar|Param|null, // Default: null + * version_format?: scalar|Param|null, // Default: "%%s?%%s" + * json_manifest_path?: scalar|Param|null, // Default: null + * base_path?: scalar|Param|null, // Default: "" + * base_urls?: string|list, + * packages?: array, + * }>, + * }, + * asset_mapper?: bool|array{ // Asset Mapper configuration + * enabled?: bool|Param, // Default: false + * paths?: string|array, + * excluded_patterns?: list, + * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true + * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true + * public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/" + * missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn" + * extensions?: array, + * importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php" + * importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims" + * importmap_script_attributes?: array, + * vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor" + * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip. + * enabled?: bool|Param, // Default: false + * formats?: list, + * extensions?: list, + * }, + * }, + * translator?: bool|array{ // Translator configuration + * enabled?: bool|Param, // Default: true + * fallbacks?: string|list, + * logging?: bool|Param, // Default: false + * formatter?: scalar|Param|null, // Default: "translator.formatter.default" + * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations" + * default_path?: scalar|Param|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations" + * paths?: list, + * pseudo_localization?: bool|array{ + * enabled?: bool|Param, // Default: false + * accents?: bool|Param, // Default: true + * expansion_factor?: float|Param, // Default: 1.0 + * brackets?: bool|Param, // Default: true + * parse_html?: bool|Param, // Default: false + * localizable_html_attributes?: list, + * }, + * providers?: array, + * locales?: list, + * }>, + * globals?: array, + * domain?: string|Param, + * }>, + * }, + * validation?: bool|array{ // Validation configuration + * enabled?: bool|Param, // Default: true + * cache?: scalar|Param|null, // Deprecated: Setting the "framework.validation.cache.cache" configuration option is deprecated. It will be removed in version 8.0. + * enable_attributes?: bool|Param, // Default: true + * static_method?: string|list, + * translation_domain?: scalar|Param|null, // Default: "validators" + * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|"loose"|Param, // Default: "html5" + * mapping?: array{ + * paths?: list, + * }, + * not_compromised_password?: bool|array{ + * enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true + * endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null + * }, + * disable_translation?: bool|Param, // Default: false + * auto_mapping?: array, + * }>, + * }, + * annotations?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * serializer?: bool|array{ // Serializer configuration + * enabled?: bool|Param, // Default: true + * enable_attributes?: bool|Param, // Default: true + * name_converter?: scalar|Param|null, + * circular_reference_handler?: scalar|Param|null, + * max_depth_handler?: scalar|Param|null, + * mapping?: array{ + * paths?: list, + * }, + * default_context?: array, + * named_serializers?: array, + * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true + * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true + * }>, + * }, + * property_access?: bool|array{ // Property access configuration + * enabled?: bool|Param, // Default: true + * magic_call?: bool|Param, // Default: false + * magic_get?: bool|Param, // Default: true + * magic_set?: bool|Param, // Default: true + * throw_exception_on_invalid_index?: bool|Param, // Default: false + * throw_exception_on_invalid_property_path?: bool|Param, // Default: true + * }, + * type_info?: bool|array{ // Type info configuration + * enabled?: bool|Param, // Default: true + * aliases?: array, + * }, + * property_info?: bool|array{ // Property info configuration + * enabled?: bool|Param, // Default: true + * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. + * }, + * cache?: array{ // Cache configuration + * prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%" + * app?: scalar|Param|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem" + * system?: scalar|Param|null, // System related cache pools configuration. // Default: "cache.adapter.system" + * directory?: scalar|Param|null, // Default: "%kernel.share_dir%/pools/app" + * default_psr6_provider?: scalar|Param|null, + * default_redis_provider?: scalar|Param|null, // Default: "redis://localhost" + * default_valkey_provider?: scalar|Param|null, // Default: "valkey://localhost" + * default_memcached_provider?: scalar|Param|null, // Default: "memcached://localhost" + * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection" + * default_pdo_provider?: scalar|Param|null, // Default: null + * pools?: array, + * tags?: scalar|Param|null, // Default: null + * public?: bool|Param, // Default: false + * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. + * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter. + * early_expiration_message_bus?: scalar|Param|null, + * clearer?: scalar|Param|null, + * }>, + * }, + * php_errors?: array{ // PHP errors handling configuration + * log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true + * throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true + * }, + * exceptions?: array, + * web_link?: bool|array{ // Web links configuration + * enabled?: bool|Param, // Default: true + * }, + * lock?: bool|string|array{ // Lock configuration + * enabled?: bool|Param, // Default: false + * resources?: string|array>, + * }, + * semaphore?: bool|string|array{ // Semaphore configuration + * enabled?: bool|Param, // Default: false + * resources?: string|array, + * }, + * messenger?: bool|array{ // Messenger configuration + * enabled?: bool|Param, // Default: false + * routing?: array, + * }>, + * serializer?: array{ + * default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" + * symfony_serializer?: array{ + * format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json" + * context?: array, + * }, + * }, + * transports?: array, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * retry_strategy?: string|array{ + * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 + * }, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null + * }>, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * stop_worker_on_signals?: int|string|list, + * default_bus?: scalar|Param|null, // Default: null + * buses?: array, + * }>, + * }>, + * }, + * scheduler?: bool|array{ // Scheduler configuration + * enabled?: bool|Param, // Default: false + * }, + * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true + * http_client?: bool|array{ // HTTP Client configuration + * enabled?: bool|Param, // Default: true + * max_host_connections?: int|Param, // The maximum number of connections to a single host. + * default_options?: array{ + * headers?: array, + * vars?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...) + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }, + * mock_response_factory?: scalar|Param|null, // The id of the service that should generate mock responses. It should be either an invokable or an iterable. + * scoped_clients?: array, + * headers?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...). + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }>, + * }, + * mailer?: bool|array{ // Mailer configuration + * enabled?: bool|Param, // Default: true + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * dsn?: scalar|Param|null, // Default: null + * transports?: array, + * envelope?: array{ // Mailer Envelope configuration + * sender?: scalar|Param|null, + * recipients?: string|list, + * allowed_recipients?: string|list, + * }, + * headers?: array, + * dkim_signer?: bool|array{ // DKIM signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" + * domain?: scalar|Param|null, // Default: "" + * select?: scalar|Param|null, // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: "" + * options?: array, + * }, + * smime_signer?: bool|array{ // S/MIME signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Path to key (in PEM format) // Default: "" + * certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: null + * extra_certificates?: scalar|Param|null, // Default: null + * sign_options?: int|Param, // Default: null + * }, + * smime_encrypter?: bool|array{ // S/MIME encrypter configuration + * enabled?: bool|Param, // Default: false + * repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: "" + * cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null + * }, + * }, + * secrets?: bool|array{ + * enabled?: bool|Param, // Default: true + * vault_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%" + * local_dotenv_file?: scalar|Param|null, // Default: "%kernel.project_dir%/.env.%kernel.environment%.local" + * decryption_env_var?: scalar|Param|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET" + * }, + * notifier?: bool|array{ // Notifier configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * chatter_transports?: array, + * texter_transports?: array, + * notification_on_failed_messages?: bool|Param, // Default: false + * channel_policy?: array>, + * admin_recipients?: list, + * }, + * rate_limiter?: bool|array{ // Rate limiter configuration + * enabled?: bool|Param, // Default: true + * limiters?: array, + * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. + * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". + * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1 + * }, + * }>, + * }, + * uid?: bool|array{ // Uid configuration + * enabled?: bool|Param, // Default: true + * default_uuid_version?: 7|6|4|1|Param, // Default: 7 + * name_based_uuid_version?: 5|3|Param, // Default: 5 + * name_based_uuid_namespace?: scalar|Param|null, + * time_based_uuid_version?: 7|6|1|Param, // Default: 7 + * time_based_uuid_node?: scalar|Param|null, + * }, + * html_sanitizer?: bool|array{ // HtmlSanitizer configuration + * enabled?: bool|Param, // Default: false + * sanitizers?: array, + * block_elements?: string|list, + * drop_elements?: string|list, + * allow_attributes?: array, + * drop_attributes?: array, + * force_attributes?: array>, + * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false + * allowed_link_schemes?: string|list, + * allowed_link_hosts?: null|string|list, + * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false + * allowed_media_schemes?: string|list, + * allowed_media_hosts?: null|string|list, + * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false + * with_attribute_sanitizers?: string|list, + * without_attribute_sanitizers?: string|list, + * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 + * }>, + * }, + * webhook?: bool|array{ // Webhook configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus" + * routing?: array, + * }, + * remote-event?: bool|array{ // RemoteEvent configuration + * enabled?: bool|Param, // Default: false + * }, + * json_streamer?: bool|array{ // JSON streamer configuration + * enabled?: bool|Param, // Default: false + * }, + * } + * @psalm-type DoctrineConfig = array{ + * dbal?: array{ + * default_connection?: scalar|Param|null, + * types?: array, + * driver_schemes?: array, + * connections?: array, + * mapping_types?: array, + * default_table_options?: array, + * schema_manager_factory?: scalar|Param|null, // Default: "doctrine.dbal.default_schema_manager_factory" + * result_cache?: scalar|Param|null, + * slaves?: array, + * replicas?: array, + * }>, + * }, + * orm?: array{ + * default_entity_manager?: scalar|Param|null, + * auto_generate_proxy_classes?: scalar|Param|null, // Auto generate mode possible values are: "NEVER", "ALWAYS", "FILE_NOT_EXISTS", "EVAL", "FILE_NOT_EXISTS_OR_CHANGED", this option is ignored when the "enable_native_lazy_objects" option is true // Default: false + * enable_lazy_ghost_objects?: bool|Param, // Enables the new implementation of proxies based on lazy ghosts instead of using the legacy implementation // Default: true + * enable_native_lazy_objects?: bool|Param, // Enables the new native implementation of PHP lazy objects instead of generated proxies // Default: false + * proxy_dir?: scalar|Param|null, // Configures the path where generated proxy classes are saved when using non-native lazy objects, this option is ignored when the "enable_native_lazy_objects" option is true // Default: "%kernel.build_dir%/doctrine/orm/Proxies" + * proxy_namespace?: scalar|Param|null, // Defines the root namespace for generated proxy classes when using non-native lazy objects, this option is ignored when the "enable_native_lazy_objects" option is true // Default: "Proxies" + * controller_resolver?: bool|array{ + * enabled?: bool|Param, // Default: true + * auto_mapping?: bool|Param|null, // Set to false to disable using route placeholders as lookup criteria when the primary key doesn't match the argument name // Default: null + * evict_cache?: bool|Param, // Set to true to fetch the entity from the database instead of using the cache, if any // Default: false + * }, + * entity_managers?: array, + * }>, + * }>, + * }, + * connection?: scalar|Param|null, + * class_metadata_factory_name?: scalar|Param|null, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" + * default_repository_class?: scalar|Param|null, // Default: "Doctrine\\ORM\\EntityRepository" + * auto_mapping?: scalar|Param|null, // Default: false + * naming_strategy?: scalar|Param|null, // Default: "doctrine.orm.naming_strategy.default" + * quote_strategy?: scalar|Param|null, // Default: "doctrine.orm.quote_strategy.default" + * typed_field_mapper?: scalar|Param|null, // Default: "doctrine.orm.typed_field_mapper.default" + * entity_listener_resolver?: scalar|Param|null, // Default: null + * fetch_mode_subselect_batch_size?: scalar|Param|null, + * repository_factory?: scalar|Param|null, // Default: "doctrine.orm.container_repository_factory" + * schema_ignore_classes?: list, + * report_fields_where_declared?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.16 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/10455. // Default: true + * validate_xml_mapping?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14. See https://github.com/doctrine/orm/pull/6728. // Default: false + * second_level_cache?: array{ + * region_cache_driver?: string|array{ + * type?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * pool?: scalar|Param|null, + * }, + * region_lock_lifetime?: scalar|Param|null, // Default: 60 + * log_enabled?: bool|Param, // Default: true + * region_lifetime?: scalar|Param|null, // Default: 3600 + * enabled?: bool|Param, // Default: true + * factory?: scalar|Param|null, + * regions?: array, + * loggers?: array, + * }, + * hydrators?: array, + * mappings?: array, + * dql?: array{ + * string_functions?: array, + * numeric_functions?: array, + * datetime_functions?: array, + * }, + * filters?: array, + * }>, + * identity_generation_preferences?: array, + * }>, + * resolve_target_entities?: array, + * }, + * } + * @psalm-type DoctrineMigrationsConfig = array{ + * enable_service_migrations?: bool|Param, // Whether to enable fetching migrations from the service container. // Default: false + * migrations_paths?: array, + * services?: array, + * factories?: array, + * storage?: array{ // Storage to use for migration status metadata. + * table_storage?: array{ // The default metadata storage, implemented as a table in the database. + * table_name?: scalar|Param|null, // Default: null + * version_column_name?: scalar|Param|null, // Default: null + * version_column_length?: scalar|Param|null, // Default: null + * executed_at_column_name?: scalar|Param|null, // Default: null + * execution_time_column_name?: scalar|Param|null, // Default: null + * }, + * }, + * migrations?: list, + * connection?: scalar|Param|null, // Connection name to use for the migrations database. // Default: null + * em?: scalar|Param|null, // Entity manager name to use for the migrations database (available when doctrine/orm is installed). // Default: null + * all_or_nothing?: scalar|Param|null, // Run all migrations in a transaction. // Default: false + * check_database_platform?: scalar|Param|null, // Adds an extra check in the generated migrations to allow execution only on the same platform as they were initially generated on. // Default: true + * custom_template?: scalar|Param|null, // Custom template path for generated migration classes. // Default: null + * organize_migrations?: scalar|Param|null, // Organize migrations mode. Possible values are: "BY_YEAR", "BY_YEAR_AND_MONTH", false // Default: false + * enable_profiler?: bool|Param, // Whether or not to enable the profiler collector to calculate and visualize migration status. This adds some queries overhead. // Default: false + * transactional?: bool|Param, // Whether or not to wrap migrations in a single transaction. // Default: true + * } + * @psalm-type SecurityConfig = array{ + * access_denied_url?: scalar|Param|null, // Default: null + * session_fixation_strategy?: "none"|"migrate"|"invalidate"|Param, // Default: "migrate" + * hide_user_not_found?: bool|Param, // Deprecated: The "hide_user_not_found" option is deprecated and will be removed in 8.0. Use the "expose_security_errors" option instead. + * expose_security_errors?: \Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::None|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::AccountStatus|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::All|Param, // Default: "none" + * erase_credentials?: bool|Param, // Default: true + * access_decision_manager?: array{ + * strategy?: "affirmative"|"consensus"|"unanimous"|"priority"|Param, + * service?: scalar|Param|null, + * strategy_service?: scalar|Param|null, + * allow_if_all_abstain?: bool|Param, // Default: false + * allow_if_equal_granted_denied?: bool|Param, // Default: true + * }, + * password_hashers?: array, + * hash_algorithm?: scalar|Param|null, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512" + * key_length?: scalar|Param|null, // Default: 40 + * ignore_case?: bool|Param, // Default: false + * encode_as_base64?: bool|Param, // Default: true + * iterations?: scalar|Param|null, // Default: 5000 + * cost?: int|Param, // Default: null + * memory_cost?: scalar|Param|null, // Default: null + * time_cost?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * }>, + * providers?: array, + * }, + * entity?: array{ + * class?: scalar|Param|null, // The full entity class name of your user class. + * property?: scalar|Param|null, // Default: null + * manager_name?: scalar|Param|null, // Default: null + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service?: scalar|Param|null, + * base_dn?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: null + * search_password?: scalar|Param|null, // Default: null + * extra_fields?: list, + * default_roles?: string|list, + * role_fetcher?: scalar|Param|null, // Default: null + * uid_key?: scalar|Param|null, // Default: "sAMAccountName" + * filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|Param|null, // Default: null + * }, + * saml?: array{ + * user_class?: scalar|Param|null, + * default_roles?: list, + * }, + * }>, + * firewalls?: array, + * security?: bool|Param, // Default: true + * user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|Param|null, + * access_denied_url?: scalar|Param|null, + * access_denied_handler?: scalar|Param|null, + * entry_point?: scalar|Param|null, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". + * provider?: scalar|Param|null, + * stateless?: bool|Param, // Default: false + * lazy?: bool|Param, // Default: false + * context?: scalar|Param|null, + * logout?: array{ + * enable_csrf?: bool|Param|null, // Default: null + * csrf_token_id?: scalar|Param|null, // Default: "logout" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_manager?: scalar|Param|null, + * path?: scalar|Param|null, // Default: "/logout" + * target?: scalar|Param|null, // Default: "/" + * invalidate_session?: bool|Param, // Default: true + * clear_site_data?: string|list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>, + * delete_cookies?: string|array, + * }, + * switch_user?: array{ + * provider?: scalar|Param|null, + * parameter?: scalar|Param|null, // Default: "_switch_user" + * role?: scalar|Param|null, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|Param|null, // Default: null + * }, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|Param|null, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int|Param, // Default: 5 + * interval?: scalar|Param|null, // Default: "1 minute" + * lock_factory?: scalar|Param|null, // The service ID of the lock factory used by the login rate limiter (or null to disable locking). // Default: null + * cache_pool?: string|Param, // The cache pool to use for storing the limiter state // Default: "cache.rate_limiter" + * storage_service?: string|Param, // The service ID of a custom storage implementation, this precedes any configured "cache_pool" // Default: null + * }, + * two_factor?: array{ + * check_path?: scalar|Param|null, // Default: "/2fa_check" + * post_only?: bool|Param, // Default: true + * auth_form_path?: scalar|Param|null, // Default: "/2fa" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * success_handler?: scalar|Param|null, // Default: null + * failure_handler?: scalar|Param|null, // Default: null + * authentication_required_handler?: scalar|Param|null, // Default: null + * auth_code_parameter_name?: scalar|Param|null, // Default: "_auth_code" + * trusted_parameter_name?: scalar|Param|null, // Default: "_trusted" + * remember_me_sets_trusted?: scalar|Param|null, // Default: false + * multi_factor?: bool|Param, // Default: false + * prepare_on_login?: bool|Param, // Default: false + * prepare_on_access_denied?: bool|Param, // Default: false + * enable_csrf?: scalar|Param|null, // Default: false + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "two_factor" + * csrf_header?: scalar|Param|null, // Default: null + * csrf_token_manager?: scalar|Param|null, // Default: "scheb_two_factor.csrf_token_manager" + * provider?: scalar|Param|null, // Default: null + * }, + * webauthn?: array{ + * user_provider?: scalar|Param|null, // Default: null + * options_storage?: scalar|Param|null, // Deprecated: The child node "options_storage" at path "security.firewalls..webauthn.options_storage" is deprecated. Please use the root option "options_storage" instead. // Default: null + * success_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultSuccessHandler" + * failure_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultFailureHandler" + * secured_rp_ids?: array, + * authentication?: bool|array{ + * enabled?: bool|Param, // Default: true + * profile?: scalar|Param|null, // Default: "default" + * options_builder?: scalar|Param|null, // Default: null + * routes?: array{ + * host?: scalar|Param|null, // Default: null + * options_method?: scalar|Param|null, // Default: "POST" + * options_path?: scalar|Param|null, // Default: "/login/options" + * result_method?: scalar|Param|null, // Default: "POST" + * result_path?: scalar|Param|null, // Default: "/login" + * }, + * options_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultRequestOptionsHandler" + * }, + * registration?: bool|array{ + * enabled?: bool|Param, // Default: false + * hide_existing_credentials?: bool|Param, // Default: true + * profile?: scalar|Param|null, // Default: "default" + * options_builder?: scalar|Param|null, // Default: null + * routes?: array{ + * host?: scalar|Param|null, // Default: null + * options_method?: scalar|Param|null, // Default: "POST" + * options_path?: scalar|Param|null, // Default: "/register/options" + * result_method?: scalar|Param|null, // Default: "POST" + * result_path?: scalar|Param|null, // Default: "/register" + * }, + * options_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultCreationOptionsHandler" + * }, + * }, + * x509?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|Param|null, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "REMOTE_USER" + * }, + * saml?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, // Default: "Nbgrp\\OneloginSamlBundle\\Security\\Http\\Authentication\\SamlAuthenticationSuccessHandler" + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * identifier_attribute?: scalar|Param|null, // Default: null + * use_attribute_friendly_name?: bool|Param, // Default: false + * user_factory?: scalar|Param|null, // Default: null + * token_factory?: scalar|Param|null, // Default: null + * persist_user?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * login_link?: array{ + * check_route?: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|Param|null, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false + * signature_properties?: list, + * lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600 + * max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null + * used_link_cache?: scalar|Param|null, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|Param|null, // The user provider to load users from. + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * login_path?: scalar|Param|null, // Default: "/login" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: null + * token_extractors?: string|list, + * token_handler?: string|array{ + * id?: scalar|Param|null, + * oidc_user_info?: string|array{ + * base_uri?: scalar|Param|null, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured). + * discovery?: array{ // Enable the OIDC discovery. + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|Param|null, // HttpClient service id to use to call the OIDC server. + * }, + * oidc?: array{ + * discovery?: array{ // Enable the OIDC discovery. + * base_uri?: string|list, + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience?: scalar|Param|null, // Audience set in the token, for validation purpose. + * issuers?: list, + * algorithm?: array, + * algorithms?: list, + * key?: scalar|Param|null, // Deprecated: The "key" option is deprecated and will be removed in 8.0. Use the "keyset" option instead. // JSON-encoded JWK used to sign the token (must contain a "kty" key). + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys). + * encryption?: bool|array{ + * enabled?: bool|Param, // Default: false + * enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false + * algorithms?: list, + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url?: scalar|Param|null, // CAS server validation URL + * prefix?: scalar|Param|null, // CAS prefix // Default: "cas" + * http_client?: scalar|Param|null, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|Param|null, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * service?: scalar|Param|null, + * user_providers?: string|list, + * catch_exceptions?: bool|Param, // Default: true + * signature_properties?: list, + * token_provider?: string|array{ + * service?: scalar|Param|null, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: false + * connection?: scalar|Param|null, // Default: null + * }, + * }, + * token_verifier?: scalar|Param|null, // The service ID of a custom rememberme token verifier. + * name?: scalar|Param|null, // Default: "REMEMBERME" + * lifetime?: int|Param, // Default: 31536000 + * path?: scalar|Param|null, // Default: "/" + * domain?: scalar|Param|null, // Default: null + * secure?: true|false|"auto"|Param, // Default: null + * httponly?: bool|Param, // Default: true + * samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * always_remember_me?: bool|Param, // Default: false + * remember_me_parameter?: scalar|Param|null, // Default: "_remember_me" + * }, + * }>, + * access_control?: list, + * attributes?: array, + * route?: scalar|Param|null, // Default: null + * methods?: string|list, + * allow_if?: scalar|Param|null, // Default: null + * roles?: string|list, + * }>, + * role_hierarchy?: array>, + * } + * @psalm-type TwigConfig = array{ + * form_themes?: list, + * globals?: array, + * autoescape_service?: scalar|Param|null, // Default: null + * autoescape_service_method?: scalar|Param|null, // Default: null + * base_template_class?: scalar|Param|null, // Deprecated: The child node "base_template_class" at path "twig.base_template_class" is deprecated. + * cache?: scalar|Param|null, // Default: true + * charset?: scalar|Param|null, // Default: "%kernel.charset%" + * debug?: bool|Param, // Default: "%kernel.debug%" + * strict_variables?: bool|Param, // Default: "%kernel.debug%" + * auto_reload?: scalar|Param|null, + * optimizations?: int|Param, + * default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" + * file_name_pattern?: string|list, + * paths?: array, + * date?: array{ // The default format options used by the date filter. + * format?: scalar|Param|null, // Default: "F j, Y H:i" + * interval_format?: scalar|Param|null, // Default: "%d days" + * timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null + * }, + * number_format?: array{ // The default format options for the number_format filter. + * decimals?: int|Param, // Default: 0 + * decimal_point?: scalar|Param|null, // Default: "." + * thousands_separator?: scalar|Param|null, // Default: "," + * }, + * mailer?: array{ + * html_to_text_converter?: scalar|Param|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null + * }, + * } + * @psalm-type WebProfilerConfig = array{ + * toolbar?: bool|array{ // Profiler toolbar configuration + * enabled?: bool|Param, // Default: false + * ajax_replace?: bool|Param, // Replace toolbar on AJAX requests // Default: false + * }, + * intercept_redirects?: bool|Param, // Default: false + * excluded_ajax_paths?: scalar|Param|null, // Default: "^/((index|app(_[\\w]+)?)\\.php/)?_wdt" + * } + * @psalm-type MonologConfig = array{ + * use_microseconds?: scalar|Param|null, // Default: true + * channels?: list, + * handlers?: array, + * }>, + * accepted_levels?: list, + * min_level?: scalar|Param|null, // Default: "DEBUG" + * max_level?: scalar|Param|null, // Default: "EMERGENCY" + * buffer_size?: scalar|Param|null, // Default: 0 + * flush_on_overflow?: bool|Param, // Default: false + * handler?: scalar|Param|null, + * url?: scalar|Param|null, + * exchange?: scalar|Param|null, + * exchange_name?: scalar|Param|null, // Default: "log" + * channel?: scalar|Param|null, // Default: null + * bot_name?: scalar|Param|null, // Default: "Monolog" + * use_attachment?: scalar|Param|null, // Default: true + * use_short_attachment?: scalar|Param|null, // Default: false + * include_extra?: scalar|Param|null, // Default: false + * icon_emoji?: scalar|Param|null, // Default: null + * webhook_url?: scalar|Param|null, + * exclude_fields?: list, + * token?: scalar|Param|null, + * region?: scalar|Param|null, + * source?: scalar|Param|null, + * use_ssl?: bool|Param, // Default: true + * user?: mixed, + * title?: scalar|Param|null, // Default: null + * host?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 514 + * config?: list, + * members?: list, + * connection_string?: scalar|Param|null, + * timeout?: scalar|Param|null, + * time?: scalar|Param|null, // Default: 60 + * deduplication_level?: scalar|Param|null, // Default: 400 + * store?: scalar|Param|null, // Default: null + * connection_timeout?: scalar|Param|null, + * persistent?: bool|Param, + * message_type?: scalar|Param|null, // Default: 0 + * parse_mode?: scalar|Param|null, // Default: null + * disable_webpage_preview?: bool|Param|null, // Default: null + * disable_notification?: bool|Param|null, // Default: null + * split_long_messages?: bool|Param, // Default: false + * delay_between_messages?: bool|Param, // Default: false + * topic?: int|Param, // Default: null + * factor?: int|Param, // Default: 1 + * tags?: string|list, + * console_formatter_options?: mixed, // Default: [] + * formatter?: scalar|Param|null, + * nested?: bool|Param, // Default: false + * publisher?: string|array{ + * id?: scalar|Param|null, + * hostname?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 12201 + * chunk_size?: scalar|Param|null, // Default: 1420 + * encoder?: "json"|"compressed_json"|Param, + * }, + * mongodb?: string|array{ + * id?: scalar|Param|null, // ID of a MongoDB\Client service + * uri?: scalar|Param|null, + * username?: scalar|Param|null, + * password?: scalar|Param|null, + * database?: scalar|Param|null, // Default: "monolog" + * collection?: scalar|Param|null, // Default: "logs" + * }, + * elasticsearch?: string|array{ + * id?: scalar|Param|null, + * hosts?: list, + * host?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 9200 + * transport?: scalar|Param|null, // Default: "Http" + * user?: scalar|Param|null, // Default: null + * password?: scalar|Param|null, // Default: null + * }, + * index?: scalar|Param|null, // Default: "monolog" + * document_type?: scalar|Param|null, // Default: "logs" + * ignore_error?: scalar|Param|null, // Default: false + * redis?: string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * password?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 6379 + * database?: scalar|Param|null, // Default: 0 + * key_name?: scalar|Param|null, // Default: "monolog_redis" + * }, + * predis?: string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * }, + * from_email?: scalar|Param|null, + * to_email?: string|list, + * subject?: scalar|Param|null, + * content_type?: scalar|Param|null, // Default: null + * headers?: list, + * mailer?: scalar|Param|null, // Default: null + * email_prototype?: string|array{ + * id?: scalar|Param|null, + * method?: scalar|Param|null, // Default: null + * }, + * verbosity_levels?: array{ + * VERBOSITY_QUIET?: scalar|Param|null, // Default: "ERROR" + * VERBOSITY_NORMAL?: scalar|Param|null, // Default: "WARNING" + * VERBOSITY_VERBOSE?: scalar|Param|null, // Default: "NOTICE" + * VERBOSITY_VERY_VERBOSE?: scalar|Param|null, // Default: "INFO" + * VERBOSITY_DEBUG?: scalar|Param|null, // Default: "DEBUG" + * }, + * channels?: string|array{ + * type?: scalar|Param|null, + * elements?: list, + * }, + * }>, + * } + * @psalm-type DebugConfig = array{ + * max_items?: int|Param, // Max number of displayed items past the first level, -1 means no limit. // Default: 2500 + * min_depth?: int|Param, // Minimum tree depth to clone all the items, 1 is default. // Default: 1 + * max_string_length?: int|Param, // Max length of displayed strings, -1 means no limit. // Default: -1 + * dump_destination?: scalar|Param|null, // A stream URL where dumps should be written to. // Default: null + * theme?: "dark"|"light"|Param, // Changes the color of the dump() output when rendered directly on the templating. "dark" (default) or "light". // Default: "dark" + * } + * @psalm-type MakerConfig = array{ + * root_namespace?: scalar|Param|null, // Default: "App" + * generate_final_classes?: bool|Param, // Default: true + * generate_final_entities?: bool|Param, // Default: false + * } + * @psalm-type WebpackEncoreConfig = array{ + * output_path?: scalar|Param|null, // The path where Encore is building the assets - i.e. Encore.setOutputPath() + * crossorigin?: false|"anonymous"|"use-credentials"|Param, // crossorigin value when Encore.enableIntegrityHashes() is used, can be false (default), anonymous or use-credentials // Default: false + * preload?: bool|Param, // preload all rendered script and link tags automatically via the http2 Link header. // Default: false + * cache?: bool|Param, // Enable caching of the entry point file(s) // Default: false + * strict_mode?: bool|Param, // Throw an exception if the entrypoints.json file is missing or an entry is missing from the data // Default: true + * builds?: array, + * script_attributes?: array, + * link_attributes?: array, + * } + * @psalm-type DatatablesConfig = array{ + * language_from_cdn?: bool|Param, // Load i18n data from DataTables CDN or locally // Default: true + * persist_state?: "none"|"query"|"fragment"|"local"|"session"|Param, // Where to persist the current table state automatically // Default: "fragment" + * method?: "GET"|"POST"|Param, // Default HTTP method to be used for callbacks // Default: "POST" + * options?: array, + * renderer?: scalar|Param|null, // Default service used to render templates, built-in TwigRenderer uses global Twig environment // Default: "Omines\\DataTablesBundle\\Twig\\TwigRenderer" + * template?: scalar|Param|null, // Default template to be used for DataTables HTML // Default: "@DataTables/datatable_html.html.twig" + * template_parameters?: array{ // Default parameters to be passed to the template + * className?: scalar|Param|null, // Default class attribute to apply to the root table elements // Default: "table table-bordered" + * columnFilter?: "thead"|"tfoot"|"both"|Param|null, // If and where to enable the DataTables Filter module // Default: null + * ... + * }, + * translation_domain?: scalar|Param|null, // Default translation domain to be used // Default: "messages" + * } + * @psalm-type LiipImagineConfig = array{ + * resolvers?: array, + * get_options?: array, + * put_options?: array, + * proxies?: array, + * }, + * flysystem?: array{ + * filesystem_service?: scalar|Param|null, + * cache_prefix?: scalar|Param|null, // Default: "" + * root_url?: scalar|Param|null, + * visibility?: "public"|"private"|"noPredefinedVisibility"|Param, // Default: "public" + * }, + * }>, + * loaders?: array, + * allow_unresolvable_data_roots?: bool|Param, // Default: false + * bundle_resources?: array{ + * enabled?: bool|Param, // Default: false + * access_control_type?: "blacklist"|"whitelist"|Param, // Sets the access control method applied to bundle names in "access_control_list" into a blacklist or whitelist. // Default: "blacklist" + * access_control_list?: list, + * }, + * }, + * flysystem?: array{ + * filesystem_service?: scalar|Param|null, + * }, + * asset_mapper?: array, + * chain?: array{ + * loaders?: list, + * }, + * }>, + * driver?: scalar|Param|null, // Default: "gd" + * cache?: scalar|Param|null, // Default: "default" + * cache_base_path?: scalar|Param|null, // Default: "" + * data_loader?: scalar|Param|null, // Default: "default" + * default_image?: scalar|Param|null, // Default: null + * default_filter_set_settings?: array{ + * quality?: scalar|Param|null, // Default: 100 + * jpeg_quality?: scalar|Param|null, // Default: null + * png_compression_level?: scalar|Param|null, // Default: null + * png_compression_filter?: scalar|Param|null, // Default: null + * format?: scalar|Param|null, // Default: null + * animated?: bool|Param, // Default: false + * cache?: scalar|Param|null, // Default: null + * data_loader?: scalar|Param|null, // Default: null + * default_image?: scalar|Param|null, // Default: null + * filters?: array>, + * post_processors?: array>, + * }, + * controller?: array{ + * filter_action?: scalar|Param|null, // Default: "Liip\\ImagineBundle\\Controller\\ImagineController::filterAction" + * filter_runtime_action?: scalar|Param|null, // Default: "Liip\\ImagineBundle\\Controller\\ImagineController::filterRuntimeAction" + * redirect_response_code?: int|Param, // Default: 302 + * }, + * filter_sets?: array>, + * post_processors?: array>, + * }>, + * twig?: array{ + * mode?: "none"|"lazy"|"legacy"|Param, // Twig mode: none/lazy/legacy (default) // Default: "legacy" + * assets_version?: scalar|Param|null, // Default: null + * }, + * enqueue?: bool|Param, // Enables integration with enqueue if set true. Allows resolve image caches in background by sending messages to MQ. // Default: false + * messenger?: bool|array{ // Enables integration with symfony/messenger if set true. Warmup image caches in background by sending messages to MQ. + * enabled?: bool|Param, // Default: false + * }, + * templating?: bool|Param, // Enables integration with symfony/templating component // Default: true + * webp?: array{ + * generate?: bool|Param, // Default: false + * quality?: int|Param, // Default: 100 + * cache?: scalar|Param|null, // Default: null + * data_loader?: scalar|Param|null, // Default: null + * post_processors?: array>, + * }, + * } + * @psalm-type DamaDoctrineTestConfig = array{ + * enable_static_connection?: mixed, // Default: true + * enable_static_meta_data_cache?: bool|Param, // Default: true + * enable_static_query_cache?: bool|Param, // Default: true + * connection_keys?: list, + * } + * @psalm-type TwigExtraConfig = array{ + * cache?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * html?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * markdown?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * intl?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * cssinliner?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * inky?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * string?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * commonmark?: array{ + * renderer?: array{ // Array of options for rendering HTML. + * block_separator?: scalar|Param|null, + * inner_separator?: scalar|Param|null, + * soft_break?: scalar|Param|null, + * }, + * html_input?: "strip"|"allow"|"escape"|Param, // How to handle HTML input. + * allow_unsafe_links?: bool|Param, // Remove risky link and image URLs by setting this to false. // Default: true + * max_nesting_level?: int|Param, // The maximum nesting level for blocks. // Default: 9223372036854775807 + * max_delimiters_per_line?: int|Param, // The maximum number of strong/emphasis delimiters per line. // Default: 9223372036854775807 + * slug_normalizer?: array{ // Array of options for configuring how URL-safe slugs are created. + * instance?: mixed, + * max_length?: int|Param, // Default: 255 + * unique?: mixed, + * }, + * commonmark?: array{ // Array of options for configuring the CommonMark core extension. + * enable_em?: bool|Param, // Default: true + * enable_strong?: bool|Param, // Default: true + * use_asterisk?: bool|Param, // Default: true + * use_underscore?: bool|Param, // Default: true + * unordered_list_markers?: list, + * }, + * ... + * }, + * } + * @psalm-type GregwarCaptchaConfig = array{ + * length?: scalar|Param|null, // Default: 5 + * width?: scalar|Param|null, // Default: 130 + * height?: scalar|Param|null, // Default: 50 + * font?: scalar|Param|null, // Default: "/home/jan/php/Part-DB-server/vendor/gregwar/captcha-bundle/DependencyInjection/../Generator/Font/captcha.ttf" + * keep_value?: scalar|Param|null, // Default: false + * charset?: scalar|Param|null, // Default: "abcdefhjkmnprstuvwxyz23456789" + * as_file?: scalar|Param|null, // Default: false + * as_url?: scalar|Param|null, // Default: false + * reload?: scalar|Param|null, // Default: false + * image_folder?: scalar|Param|null, // Default: "captcha" + * web_path?: scalar|Param|null, // Default: "%kernel.project_dir%/public" + * gc_freq?: scalar|Param|null, // Default: 100 + * expiration?: scalar|Param|null, // Default: 60 + * quality?: scalar|Param|null, // Default: 50 + * invalid_message?: scalar|Param|null, // Default: "Bad code value" + * bypass_code?: scalar|Param|null, // Default: null + * whitelist_key?: scalar|Param|null, // Default: "captcha_whitelist_key" + * humanity?: scalar|Param|null, // Default: 0 + * distortion?: scalar|Param|null, // Default: true + * max_front_lines?: scalar|Param|null, // Default: null + * max_behind_lines?: scalar|Param|null, // Default: null + * interpolation?: scalar|Param|null, // Default: true + * text_color?: list, + * background_color?: list, + * background_images?: list, + * disabled?: scalar|Param|null, // Default: false + * ignore_all_effects?: scalar|Param|null, // Default: false + * session_key?: scalar|Param|null, // Default: "captcha" + * } + * @psalm-type FlorianvSwapConfig = array{ + * cache?: array{ + * ttl?: int|Param, // Default: 3600 + * type?: scalar|Param|null, // A cache type or service id // Default: null + * }, + * providers?: array{ + * apilayer_fixer?: array{ + * priority?: int|Param, // Default: 0 + * api_key?: scalar|Param|null, + * }, + * apilayer_currency_data?: array{ + * priority?: int|Param, // Default: 0 + * api_key?: scalar|Param|null, + * }, + * apilayer_exchange_rates_data?: array{ + * priority?: int|Param, // Default: 0 + * api_key?: scalar|Param|null, + * }, + * abstract_api?: array{ + * priority?: int|Param, // Default: 0 + * api_key?: scalar|Param|null, + * }, + * fixer?: array{ + * priority?: int|Param, // Default: 0 + * access_key?: scalar|Param|null, + * enterprise?: bool|Param, // Default: false + * }, + * cryptonator?: array{ + * priority?: int|Param, // Default: 0 + * }, + * exchange_rates_api?: array{ + * priority?: int|Param, // Default: 0 + * access_key?: scalar|Param|null, + * enterprise?: bool|Param, // Default: false + * }, + * webservicex?: array{ + * priority?: int|Param, // Default: 0 + * }, + * central_bank_of_czech_republic?: array{ + * priority?: int|Param, // Default: 0 + * }, + * central_bank_of_republic_turkey?: array{ + * priority?: int|Param, // Default: 0 + * }, + * european_central_bank?: array{ + * priority?: int|Param, // Default: 0 + * }, + * national_bank_of_romania?: array{ + * priority?: int|Param, // Default: 0 + * }, + * russian_central_bank?: array{ + * priority?: int|Param, // Default: 0 + * }, + * frankfurter?: array{ + * priority?: int|Param, // Default: 0 + * }, + * fawazahmed_currency_api?: array{ + * priority?: int|Param, // Default: 0 + * }, + * bulgarian_national_bank?: array{ + * priority?: int|Param, // Default: 0 + * }, + * national_bank_of_ukraine?: array{ + * priority?: int|Param, // Default: 0 + * }, + * currency_data_feed?: array{ + * priority?: int|Param, // Default: 0 + * api_key?: scalar|Param|null, + * }, + * currency_layer?: array{ + * priority?: int|Param, // Default: 0 + * access_key?: scalar|Param|null, + * enterprise?: bool|Param, // Default: false + * }, + * forge?: array{ + * priority?: int|Param, // Default: 0 + * api_key?: scalar|Param|null, + * }, + * open_exchange_rates?: array{ + * priority?: int|Param, // Default: 0 + * app_id?: scalar|Param|null, + * enterprise?: bool|Param, // Default: false + * }, + * xignite?: array{ + * priority?: int|Param, // Default: 0 + * token?: scalar|Param|null, + * }, + * xchangeapi?: array{ + * priority?: int|Param, // Default: 0 + * api_key?: scalar|Param|null, + * }, + * currency_converter?: array{ + * priority?: int|Param, // Default: 0 + * access_key?: scalar|Param|null, + * enterprise?: bool|Param, // Default: false + * }, + * array?: array{ + * priority?: int|Param, // Default: 0 + * latestRates?: mixed, + * historicalRates?: mixed, + * }, + * }, + * } + * @psalm-type NelmioSecurityConfig = array{ + * signed_cookie?: array{ + * names?: list, + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * hash_algo?: scalar|Param|null, + * legacy_hash_algo?: scalar|Param|null, // Fallback algorithm to allow for frictionless hash algorithm upgrades. Use with caution and as a temporary measure as it allows for downgrade attacks. // Default: null + * separator?: scalar|Param|null, // Default: "." + * }, + * clickjacking?: array{ + * hosts?: list, + * paths?: array, + * content_types?: list, + * }, + * external_redirects?: array{ + * abort?: bool|Param, // Default: false + * override?: scalar|Param|null, // Default: null + * forward_as?: scalar|Param|null, // Default: null + * log?: bool|Param, // Default: false + * allow_list?: list, + * }, + * flexible_ssl?: bool|array{ + * enabled?: bool|Param, // Default: false + * cookie_name?: scalar|Param|null, // Default: "auth" + * unsecured_logout?: bool|Param, // Default: false + * }, + * forced_ssl?: bool|array{ + * enabled?: bool|Param, // Default: false + * hsts_max_age?: scalar|Param|null, // Default: null + * hsts_subdomains?: bool|Param, // Default: false + * hsts_preload?: bool|Param, // Default: false + * allow_list?: list, + * hosts?: list, + * redirect_status_code?: scalar|Param|null, // Default: 302 + * }, + * content_type?: array{ + * nosniff?: bool|Param, // Default: false + * }, + * xss_protection?: array{ // Deprecated: The "xss_protection" option is deprecated, use Content Security Policy without allowing "unsafe-inline" scripts instead. + * enabled?: bool|Param, // Default: false + * mode_block?: bool|Param, // Default: false + * report_uri?: scalar|Param|null, // Default: null + * }, + * csp?: bool|array{ + * enabled?: bool|Param, // Default: true + * request_matcher?: scalar|Param|null, // Default: null + * hosts?: list, + * content_types?: list, + * report_endpoint?: array{ + * log_channel?: scalar|Param|null, // Default: null + * log_formatter?: scalar|Param|null, // Default: "nelmio_security.csp_report.log_formatter" + * log_level?: "alert"|"critical"|"debug"|"emergency"|"error"|"info"|"notice"|"warning"|Param, // Default: "notice" + * filters?: array{ + * domains?: bool|Param, // Default: true + * schemes?: bool|Param, // Default: true + * browser_bugs?: bool|Param, // Default: true + * injected_scripts?: bool|Param, // Default: true + * }, + * dismiss?: list>, + * }, + * compat_headers?: bool|Param, // Default: true + * report_logger_service?: scalar|Param|null, // Default: "logger" + * hash?: array{ + * algorithm?: "sha256"|"sha384"|"sha512"|Param, // The algorithm to use for hashes // Default: "sha256" + * }, + * report?: array{ + * level1_fallback?: bool|Param, // Provides CSP Level 1 fallback when using hash or nonce (CSP level 2) by adding 'unsafe-inline' source. See https://www.w3.org/TR/CSP2/#directive-script-src and https://www.w3.org/TR/CSP2/#directive-style-src // Default: true + * browser_adaptive?: bool|array{ // Do not send directives that browser do not support + * enabled?: bool|Param, // Default: false + * parser?: scalar|Param|null, // Default: "nelmio_security.ua_parser.ua_php" + * }, + * default-src?: list, + * base-uri?: list, + * block-all-mixed-content?: bool|Param, // Default: false + * child-src?: list, + * connect-src?: list, + * font-src?: list, + * form-action?: list, + * frame-ancestors?: list, + * frame-src?: list, + * img-src?: list, + * manifest-src?: list, + * media-src?: list, + * object-src?: list, + * plugin-types?: list, + * script-src?: list, + * style-src?: list, + * upgrade-insecure-requests?: bool|Param, // Default: false + * report-uri?: string|list, + * worker-src?: list, + * prefetch-src?: list, + * report-to?: scalar|Param|null, + * }, + * enforce?: array{ + * level1_fallback?: bool|Param, // Provides CSP Level 1 fallback when using hash or nonce (CSP level 2) by adding 'unsafe-inline' source. See https://www.w3.org/TR/CSP2/#directive-script-src and https://www.w3.org/TR/CSP2/#directive-style-src // Default: true + * browser_adaptive?: bool|array{ // Do not send directives that browser do not support + * enabled?: bool|Param, // Default: false + * parser?: scalar|Param|null, // Default: "nelmio_security.ua_parser.ua_php" + * }, + * default-src?: list, + * base-uri?: list, + * block-all-mixed-content?: bool|Param, // Default: false + * child-src?: list, + * connect-src?: list, + * font-src?: list, + * form-action?: list, + * frame-ancestors?: list, + * frame-src?: list, + * img-src?: list, + * manifest-src?: list, + * media-src?: list, + * object-src?: list, + * plugin-types?: list, + * script-src?: list, + * style-src?: list, + * upgrade-insecure-requests?: bool|Param, // Default: false + * report-uri?: string|list, + * worker-src?: list, + * prefetch-src?: list, + * report-to?: scalar|Param|null, + * }, + * }, + * referrer_policy?: bool|array{ + * enabled?: bool|Param, // Default: false + * policies?: string|list, + * }, + * permissions_policy?: bool|array{ + * enabled?: bool|Param, // Default: false + * policies?: array{ + * accelerometer?: mixed, // Default: null + * ambient_light_sensor?: mixed, // Default: null + * attribution_reporting?: mixed, // Default: null + * autoplay?: mixed, // Default: null + * bluetooth?: mixed, // Default: null + * browsing_topics?: mixed, // Default: null + * camera?: mixed, // Default: null + * captured_surface_control?: mixed, // Default: null + * compute_pressure?: mixed, // Default: null + * cross_origin_isolated?: mixed, // Default: null + * deferred_fetch?: mixed, // Default: null + * deferred_fetch_minimal?: mixed, // Default: null + * display_capture?: mixed, // Default: null + * encrypted_media?: mixed, // Default: null + * fullscreen?: mixed, // Default: null + * gamepad?: mixed, // Default: null + * geolocation?: mixed, // Default: null + * gyroscope?: mixed, // Default: null + * hid?: mixed, // Default: null + * identity_credentials_get?: mixed, // Default: null + * idle_detection?: mixed, // Default: null + * interest_cohort?: mixed, // Default: null + * language_detector?: mixed, // Default: null + * local_fonts?: mixed, // Default: null + * magnetometer?: mixed, // Default: null + * microphone?: mixed, // Default: null + * midi?: mixed, // Default: null + * otp_credentials?: mixed, // Default: null + * payment?: mixed, // Default: null + * picture_in_picture?: mixed, // Default: null + * publickey_credentials_create?: mixed, // Default: null + * publickey_credentials_get?: mixed, // Default: null + * screen_wake_lock?: mixed, // Default: null + * serial?: mixed, // Default: null + * speaker_selection?: mixed, // Default: null + * storage_access?: mixed, // Default: null + * summarizer?: mixed, // Default: null + * translator?: mixed, // Default: null + * usb?: mixed, // Default: null + * web_share?: mixed, // Default: null + * window_management?: mixed, // Default: null + * xr_spatial_tracking?: mixed, // Default: null + * }, + * }, + * cross_origin_isolation?: bool|array{ + * enabled?: bool|Param, // Default: false + * paths?: array, + * }, + * } + * @psalm-type TurboConfig = array{ + * broadcast?: bool|array{ + * enabled?: bool|Param, // Default: true + * entity_template_prefixes?: list, + * doctrine_orm?: bool|array{ // Enable the Doctrine ORM integration + * enabled?: bool|Param, // Default: true + * }, + * }, + * default_transport?: scalar|Param|null, // Default: "default" + * } + * @psalm-type TfaWebauthnConfig = array{ + * enabled?: scalar|Param|null, // Default: false + * timeout?: int|Param, // Default: 60000 + * rpID?: scalar|Param|null, // Default: null + * rpName?: scalar|Param|null, // Default: "Webauthn Application" + * rpIcon?: scalar|Param|null, // Default: null + * template?: scalar|Param|null, // Default: "@TFAWebauthn/Authentication/form.html.twig" + * U2FAppID?: scalar|Param|null, // Default: null + * } + * @psalm-type SchebTwoFactorConfig = array{ + * persister?: scalar|Param|null, // Default: "scheb_two_factor.persister.doctrine" + * model_manager_name?: scalar|Param|null, // Default: null + * security_tokens?: list, + * ip_whitelist?: list, + * ip_whitelist_provider?: scalar|Param|null, // Default: "scheb_two_factor.default_ip_whitelist_provider" + * two_factor_token_factory?: scalar|Param|null, // Default: "scheb_two_factor.default_token_factory" + * two_factor_provider_decider?: scalar|Param|null, // Default: "scheb_two_factor.default_provider_decider" + * two_factor_condition?: scalar|Param|null, // Default: null + * code_reuse_cache?: scalar|Param|null, // Default: null + * code_reuse_cache_duration?: int|Param, // Default: 60 + * code_reuse_default_handler?: scalar|Param|null, // Default: null + * trusted_device?: bool|array{ + * enabled?: scalar|Param|null, // Default: false + * manager?: scalar|Param|null, // Default: "scheb_two_factor.default_trusted_device_manager" + * lifetime?: int|Param, // Default: 5184000 + * extend_lifetime?: bool|Param, // Default: false + * key?: scalar|Param|null, // Default: null + * cookie_name?: scalar|Param|null, // Default: "trusted_device" + * cookie_secure?: true|false|"auto"|Param, // Default: "auto" + * cookie_domain?: scalar|Param|null, // Default: null + * cookie_path?: scalar|Param|null, // Default: "/" + * cookie_same_site?: scalar|Param|null, // Default: "lax" + * }, + * backup_codes?: bool|array{ + * enabled?: scalar|Param|null, // Default: false + * manager?: scalar|Param|null, // Default: "scheb_two_factor.default_backup_code_manager" + * }, + * google?: bool|array{ + * enabled?: scalar|Param|null, // Default: false + * form_renderer?: scalar|Param|null, // Default: null + * issuer?: scalar|Param|null, // Default: null + * server_name?: scalar|Param|null, // Default: null + * template?: scalar|Param|null, // Default: "@SchebTwoFactor/Authentication/form.html.twig" + * digits?: int|Param, // Default: 6 + * leeway?: int|Param, // Default: 0 + * }, + * } + * @psalm-type WebauthnConfig = array{ + * fake_credential_generator?: scalar|Param|null, // A service that implements the FakeCredentialGenerator to generate fake credentials for preventing username enumeration. // Default: "Webauthn\\SimpleFakeCredentialGenerator" + * clock?: scalar|Param|null, // PSR-20 Clock service. // Default: "webauthn.clock.default" + * options_storage?: scalar|Param|null, // Service responsible of the options/user entity storage during the ceremony // Default: "Webauthn\\Bundle\\Security\\Storage\\SessionStorage" + * event_dispatcher?: scalar|Param|null, // PSR-14 Event Dispatcher service. // Default: "Psr\\EventDispatcher\\EventDispatcherInterface" + * http_client?: scalar|Param|null, // A Symfony HTTP client. // Default: "webauthn.http_client.default" + * logger?: scalar|Param|null, // A PSR-3 logger to receive logs during the processes // Default: "webauthn.logger.default" + * credential_repository?: scalar|Param|null, // This repository is responsible of the credential storage // Default: "Webauthn\\Bundle\\Repository\\DummyPublicKeyCredentialSourceRepository" + * user_repository?: scalar|Param|null, // This repository is responsible of the user storage // Default: "Webauthn\\Bundle\\Repository\\DummyPublicKeyCredentialUserEntityRepository" + * allowed_origins?: array, + * allow_subdomains?: bool|Param, // Default: false + * secured_rp_ids?: array, + * counter_checker?: scalar|Param|null, // This service will check if the counter is valid. By default it throws an exception (recommended). // Default: "Webauthn\\Counter\\ThrowExceptionIfInvalid" + * top_origin_validator?: scalar|Param|null, // For cross origin (e.g. iframe), this service will be in charge of verifying the top origin. // Default: null + * creation_profiles?: bool|array, + * public_key_credential_parameters?: list, + * attestation_conveyance?: scalar|Param|null, // Default: "none" + * conditional_create?: bool|Param, // Enable Conditional Create (auto-register) for this profile. When true, user presence can be false after password authentication. See https://github.com/w3c/webauthn/wiki/Explainer:-Conditional-Create // Default: false + * }>, + * request_profiles?: bool|array, + * }>, + * client_override_policy?: array{ // Configuration for allowing client request values to override profile configuration + * user_verification?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override the user verification requirement // Default: false + * allowed_values?: list, + * }, + * authenticator_attachment?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override the authenticator attachment // Default: true + * allowed_values?: list, + * }, + * resident_key?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override the resident key requirement // Default: true + * allowed_values?: list, + * }, + * attestation_conveyance?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override the attestation conveyance preference // Default: true + * allowed_values?: list, + * }, + * extensions?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override extensions // Default: true + * }, + * mediation?: array{ + * enabled?: bool|Param, // Whether to allow client requests to request the conditional mediation flow (auto-register). // Default: false + * allowed_values?: list, + * }, + * }, + * metadata?: bool|array{ // Enable the support of the Metadata Statements. Please read the documentation for this feature. + * enabled?: bool|Param, // Default: false + * mds_repository?: scalar|Param|null, // The Metadata Statement repository. + * status_report_repository?: scalar|Param|null, // The Status Report repository. + * certificate_chain_checker?: scalar|Param|null, // A Certificate Chain checker. // Default: "Webauthn\\MetadataService\\CertificateChain\\PhpCertificateChainValidator" + * }, + * controllers?: bool|array{ + * enabled?: bool|Param, // Default: false + * creation?: array, + * allow_subdomains?: bool|Param, // Default: false + * secured_rp_ids?: array, + * }>, + * request?: array, + * allow_subdomains?: bool|Param, // Default: false + * secured_rp_ids?: array, + * }>, + * }, + * passkey_endpoints?: bool|array{ // Enable the .well-known/passkey-endpoints discovery endpoint as defined in the W3C Passkey Endpoints specification. + * enabled?: bool|Param, // Default: false + * enroll?: string|array{ // URL to the passkey enrollment/creation interface. + * path?: scalar|Param|null, // The absolute HTTPS URL or Symfony route name. + * params?: list, + * }, + * manage?: string|array{ // URL to the passkey management interface. + * path?: scalar|Param|null, // The absolute HTTPS URL or Symfony route name. + * params?: list, + * }, + * prf_usage_details?: string|array{ // URL to informational page about PRF (Pseudo-Random Function) extension usage. + * path?: scalar|Param|null, // The absolute HTTPS URL or Symfony route name. + * params?: list, + * }, + * }, + * } + * @psalm-type NbgrpOneloginSamlConfig = array{ // nb:group OneLogin PHP Symfony Bundle configuration + * onelogin_settings?: array/saml/" + * strict?: bool|Param, + * debug?: bool|Param, + * idp?: array{ + * entityId?: scalar|Param|null, + * singleSignOnService?: array{ + * url?: scalar|Param|null, + * binding?: scalar|Param|null, + * }, + * singleLogoutService?: array{ + * url?: scalar|Param|null, + * responseUrl?: scalar|Param|null, + * binding?: scalar|Param|null, + * }, + * x509cert?: scalar|Param|null, + * certFingerprint?: scalar|Param|null, + * certFingerprintAlgorithm?: "sha1"|"sha256"|"sha384"|"sha512"|Param, + * x509certMulti?: array{ + * signing?: list, + * encryption?: list, + * }, + * }, + * sp?: array{ + * entityId?: scalar|Param|null, // Default: "/saml/metadata" + * assertionConsumerService?: array{ + * url?: scalar|Param|null, // Default: "/saml/acs" + * binding?: scalar|Param|null, + * }, + * attributeConsumingService?: array{ + * serviceName?: scalar|Param|null, + * serviceDescription?: scalar|Param|null, + * requestedAttributes?: list, + * }>, + * }, + * singleLogoutService?: array{ + * url?: scalar|Param|null, // Default: "/saml/logout" + * binding?: scalar|Param|null, + * }, + * NameIDFormat?: scalar|Param|null, + * x509cert?: scalar|Param|null, + * privateKey?: scalar|Param|null, + * x509certNew?: scalar|Param|null, + * }, + * compress?: array{ + * requests?: bool|Param, + * responses?: bool|Param, + * }, + * security?: array{ + * nameIdEncrypted?: bool|Param, + * authnRequestsSigned?: bool|Param, + * logoutRequestSigned?: bool|Param, + * logoutResponseSigned?: bool|Param, + * signMetadata?: bool|Param, + * wantMessagesSigned?: bool|Param, + * wantAssertionsEncrypted?: bool|Param, + * wantAssertionsSigned?: bool|Param, + * wantNameId?: bool|Param, + * wantNameIdEncrypted?: bool|Param, + * requestedAuthnContext?: mixed, + * requestedAuthnContextComparison?: "exact"|"minimum"|"maximum"|"better"|Param, + * wantXMLValidation?: bool|Param, + * relaxDestinationValidation?: bool|Param, + * destinationStrictlyMatches?: bool|Param, + * allowRepeatAttributeName?: bool|Param, + * rejectUnsolicitedResponsesWithInResponseTo?: bool|Param, + * signatureAlgorithm?: "http://www.w3.org/2000/09/xmldsig#rsa-sha1"|"http://www.w3.org/2000/09/xmldsig#dsa-sha1"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"|Param, + * digestAlgorithm?: "http://www.w3.org/2000/09/xmldsig#sha1"|"http://www.w3.org/2001/04/xmlenc#sha256"|"http://www.w3.org/2001/04/xmldsig-more#sha384"|"http://www.w3.org/2001/04/xmlenc#sha512"|Param, + * encryption_algorithm?: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc"|"http://www.w3.org/2001/04/xmlenc#aes128-cbc"|"http://www.w3.org/2001/04/xmlenc#aes192-cbc"|"http://www.w3.org/2001/04/xmlenc#aes256-cbc"|"http://www.w3.org/2009/xmlenc11#aes128-gcm"|"http://www.w3.org/2009/xmlenc11#aes192-gcm"|"http://www.w3.org/2009/xmlenc11#aes256-gcm"|Param, + * lowercaseUrlencoding?: bool|Param, + * }, + * contactPerson?: array{ + * technical?: array{ + * givenName?: scalar|Param|null, + * emailAddress?: scalar|Param|null, + * }, + * support?: array{ + * givenName?: scalar|Param|null, + * emailAddress?: scalar|Param|null, + * }, + * administrative?: array{ + * givenName?: scalar|Param|null, + * emailAddress?: scalar|Param|null, + * }, + * billing?: array{ + * givenName?: scalar|Param|null, + * emailAddress?: scalar|Param|null, + * }, + * other?: array{ + * givenName?: scalar|Param|null, + * emailAddress?: scalar|Param|null, + * }, + * }, + * organization?: list, + * }>, + * use_proxy_vars?: bool|Param, // Default: false + * idp_parameter_name?: scalar|Param|null, // Default: "idp" + * entity_manager_name?: scalar|Param|null, + * authn_request?: array{ + * parameters?: list, + * forceAuthn?: bool|Param, // Default: false + * isPassive?: bool|Param, // Default: false + * setNameIdPolicy?: bool|Param, // Default: true + * nameIdValueReq?: scalar|Param|null, // Default: null + * }, + * } + * @psalm-type StimulusConfig = array{ + * controller_paths?: list, + * controllers_json?: scalar|Param|null, // Default: "%kernel.project_dir%/assets/controllers.json" + * } + * @psalm-type UxTranslatorConfig = array{ + * dump_directory?: scalar|Param|null, // The directory where translations and TypeScript types are dumped. // Default: "%kernel.project_dir%/var/translations" + * dump_typescript?: bool|Param, // Control whether TypeScript types are dumped alongside translations. Disable this if you do not use TypeScript (e.g. in production when using AssetMapper). // Default: true + * domains?: string|array{ // List of domains to include/exclude from the generated translations. Prefix with a `!` to exclude a domain. + * type?: scalar|Param|null, + * elements?: list, + * }, + * keys_patterns?: string|list, + * } + * @psalm-type DompdfFontLoaderConfig = array{ + * autodiscovery?: bool|array{ + * paths?: list, + * exclude_patterns?: list, + * file_pattern?: scalar|Param|null, // Default: "/\\.(ttf)$/" + * enabled?: bool|Param, // Default: true + * }, + * auto_install?: bool|Param, // Default: false + * fonts?: list, + * } + * @psalm-type KnpuOauth2ClientConfig = array{ + * http_client?: scalar|Param|null, // Service id of HTTP client to use (must implement GuzzleHttp\ClientInterface) // Default: null + * http_client_options?: array{ + * timeout?: int|Param, + * proxy?: scalar|Param|null, + * verify?: bool|Param, // Use only with proxy option set + * }, + * clients?: array>, + * } + * @psalm-type NelmioCorsConfig = array{ + * defaults?: array{ + * allow_credentials?: bool|Param, // Default: false + * allow_origin?: list, + * allow_headers?: list, + * allow_methods?: list, + * allow_private_network?: bool|Param, // Default: false + * expose_headers?: list, + * max_age?: scalar|Param|null, // Default: 0 + * hosts?: list, + * origin_regex?: bool|Param, // Default: false + * forced_allow_origin_value?: scalar|Param|null, // Default: null + * skip_same_as_origin?: bool|Param, // Default: true + * }, + * paths?: array, + * allow_headers?: list, + * allow_methods?: list, + * allow_private_network?: bool|Param, + * expose_headers?: list, + * max_age?: scalar|Param|null, // Default: 0 + * hosts?: list, + * origin_regex?: bool|Param, + * forced_allow_origin_value?: scalar|Param|null, // Default: null + * skip_same_as_origin?: bool|Param, + * }>, + * } + * @psalm-type JbtronicsSettingsConfig = array{ + * search_paths?: list, + * proxy_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/jbtronics_settings/proxies" + * proxy_namespace?: scalar|Param|null, // Default: "Jbtronics\\SettingsBundle\\Proxies" + * default_storage_adapter?: scalar|Param|null, // Default: null + * save_after_migration?: bool|Param, // Default: true + * yaml_mapping_paths?: list, + * metadata_compiler_providers?: list, + * file_storage?: array{ + * storage_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/var/jbtronics_settings/" + * default_filename?: scalar|Param|null, // Default: "settings" + * }, + * orm_storage?: array{ + * default_entity_class?: scalar|Param|null, // Default: null + * prefetch_all?: bool|Param, // Default: true + * }, + * cache?: array{ + * metadata_service?: scalar|Param|null, // Default: "cache.system" + * service?: scalar|Param|null, // Default: "cache.app.taggable" + * default_cacheable?: bool|Param, // Default: false + * ttl?: int|Param, // Default: 0 + * invalidate_on_env_change?: bool|Param, // Default: true + * }, + * } + * @psalm-type JbtronicsTranslationEditorConfig = array{ + * translations_path?: scalar|Param|null, // Default: "%translator.default_path%" + * format?: scalar|Param|null, // Default: "xlf" + * xliff_version?: scalar|Param|null, // Default: "2.0" + * use_intl_icu_format?: bool|Param, // Default: false + * writer_options?: list, + * } + * @psalm-type ApiPlatformConfig = array{ + * title?: scalar|Param|null, // The title of the API. // Default: "" + * description?: scalar|Param|null, // The description of the API. // Default: "" + * version?: scalar|Param|null, // The version of the API. // Default: "0.0.0" + * show_webby?: bool|Param, // If true, show Webby on the documentation page // Default: true + * use_symfony_listeners?: bool|Param, // Uses Symfony event listeners instead of the ApiPlatform\Symfony\Controller\MainController. // Default: false + * name_converter?: scalar|Param|null, // Specify a name converter to use. // Default: null + * asset_package?: scalar|Param|null, // Specify an asset package name to use. // Default: null + * path_segment_name_generator?: scalar|Param|null, // Specify a path name generator to use. // Default: "api_platform.metadata.path_segment_name_generator.underscore" + * inflector?: scalar|Param|null, // Specify an inflector to use. // Default: "api_platform.metadata.inflector" + * validator?: array{ + * serialize_payload_fields?: mixed, // Set to null to serialize all payload fields when a validation error is thrown, or set the fields you want to include explicitly. // Default: [] + * query_parameter_validation?: bool|Param, // Deprecated: Will be removed in API Platform 5.0. // Default: true + * }, + * jsonapi?: array{ + * use_iri_as_id?: bool|Param, // Set to false to use entity identifiers instead of IRIs as the "id" field in JSON:API responses. // Default: true + * }, + * eager_loading?: bool|array{ + * enabled?: bool|Param, // Default: true + * fetch_partial?: bool|Param, // Fetch only partial data according to serialization groups. If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used. // Default: false + * max_joins?: int|Param, // Max number of joined relations before EagerLoading throws a RuntimeException // Default: 30 + * force_eager?: bool|Param, // Force join on every relation. If disabled, it will only join relations having the EAGER fetch mode. // Default: true + * }, + * handle_symfony_errors?: bool|Param, // Allows to handle symfony exceptions. // Default: false + * enable_swagger?: bool|Param, // Enable the Swagger documentation and export. // Default: true + * enable_json_streamer?: bool|Param, // Enable json streamer. // Default: false + * enable_swagger_ui?: bool|Param, // Enable Swagger UI // Default: true + * enable_re_doc?: bool|Param, // Enable ReDoc // Default: true + * enable_scalar?: bool|Param, // Enable Scalar API Reference // Default: true + * enable_entrypoint?: bool|Param, // Enable the entrypoint // Default: true + * enable_docs?: bool|Param, // Enable the docs // Default: true + * enable_profiler?: bool|Param, // Enable the data collector and the WebProfilerBundle integration. // Default: true + * enable_phpdoc_parser?: bool|Param, // Enable resource metadata collector using PHPStan PhpDocParser. // Default: true + * enable_link_security?: bool|Param, // Deprecated: This option is always enabled and will be removed in API Platform 5.0. // Enable security for Links (sub resources). // Default: true + * collection?: array{ + * exists_parameter_name?: scalar|Param|null, // The name of the query parameter to filter on nullable field values. // Default: "exists" + * order?: scalar|Param|null, // The default order of results. // Default: "ASC" + * order_parameter_name?: scalar|Param|null, // The name of the query parameter to order results. // Default: "order" + * order_nulls_comparison?: "nulls_smallest"|"nulls_largest"|"nulls_always_first"|"nulls_always_last"|Param|null, // The nulls comparison strategy. // Default: null + * pagination?: bool|array{ + * enabled?: bool|Param, // Default: true + * page_parameter_name?: scalar|Param|null, // The default name of the parameter handling the page number. // Default: "page" + * enabled_parameter_name?: scalar|Param|null, // The name of the query parameter to enable or disable pagination. // Default: "pagination" + * items_per_page_parameter_name?: scalar|Param|null, // The name of the query parameter to set the number of items per page. // Default: "itemsPerPage" + * partial_parameter_name?: scalar|Param|null, // The name of the query parameter to enable or disable partial pagination. // Default: "partial" + * }, + * }, + * mapping?: array{ + * imports?: list, + * paths?: list, + * }, + * resource_class_directories?: list, + * serializer?: array{ + * hydra_prefix?: bool|Param, // Use the "hydra:" prefix. // Default: false + * }, + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * doctrine_mongodb_odm?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * oauth?: bool|array{ + * enabled?: bool|Param, // Default: false + * clientId?: scalar|Param|null, // The oauth client id. // Default: "" + * clientSecret?: scalar|Param|null, // The OAuth client secret. Never use this parameter in your production environment. It exposes crucial security information. This feature is intended for dev/test environments only. Enable "oauth.pkce" instead // Default: "" + * pkce?: bool|Param, // Enable the oauth PKCE. // Default: false + * type?: scalar|Param|null, // The oauth type. // Default: "oauth2" + * flow?: scalar|Param|null, // The oauth flow grant type. // Default: "application" + * tokenUrl?: scalar|Param|null, // The oauth token url. // Default: "" + * authorizationUrl?: scalar|Param|null, // The oauth authentication url. // Default: "" + * refreshUrl?: scalar|Param|null, // The oauth refresh url. // Default: "" + * scopes?: list, + * }, + * graphql?: bool|array{ + * enabled?: bool|Param, // Default: false + * default_ide?: scalar|Param|null, // Default: "graphiql" + * graphiql?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * introspection?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * max_query_depth?: int|Param, // Default: 20 + * graphql_playground?: bool|array{ // Deprecated: The "graphql_playground" configuration is deprecated and will be ignored. + * enabled?: bool|Param, // Default: false + * }, + * max_query_complexity?: int|Param, // Default: 500 + * nesting_separator?: scalar|Param|null, // The separator to use to filter nested fields. // Default: "_" + * collection?: array{ + * pagination?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * }, + * }, + * swagger?: array{ + * persist_authorization?: bool|Param, // Persist the SwaggerUI Authorization in the localStorage. // Default: false + * versions?: list, + * api_keys?: array, + * http_auth?: array, + * swagger_ui_extra_configuration?: mixed, // To pass extra configuration to Swagger UI, like docExpansion or filter. // Default: [] + * }, + * http_cache?: array{ + * public?: bool|Param|null, // To make all responses public by default. // Default: null + * invalidation?: bool|array{ // Enable the tags-based cache invalidation system. + * enabled?: bool|Param, // Default: false + * varnish_urls?: list, + * urls?: list, + * scoped_clients?: list, + * max_header_length?: int|Param, // Max header length supported by the cache server. // Default: 7500 + * request_options?: mixed, // To pass options to the client charged with the request. // Default: [] + * purger?: scalar|Param|null, // Specify a purger to use (available values: "api_platform.http_cache.purger.varnish.ban", "api_platform.http_cache.purger.varnish.xkey", "api_platform.http_cache.purger.souin"). // Default: "api_platform.http_cache.purger.varnish" + * xkey?: array{ // Deprecated: The "xkey" configuration is deprecated, use your own purger to customize surrogate keys or the appropriate parameters. + * glue?: scalar|Param|null, // xkey glue between keys // Default: " " + * }, + * }, + * }, + * mercure?: bool|array{ + * enabled?: bool|Param, // Default: false + * hub_url?: scalar|Param|null, // The URL sent in the Link HTTP header. If not set, will default to the URL for MercureBundle's default hub. // Default: null + * include_type?: bool|Param, // Always include @type in updates (including delete ones). // Default: false + * }, + * messenger?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * elasticsearch?: bool|array{ + * enabled?: bool|Param, // Default: false + * hosts?: list, + * ssl_ca_bundle?: scalar|Param|null, // Path to the SSL CA bundle file for Elasticsearch SSL verification. // Default: null + * ssl_verification?: bool|Param, // Enable or disable SSL verification for Elasticsearch connections. // Default: true + * client?: "elasticsearch"|"opensearch"|Param, // The search engine client to use: "elasticsearch" or "opensearch". // Default: "elasticsearch" + * }, + * openapi?: array{ + * contact?: array{ + * name?: scalar|Param|null, // The identifying name of the contact person/organization. // Default: null + * url?: scalar|Param|null, // The URL pointing to the contact information. MUST be in the format of a URL. // Default: null + * email?: scalar|Param|null, // The email address of the contact person/organization. MUST be in the format of an email address. // Default: null + * }, + * termsOfService?: scalar|Param|null, // A URL to the Terms of Service for the API. MUST be in the format of a URL. // Default: null + * tags?: list, + * license?: array{ + * name?: scalar|Param|null, // The license name used for the API. // Default: null + * url?: scalar|Param|null, // URL to the license used for the API. MUST be in the format of a URL. // Default: null + * identifier?: scalar|Param|null, // An SPDX license expression for the API. The identifier field is mutually exclusive of the url field. // Default: null + * }, + * swagger_ui_extra_configuration?: mixed, // To pass extra configuration to Swagger UI, like docExpansion or filter. // Default: [] + * scalar_extra_configuration?: mixed, // To pass extra configuration to Scalar API Reference, like theme or darkMode. // Default: [] + * overrideResponses?: bool|Param, // Whether API Platform adds automatic responses to the OpenAPI documentation. // Default: true + * error_resource_class?: scalar|Param|null, // The class used to represent errors in the OpenAPI documentation. // Default: null + * validation_error_resource_class?: scalar|Param|null, // The class used to represent validation errors in the OpenAPI documentation. // Default: null + * }, + * maker?: bool|array{ + * enabled?: bool|Param, // Default: true + * namespace_prefix?: scalar|Param|null, // Add a prefix to all maker generated classes. e.g set it to "Api" to set the maker namespace to "App\Api\" (if the maker.root_namespace config is App). e.g. App\Api\State\MyStateProcessor // Default: "" + * }, + * mcp?: bool|array{ + * enabled?: bool|Param, // Default: true + * format?: scalar|Param|null, // The serialization format used for MCP tool input/output. Must be a format registered in api_platform.formats (e.g. "jsonld", "json", "jsonapi"). // Default: "jsonld" + * }, + * exception_to_status?: array, + * formats?: array, + * }>, + * patch_formats?: array, + * }>, + * docs_formats?: array, + * }>, + * error_formats?: array, + * }>, + * jsonschema_formats?: list, + * defaults?: array{ + * uri_template?: mixed, + * short_name?: mixed, + * description?: mixed, + * types?: mixed, + * operations?: mixed, + * formats?: mixed, + * input_formats?: mixed, + * output_formats?: mixed, + * uri_variables?: mixed, + * route_prefix?: mixed, + * defaults?: mixed, + * requirements?: mixed, + * options?: mixed, + * stateless?: mixed, + * sunset?: mixed, + * accept_patch?: mixed, + * status?: mixed, + * host?: mixed, + * schemes?: mixed, + * condition?: mixed, + * controller?: mixed, + * class?: mixed, + * url_generation_strategy?: mixed, + * deprecation_reason?: mixed, + * headers?: mixed, + * cache_headers?: mixed, + * normalization_context?: mixed, + * denormalization_context?: mixed, + * collect_denormalization_errors?: mixed, + * hydra_context?: mixed, + * openapi?: mixed, + * validation_context?: mixed, + * filters?: mixed, + * mercure?: mixed, + * messenger?: mixed, + * input?: mixed, + * output?: mixed, + * order?: mixed, + * fetch_partial?: mixed, + * force_eager?: mixed, + * pagination_client_enabled?: mixed, + * pagination_client_items_per_page?: mixed, + * pagination_client_partial?: mixed, + * pagination_via_cursor?: mixed, + * pagination_enabled?: mixed, + * pagination_fetch_join_collection?: mixed, + * pagination_use_output_walkers?: mixed, + * pagination_items_per_page?: mixed, + * pagination_maximum_items_per_page?: mixed, + * pagination_partial?: mixed, + * pagination_type?: mixed, + * security?: mixed, + * security_message?: mixed, + * security_post_denormalize?: mixed, + * security_post_denormalize_message?: mixed, + * security_post_validation?: mixed, + * security_post_validation_message?: mixed, + * composite_identifier?: mixed, + * exception_to_status?: mixed, + * query_parameter_validation_enabled?: mixed, + * links?: mixed, + * graph_ql_operations?: mixed, + * provider?: mixed, + * processor?: mixed, + * state_options?: mixed, + * rules?: mixed, + * policy?: mixed, + * middleware?: mixed, + * parameters?: array + * }>, + * strict_query_parameter_validation?: mixed, + * hide_hydra_operation?: mixed, + * json_stream?: mixed, + * extra_properties?: mixed, + * map?: mixed, + * mcp?: mixed, + * route_name?: mixed, + * errors?: mixed, + * read?: mixed, + * deserialize?: mixed, + * validate?: mixed, + * write?: mixed, + * serialize?: mixed, + * content_negotiation?: mixed, + * priority?: mixed, + * name?: mixed, + * allow_create?: mixed, + * item_uri_template?: mixed, + * ... + * }, + * } + * @psalm-type AiConfig = array{ + * platform?: array{ + * albert?: array{ + * api_key?: string|Param, + * base_url?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * amazeeai?: array{ + * base_url?: string|Param, + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * anthropic?: array{ + * api_key?: string|Param, + * version?: string|Param, // Default: null + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * cache_retention?: "none"|"short"|"long"|Param, // Prompt cache retention policy for Anthropic models // Default: "short" + * }, + * azure?: array, + * bedrock?: array, + * cache?: array, + * cartesia?: array{ + * api_key?: string|Param, + * version?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * cerebras?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * cohere?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * decart?: array{ + * api_key?: string|Param, + * host?: string|Param, // Default: "https://api.decart.ai/v1" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * deepseek?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * dockermodelrunner?: array{ + * host_url?: string|Param, // Default: "http://127.0.0.1:12434" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * elevenlabs?: array{ + * api_key?: string|Param, + * endpoint?: string|Param, // Default: "https://api.elevenlabs.io/v1/" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * failover?: array, + * rate_limiter?: string|Param, + * }>, + * gemini?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * generic?: array, + * huggingface?: array{ + * api_key?: string|Param, + * provider?: string|Param, // Default: "hf-inference" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * lmstudio?: array{ + * host_url?: string|Param, // Default: "http://127.0.0.1:1234" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * mistral?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * ollama?: array{ + * endpoint?: string|Param, // Endpoint for Ollama (e.g. "http://127.0.0.1:11434" for local, or a cloud endpoint). If null, the http_client is used as-is and must already be configured with a base URI. + * api_key?: string|Param, // API key for Ollama Cloud authentication (optional for local usage) + * http_client?: string|Param, // Service ID of the HTTP client to use. When "endpoint" is null, this client must be pre-configured (e.g. with a base_uri). // Default: "http_client" + * }, + * openai?: array{ + * api_key?: string|Param, + * region?: scalar|Param|null, // The region for OpenAI API (EU, US, or null for default) // Default: null + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * openresponses?: array, + * openrouter?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * ovh?: array{ + * api_key?: scalar|Param|null, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * perplexity?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * scaleway?: array{ + * api_key?: scalar|Param|null, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * transformersphp?: array, + * vertexai?: array{ + * location?: string|Param, // Required for the project-scoped endpoint. Must be set together with "project_id". // Default: null + * project_id?: string|Param, // Required for the project-scoped endpoint. Must be set together with "location". // Default: null + * api_key?: string|Param, // When set without "location" and "project_id", uses the global endpoint. Note: API keys only identify the project for billing and do not provide identity-based access control. For production use with IAM, audit logging, or data residency, prefer the project-scoped endpoint with service account authentication. // Default: null + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * voyage?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * }, + * model?: array|\Symfony\AI\Platform\Capability|Param>, + * }>>, + * agent?: array, + * }, + * keep_tool_messages?: bool|Param, // Keep tool messages in the conversation history // Default: false + * include_sources?: bool|Param, // Include sources exposed by tools as part of the tool result metadata // Default: false + * fault_tolerant_toolbox?: bool|Param, // Continue the agent run even if a tool call fails // Default: true + * speech?: bool|array{ // Speech (TTS/STT) decorator configuration + * enabled?: bool|Param, // Default: true + * text_to_speech_platform?: string|Param, // Service name of the TTS platform (e.g. ai.platform.elevenlabs). // Default: null + * speech_to_text_platform?: string|Param, // Service name of the STT platform (e.g. ai.platform.openai). // Default: null + * tts_model?: string|Param, // Text-to-speech model name // Default: null + * tts_options?: mixed, // Provider-specific TTS options // Default: [] + * stt_model?: string|Param, // Speech-to-text model name // Default: null + * stt_options?: mixed, // Provider-specific STT options // Default: [] + * }, + * }>, + * multi_agent?: array>, + * fallback?: string|Param, // Service ID of the fallback agent for unmatched requests + * }>, + * store?: array{ + * azuresearch?: array, + * cache?: array, + * chromadb?: array, + * clickhouse?: array, + * cloudflare?: array, + * elasticsearch?: array, + * manticoresearch?: array, + * mariadb?: array, + * meilisearch?: array, + * memory?: array, + * milvus?: array, + * mongodb?: array, + * neo4j?: array, + * opensearch?: array, + * pinecone?: array, + * top_k?: int|Param, + * }>, + * postgres?: array, + * qdrant?: array, + * redis?: array, + * s3vectors?: array, + * vector_bucket_name?: string|Param, + * index_name?: string|Param, + * filter?: array, + * top_k?: int|Param, // Default number of results to return // Default: 3 + * }>, + * sqlite?: array, + * supabase?: array, + * surrealdb?: array, + * typesense?: array, + * weaviate?: array, + * vektor?: array, + * }, + * message_store?: array{ + * cache?: array, + * cloudflare?: array, + * doctrine?: array{ + * dbal?: array, + * }, + * meilisearch?: array, + * memory?: array, + * mongodb?: array, + * pogocache?: array, + * redis?: array, + * session?: array, + * surrealdb?: array, + * }, + * chat?: array, + * vectorizer?: array, + * indexer?: array, + * filters?: list, + * vectorizer?: scalar|Param|null, // Service name of vectorizer // Default: "Symfony\\AI\\Store\\Document\\VectorizerInterface" + * store?: string|Param, // Service name of store // Default: "Symfony\\AI\\Store\\StoreInterface" + * }>, + * retriever?: array, + * } + * @psalm-type ConfigType = array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * twig?: TwigConfig, + * monolog?: MonologConfig, + * webpack_encore?: WebpackEncoreConfig, + * datatables?: DatatablesConfig, + * liip_imagine?: LiipImagineConfig, + * twig_extra?: TwigExtraConfig, + * gregwar_captcha?: GregwarCaptchaConfig, + * florianv_swap?: FlorianvSwapConfig, + * nelmio_security?: NelmioSecurityConfig, + * turbo?: TurboConfig, + * tfa_webauthn?: TfaWebauthnConfig, + * scheb_two_factor?: SchebTwoFactorConfig, + * webauthn?: WebauthnConfig, + * nbgrp_onelogin_saml?: NbgrpOneloginSamlConfig, + * stimulus?: StimulusConfig, + * ux_translator?: UxTranslatorConfig, + * dompdf_font_loader?: DompdfFontLoaderConfig, + * knpu_oauth2_client?: KnpuOauth2ClientConfig, + * nelmio_cors?: NelmioCorsConfig, + * jbtronics_settings?: JbtronicsSettingsConfig, + * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, + * "when@dev"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * twig?: TwigConfig, + * web_profiler?: WebProfilerConfig, + * monolog?: MonologConfig, + * debug?: DebugConfig, + * maker?: MakerConfig, + * webpack_encore?: WebpackEncoreConfig, + * datatables?: DatatablesConfig, + * liip_imagine?: LiipImagineConfig, + * twig_extra?: TwigExtraConfig, + * gregwar_captcha?: GregwarCaptchaConfig, + * florianv_swap?: FlorianvSwapConfig, + * nelmio_security?: NelmioSecurityConfig, + * turbo?: TurboConfig, + * tfa_webauthn?: TfaWebauthnConfig, + * scheb_two_factor?: SchebTwoFactorConfig, + * webauthn?: WebauthnConfig, + * nbgrp_onelogin_saml?: NbgrpOneloginSamlConfig, + * stimulus?: StimulusConfig, + * ux_translator?: UxTranslatorConfig, + * dompdf_font_loader?: DompdfFontLoaderConfig, + * knpu_oauth2_client?: KnpuOauth2ClientConfig, + * nelmio_cors?: NelmioCorsConfig, + * jbtronics_settings?: JbtronicsSettingsConfig, + * jbtronics_translation_editor?: JbtronicsTranslationEditorConfig, + * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, + * }, + * "when@docker"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * twig?: TwigConfig, + * monolog?: MonologConfig, + * webpack_encore?: WebpackEncoreConfig, + * datatables?: DatatablesConfig, + * liip_imagine?: LiipImagineConfig, + * twig_extra?: TwigExtraConfig, + * gregwar_captcha?: GregwarCaptchaConfig, + * florianv_swap?: FlorianvSwapConfig, + * nelmio_security?: NelmioSecurityConfig, + * turbo?: TurboConfig, + * tfa_webauthn?: TfaWebauthnConfig, + * scheb_two_factor?: SchebTwoFactorConfig, + * webauthn?: WebauthnConfig, + * nbgrp_onelogin_saml?: NbgrpOneloginSamlConfig, + * stimulus?: StimulusConfig, + * ux_translator?: UxTranslatorConfig, + * dompdf_font_loader?: DompdfFontLoaderConfig, + * knpu_oauth2_client?: KnpuOauth2ClientConfig, + * nelmio_cors?: NelmioCorsConfig, + * jbtronics_settings?: JbtronicsSettingsConfig, + * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, + * }, + * "when@prod"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * twig?: TwigConfig, + * monolog?: MonologConfig, + * webpack_encore?: WebpackEncoreConfig, + * datatables?: DatatablesConfig, + * liip_imagine?: LiipImagineConfig, + * twig_extra?: TwigExtraConfig, + * gregwar_captcha?: GregwarCaptchaConfig, + * florianv_swap?: FlorianvSwapConfig, + * nelmio_security?: NelmioSecurityConfig, + * turbo?: TurboConfig, + * tfa_webauthn?: TfaWebauthnConfig, + * scheb_two_factor?: SchebTwoFactorConfig, + * webauthn?: WebauthnConfig, + * nbgrp_onelogin_saml?: NbgrpOneloginSamlConfig, + * stimulus?: StimulusConfig, + * ux_translator?: UxTranslatorConfig, + * dompdf_font_loader?: DompdfFontLoaderConfig, + * knpu_oauth2_client?: KnpuOauth2ClientConfig, + * nelmio_cors?: NelmioCorsConfig, + * jbtronics_settings?: JbtronicsSettingsConfig, + * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, + * }, + * "when@test"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * twig?: TwigConfig, + * web_profiler?: WebProfilerConfig, + * monolog?: MonologConfig, + * debug?: DebugConfig, + * webpack_encore?: WebpackEncoreConfig, + * datatables?: DatatablesConfig, + * liip_imagine?: LiipImagineConfig, + * dama_doctrine_test?: DamaDoctrineTestConfig, + * twig_extra?: TwigExtraConfig, + * gregwar_captcha?: GregwarCaptchaConfig, + * florianv_swap?: FlorianvSwapConfig, + * nelmio_security?: NelmioSecurityConfig, + * turbo?: TurboConfig, + * tfa_webauthn?: TfaWebauthnConfig, + * scheb_two_factor?: SchebTwoFactorConfig, + * webauthn?: WebauthnConfig, + * nbgrp_onelogin_saml?: NbgrpOneloginSamlConfig, + * stimulus?: StimulusConfig, + * ux_translator?: UxTranslatorConfig, + * dompdf_font_loader?: DompdfFontLoaderConfig, + * knpu_oauth2_client?: KnpuOauth2ClientConfig, + * nelmio_cors?: NelmioCorsConfig, + * jbtronics_settings?: JbtronicsSettingsConfig, + * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, + * }, + * ..., + * }> + * } + */ +final class App +{ + /** + * @param ConfigType $config + * + * @psalm-return ConfigType + */ + public static function config(array $config): array + { + /** @var ConfigType $config */ + $config = AppReference::config($config); + + return $config; + } +} + +namespace Symfony\Component\Routing\Loader\Configurator; + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'routing.controllers', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array{ + * "when@dev"?: array, + * "when@docker"?: array, + * "when@prod"?: array, + * "when@test"?: array, + * ... + * } + */ +final class Routes +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return $config; + } +} diff --git a/config/routes.yaml b/config/routes.yaml index 8b38fa71..4830b774 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -1,3 +1,12 @@ +# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json + +# This file is the entry point to configure the routes of your app. +# Methods with the #[Route] attribute are automatically imported. +# See also https://symfony.com/doc/current/routing.html + +# To list all registered routes, run the following command: +# bin/console debug:router + # Redirect every url without an locale to the locale of the user/the global base locale scan_qr: @@ -16,4 +25,4 @@ redirector: url: ".*" controller: App\Controller\RedirectController::addLocalePart # Dont match localized routes (no redirection loop, if no root with that name exists) or API prefixed routes - condition: "not (request.getPathInfo() matches '/^\\\\/([a-z]{2}(_[A-Z]{2})?|api)\\\\//')" \ No newline at end of file + condition: "not (request.getPathInfo() matches '/^\\\\/([a-z]{2}(_[A-Z]{2})?|api)\\\\//')" diff --git a/config/services.yaml b/config/services.yaml index 17611cea..5021c577 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,5 +1,8 @@ +# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json + # This file is the entry point to configure your own services. # Files in the packages/ subdirectory configure your dependencies. +# See also https://symfony.com/doc/current/service_container/import.html # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration @@ -29,6 +32,9 @@ services: # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/' + exclude: + - '../src/Entity/' + - '../src/Helpers/' # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class @@ -227,9 +233,15 @@ services: arguments: $enforce_index_php: '%env(bool:NO_URL_REWRITE_AVAILABLE)%' - App\Doctrine\Purger\ResetAutoIncrementPurgerFactory: + App\Repository\PartRepository: + arguments: + $translator: '@translator' + tags: ['doctrine.repository_service'] + + App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber: tags: - - { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' } + - { name: doctrine.event_listener, event: onFlush, connection: default } + # We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container. App\Services\UserSystem\PermissionPresetsHelper: @@ -262,7 +274,11 @@ services: tags: - { name: monolog.processor } -when@test: + App\Doctrine\Purger\ResetAutoIncrementPurgerFactory: + tags: + - { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' } + +when@test: &test services: # Decorate the doctrine fixtures load command to use our custom purger by default doctrine.fixtures_load_command.custom: diff --git a/crowdin.yml b/crowdin.yml index 7465d429..f1fc011d 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -5,3 +5,5 @@ files: translation: /translations/validators.%two_letters_code%.xlf - source: /translations/security.en.xlf translation: /translations/security.%two_letters_code%.xlf + - source: /translations/frontend.en.xlf + translation: /translations/frontend.%two_letters_code%.xlf diff --git a/docs/api/intro.md b/docs/api/intro.md index 78a8d2c1..283afbe6 100644 --- a/docs/api/intro.md +++ b/docs/api/intro.md @@ -17,7 +17,7 @@ This allows external applications to interact with Part-DB, extend it or integra > Some features might be missing or not working yet. > Also be aware, that there might be security issues in the API, which could allow attackers to access or edit data via > the API, which -> they normally should be able to access. So currently you should only use the API with trusted users and trusted +> they normally should not be able to access. So currently you should only use the API with trusted users and trusted > applications. Part-DB uses [API Platform](https://api-platform.com/) to provide the API, which allows for easy creation of REST APIs @@ -46,7 +46,7 @@ See [Authentication chapter]({% link api/authentication.md %}) for more details. The API is split into different endpoints, which are reachable under the `/api/` path of your Part-DB instance ( e.g. `https://your-part-db.local/api/`). -There are various endpoints for each entity type (like `part`, `manufacturer`, etc.), which allow you to read and write data, and some special endpoints like `search` or `statistics`. +There are various endpoints for each entity type (like `parts`, `manufacturers`, etc.), which allow you to read and write data, and some special endpoints like `search` or `statistics`. For example, all API endpoints for managing categories are available under `/api/categories/`. Depending on the exact path and the HTTP method used, you can read, create, update or delete categories. @@ -56,7 +56,7 @@ For most entities, there are endpoints like this: * **POST**: `/api/categories/` - Create a new category * **GET**: `/api/categories/{id}` - Get a specific category by its ID * **DELETE**: `/api/categories/{id}` - Delete a specific category by its ID -* **UPDATE**: `/api/categories/{id}` - Update a specific category by its ID. Only the fields which are sent in the +* **PATCH**: `/api/categories/{id}` - Update a specific category by its ID. Only the fields which are sent in the request are updated, all other fields are left unchanged. Be aware that you have to set the [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) content type header (`Content-Type: application/merge-patch+json`) for this to work. @@ -106,11 +106,11 @@ This is a great way to test the API and see how it works, without having to writ By default, all list endpoints are paginated, which means only a certain number of results is returned per request. To get another page of the results, you have to use the `page` query parameter, which contains the page number you want -to get (e.g. `/api/categoues/?page=2`). +to get (e.g. `/api/categories/?page=2`). When using JSONLD, the links to the next page are also included in the `hydra:view` property of the response. To change the size of the pages (the number of items in a single page) use the `itemsPerPage` query parameter ( -e.g. `/api/categoues/?itemsPerPage=50`). +e.g. `/api/categories/?itemsPerPage=50`). See [API Platform docs](https://api-platform.com/docs/core/pagination) for more infos. diff --git a/docs/concepts.md b/docs/concepts.md index ddf38633..8a3551bd 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -23,14 +23,14 @@ each other so that it does not matter which one of your 1000 things of Part you A part entity has many fields, which can be used to describe it better. Most of the fields are optional: * **Name** (Required): The name of the part or how you want to call it. This could be a manufacturer-provided name, or a - name you thought of yourself. Each name needs to be unique and must exist in a single category. + name you thought of yourself. Each name needs to be unique and must exist in a single category only. * **Description**: A short (single-line) description of what this part is/does. For longer information, you should use the comment field or the specifications * **Category** (Required): The category (see there) to which this part belongs to. * **Tags**: The list of tags this part belongs to. Tags can be used to group parts logically (similar to the category), - but tags are much less strict and formal (they don't have to be defined forehands) and you can assign multiple tags to + but tags are much less strict and formal (they don't have to be defined beforehand) and you can assign multiple tags to a part. When clicking on a tag, a list with all parts which have the same tag, is shown. -* **Min Instock**: *Not really implemented yet*. Parts where the total instock is below this value, will show up for +* **Min Instock**: *Not fully implemented yet*. Parts where the total instock is below this value will show up for ordering. * **Footprint**: See there. Useful especially for electronic parts, which have one of the common electronic footprints ( like DIP8, SMD0805 or similar). If a part has no explicitly defined preview picture, the preview picture of its @@ -48,9 +48,9 @@ A part entity has many fields, which can be used to describe it better. Most of completely trustworthy. * **Favorite**: Parts with this flag are highlighted in parts lists * **Mass**: The mass of a single piece of this part (so of a single transistor). Given in grams. -* **Internal Part number** (IPN): Each part is automatically assigned a numerical ID that identifies a part in the - database. This ID depends on when a part was created and can not be changed. If you want to assign your own unique - identifiers, or sync parts identifiers with the identifiers of another database you can use this field. +* **Internal Part Number** (IPN): Each part is automatically assigned a numerical ID that identifies a part in the + database. This ID depends on when a part was created and cannot be changed. If you want to assign your own unique + identifiers, or sync parts identifiers with the identifiers of another database, you can use this field. ### Stock / Part lot @@ -99,12 +99,12 @@ possible category tree could look like this: ### Supplier -A Supplier is a vendor/distributor where you can buy/order parts. Price information of parts is associated with a +A supplier is a vendor/distributor where you can buy/order parts. Price information of parts is associated with a supplier. ### Manufacturer -A manufacturer represents the company that manufacturers/builds various parts (not necessarily sell them). If the +A manufacturer represents the company that manufactures/builds various parts (not necessarily sells them). If the manufacturer also sells the parts, you have to create a supplier for that. ### Storage location diff --git a/docs/configuration.md b/docs/configuration.md index d4b21781..7ba716c6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,11 +6,11 @@ nav_order: 5 # Configuration -Part-DBs behavior can be configured to your needs. There are different kinds of configuration options: Options, which are +Part-DB's behavior can be configured to your needs. There are different kinds of configuration options: Options that are user-changeable (changeable dynamically via frontend), options that can be configured by environment variables, and options that are only configurable via Symfony config files. -## User configruation +## User configuration The following things can be changed for every user and a user can change it for himself (if he has the correct permission for it). Configuration is either possible via the user's own settings page (where you can also change the password) or via @@ -40,10 +40,10 @@ The following configuration options can only be changed by the server administra variables, changing the `.env.local` file or setting env for your docker container. Here are just the most important options listed, see `.env` file for the full list of possible env variables. -Environment variables allow to overwrite settings in the web interface. This is useful, if you want to enforce certain -settings to be unchangable by users, or if you want to configure settings in a central place in a deployed environment. +Environment variables allow you to overwrite settings in the web interface. This is useful if you want to enforce certain +settings to be unchangeable by users, or if you want to configure settings in a central place in a deployed environment. On the settings page, you can hover over a setting to see, which environment variable can be used to overwrite it, it -is shown as tooltip. API keys or similar sensitve data which is overwritten by env variables, are redacted on the web +is shown as tooltip. API keys or similar sensitive data which is overwritten by env variables, are redacted on the web interface, so that even administrators cannot see them (only the last 2 characters and the length). For technical and security reasons some settings can only be configured via environment variables and not via the web @@ -86,6 +86,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `ATTACHMENT_DOWNLOAD_BY_DEFAULT`: When this is set to 1, the "download external file" checkbox is checked by default when adding a new attachment. Otherwise, it is unchecked by default. Use this if you wanna download all attachments locally by default. Attachment download is only possible, when `ALLOW_ATTACHMENT_DOWNLOADS` is set to 1. +* `ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK` (default `0`): When this is set to 1, users can make Part-DB directly download a file specified as a URL from the local network and create it as a local file. This allows users access to all resources available in the local network, which could be a security risk, so use this only if you trust your users and have a secure local network. +* `ATTACHMENT_SHOW_HTML_FILES`: When enabled, user uploaded HTML attachments can be viewed directly in the browser. + Many potential malicious functions are restricted, still this is a potential security risk and should only be enabled, + if you trust the users who can upload files. When set to 0, HTML files are rendered as plain text. * `USE_GRAVATAR`: Set to `1` to use [gravatar.com](https://gravatar.com/) images for user avatars (as long as they have not set their own picture). The users browsers have to download the pictures from a third-party (gravatar) server, so this might be a privacy risk. @@ -105,17 +109,29 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `part_delete`: Delete operation of an existing part * `part_create`: Creation of a new part * `part_stock_operation`: Stock operation on a part (therefore withdraw, add or move stock) - * `datastructure_edit`: Edit operation of an existing datastructure (e.g. category, manufacturer, ...) - * `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...) - * `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...) -* `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new - versions, or if your server can not connect to the internet. + * `datastructure_edit`: Edit operation of an existing data structure (e.g. category, manufacturer, ...) + * `datastructure_delete`: Delete operation of an existing data structure (e.g. category, manufacturer, ...) + * `datastructure_create`: Creation of a new data structure (e.g. category, manufacturer, ...) +* `CHECK_FOR_UPDATES` (default `1`): Set this to 0 if you do not want Part-DB to connect to GitHub to check for new + versions, or if your server cannot connect to the internet. * `APP_SECRET` (env only): This variable is a configuration parameter used for various security-related purposes, particularly for securing and protecting various aspects of your application. It's a secret key that is used for cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this value should be handled as confidential data and not shared publicly. * `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the part image gallery +* `IPN_SUGGEST_REGEX`: A global regular expression, that part IPNs have to fulfill. Enforce your own format for your users. +* `IPN_SUGGEST_REGEX_HELP`: Define your own user help text for the Regex format specification. +* `IPN_AUTO_APPEND_SUFFIX`: When enabled, an incremental suffix will be added to the user input when entering an existing +* IPN again upon saving. +* `IPN_SUGGEST_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number). + IPN prefixes, maintained within part categories and their hierarchy, form the foundation for suggesting complete IPNs. + These suggestions become accessible during IPN input of a part. The constant specifies the digits used to calculate and assign + unique increments for parts within a category hierarchy, ensuring consistency and uniqueness in IPN generation. +* `IPN_USE_DUPLICATE_DESCRIPTION`: When enabled, the part’s description is used to find existing parts with the same + description and to determine the next available IPN by incrementing their numeric suffix for the suggestion list. +* `KEYBINDINGS_SPECIAL_CHARS_ENABLED`: Set this to 0 to disable the special character keybindings (Alt + key) for inserting special characters. This can be useful if + they conflict with your keyboard layout or system shortcuts. ### E-Mail settings (all env only) @@ -129,6 +145,18 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email notification. You have to configure the mail provider first before via the MAILER_DSN setting. +### Update manager settings +* `DISABLE_WEB_UPDATES` (default `1`): Set this to 0 to enable web-based updates. When enabled, you can perform updates + via the web interface in the update manager. This is disabled by default for security reasons, as it can be a risk if + not used carefully. You can still use the CLI commands to perform updates, even when web updates are disabled. +* `DISABLE_BACKUP_RESTORE` (default `1`): Set this to 0 to enable backup restore via the web interface. When enabled, you can + restore backups via the web interface in the update manager. This is disabled by default for security reasons, as it can + be a risk if not used carefully. You can still use the CLI commands to perform backup restores, even when web-based + backup restore is disabled. +* `DISABLE_BACKUP_DOWNLOAD` (default `1`): Set this to 0 to enable backup download via the web interface. When enabled, you can download backups via the web interface + in the update manager. This is disabled by default for security reasons, as it can be a risk if not used carefully, as + the downloads contain sensitive data like password hashes or secrets. + ### Table related settings * `TABLE_DEFAULT_PAGE_SIZE`: The default page size for tables. This is the number of rows which are shown per page. Set @@ -136,7 +164,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `TABLE_PARTS_DEFAULT_COLUMNS`: The columns in parts tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns - are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. + are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `partCustomState`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. ### History/Eventlog-related settings @@ -252,10 +280,10 @@ markdown (and even some subset of HTML) syntax to format the text. ## parameters.yaml -You can also configure some options via the `config/parameters.yaml` file. This should normally not need, -and you should know what you are doing, when you change something here. You should expect, that you will have to do some -manual merge, when you have changed something here and update to a newer version of Part-DB. It is possible that -configuration options here will change or be completely removed in future versions of Part-DB. +You can also configure some options via the `config/parameters.yaml` file. This should normally not be needed, +and you should know what you are doing when you change something here. You should expect that you will have to do some +manual merges when you have changed something here and update to a newer version of Part-DB. It is possible that +configuration options here will change or be completely removed in future versions of Part-DB. If you change something here, you have to clear the cache, before the changes will take effect with the command `bin/console cache:clear`. diff --git a/docs/index.md b/docs/index.md index d732f31d..700937f4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,8 +18,7 @@ It is installed on a web server and so can be accessed with any browser without > You can log in with username: **user** and password: **user**, to change/create data. > > Every change to the master branch gets automatically deployed, so it represents the current development progress and -> is -> maybe not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading +> may not be completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading > the page > for the first time. @@ -28,32 +27,33 @@ It is installed on a web server and so can be accessed with any browser without * Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer, and multiple store locations and price information. Parts can be grouped using tags. You can associate various files like datasheets or pictures with the parts. -* Multi-language support (currently German, English, Russian, Japanese and French (experimental)) -* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the builtin barcode scanner -* User system with groups and detailed (fine granular) permissions. +* Multi-language support (currently German, English, Russian, Japanese, French, Czech, Danish, and Chinese) +* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the built-in barcode scanner +* User system with groups and detailed (fine-grained) permissions. Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups. - Password reset via email can be setup. + Password reset via email can be set up. * Optional support for single sign-on (SSO) via SAML (using an intermediate service like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) * Import/Export system * Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build this project and directly withdraw all components needed from DB -* Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older +* Event log: Track what changes happen to your inventory, track which user does what. Revert your parts to older versions. -* Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface. +* Responsive design: You can use Part-DB on your PC, your tablet, and your smartphone using the same interface. * MySQL, SQLite and PostgreSQL are supported as database backends * Support for rich text descriptions and comments in parts * Support for multiple currencies and automatic update of exchange rates supported * Powerful search and filter function, including parametric search (search for parts according to some specifications) * Easy migration from an existing PartKeepr instance (see [here]({%link partkeepr_migration.md %})) -* Use cloud providers (like Octopart, Digikey, Farnell or TME) to automatically get part information, datasheets and +* Use cloud providers (like Octopart, Digikey, Farnell, Mouser, or TME) to automatically get part information, datasheets, and prices for parts (see [here]({% link usage/information_provider_system.md %})) +* Retrieve part information from arbitrary shop websites, using either conventional data extraction from structured metadata, or AI based data extraction * API to access Part-DB from other applications/scripts -* [Integration with KiCad]({%link usage/eda_integration.md %}): Use Part-DB as central datasource for your - KiCad and see available parts from Part-DB directly inside KiCad. +* [Integration with KiCad]({%link usage/eda_integration.md %}): Use Part-DB as the central datasource for your + KiCad and see available parts from Part-DB directly inside KiCad. -With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory, -or makerspaces, where many users have should have (controlled) access to the shared inventory. +With these features, Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory, +or makerspaces, where many users should have (controlled) access to the shared inventory. Part-DB is also used by small companies and universities for managing their inventory. @@ -68,11 +68,11 @@ See [LICENSE](https://github.com/Part-DB/Part-DB-symfony/blob/master/LICENSE) fo ## Donate for development If you want to donate to the Part-DB developer, see the sponsor button in the top bar (next to the repo name). -There you will find various methods to support development on a monthly or a one time base. +There you will find various methods to support development on a monthly or a one-time basis. ## Built with -* [Symfony 5](https://symfony.com/): The main framework used for the serverside PHP +* [Symfony 6](https://symfony.com/): The main framework used for the serverside PHP * [Bootstrap 5](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com/): Used as website theme * [Fontawesome](https://fontawesome.com/): Used as icon set * [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend diff --git a/docs/installation/choosing_database.md b/docs/installation/choosing_database.md index cd9657d4..27d70e54 100644 --- a/docs/installation/choosing_database.md +++ b/docs/installation/choosing_database.md @@ -21,8 +21,8 @@ differences between them, which might be important for you. Therefore the pros a are listed here. {: .important } -You have to choose between the database types before you start using Part-DB and **you can not change it (easily) after -you have started creating data**. So you should choose the database type for your use case (and possible future uses). +While you can change the database platform later (see below), it is still experimental and not recommended. +So you should choose the database type for your use case (and possible future uses). ## Comparison @@ -38,7 +38,7 @@ you have started creating data**. So you should choose the database type for you * **Performance**: SQLite is not as fast as MySQL or PostgreSQL, especially when using complex queries or many users. * **Emulated RegEx search**: SQLite does not support RegEx search natively. Part-DB can emulate it, however that is pretty slow. -* **Emualted natural sorting**: SQLite does not support natural sorting natively. Part-DB can emulate it, but it is pretty slow. +* **Emulated natural sorting**: SQLite does not support natural sorting natively. Part-DB can emulate it, but it is pretty slow. * **Limitations with Unicode**: SQLite has limitations in comparisons and sorting of Unicode characters, which might lead to unexpected behavior when using non-ASCII characters in your data. For example `µ` (micro sign) is not seen as equal to `μ` (greek minuscule mu), therefore searching for `µ` (micro sign) will not find parts containing `μ` (mu) and vice versa. @@ -131,7 +131,7 @@ The host (here 127.0.0.1) and port should also be specified according to your My In the `serverVersion` parameter you can specify the version of the MySQL/MariaDB server you are using, in the way the server returns it (e.g. `8.0.37` for MySQL and `10.4.14-MariaDB`). If you do not know it, you can leave the default value. -If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `unix_socket` parameter. +If you want to use a unix socket for the connection instead of a TCP connection, you can specify the socket path in the `unix_socket` parameter. ```shell DATABASE_URL="mysql://user:password@localhost/database?serverVersion=8.0.37&unix_socket=/var/run/mysqld/mysqld.sock" ``` @@ -150,7 +150,7 @@ In the `serverVersion` parameter you can specify the version of the PostgreSQL s The `charset` parameter specify the character set of the database. It should be set to `utf8` to ensure that all characters are stored correctly. -If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `host` parameter. +If you want to use a unix socket for the connection instead of a TCP connection, you can specify the socket path in the `host` parameter. ```shell DATABASE_URL="postgresql://db_user@localhost/db_name?serverVersion=16.6&charset=utf8&host=/var/run/postgresql" ``` @@ -177,6 +177,26 @@ In natural sorting, it would be sorted as: Part-DB can sort names in part tables and tree views naturally. PostgreSQL and MariaDB 10.7+ support natural sorting natively, and it is automatically used if available. -For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicity enabled by setting the +For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicitly enabled by setting the `DATABASE_EMULATE_NATURAL_SORT` environment variable to `1`. If it is 0 the classical binary sorting is used, on these databases. The emulations might have some quirks and issues, so it is recommended to use a database which supports natural sorting natively, if you want to use it. + +## Converting between database platforms + +{: .important } +The database conversion is still experimental. Therefore it is recommended to backup your database before performing a conversion, and check if everything works as expected afterwards. + +If you want to change the database platform of your Part-DB installation (e.g. from SQLite to MySQL/MariaDB or PostgreSQL, or vice versa), there is the `partdb:migrations:convert-db-platform` console command, which can help you with that: + +1. Make a backup of your current database to be safe if something goes wrong (see the backup documentation). +2. Ensure that your database is at the latest schema by running the migrations: `php bin/console doctrine:migrations:migrate` +3. Change the `DATABASE_URL` environment variable to the new database platform and connection information. Copy the old `DATABASE_URL` as you will need it later. +4. Run `php bin/console doctrine:migrations:migrate` again to create the database schema in the new database. You will not need the admin password, that is shown when running the migrations. +5. Run the conversion command, where you have to provide the old `DATABASE_URL` as parameter: `php bin/console partdb:migrations:convert-db-platform ` + Replace `with a line break # If you use a reverse proxy in front of Part-DB, you must configure the trusted proxies IP addresses here (see reverse proxy documentation for more information): - # - TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 + # - TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 + + # If you need to install additional composer packages (e.g., for specific mailer transports), you can specify them here: + # The packages will be installed automatically when the container starts + # - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer ``` 4. Customize the settings by changing the environment variables (or adding new ones). See [Configuration]({% link @@ -91,6 +95,11 @@ services: docker-compose up -d ``` +{: .warning } +> If you run a root console inside the docker container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell. +> Otherwise Part-DB console might use the wrong configuration to execute commands. + + 6. Create the initial database with ```bash @@ -136,19 +145,22 @@ services: # In docker env logs will be redirected to stderr - APP_ENV=docker - # Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to + # Uncomment this, if you want to use the automatic database migration feature. With this you do not have to # run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/ # folder (under .automigration-backup), so you can restore it, if the migration fails. # This feature is currently experimental, so use it at your own risk! # - DB_AUTOMIGRATE=true # You can configure Part-DB using the webUI or environment variables - # However you can add add any other environment configuration you want here + # However you can add any other environment configuration you want here # See .env file for all available options or https://docs.part-db.de/configuration.html - # Override value if you want to show to show a given text on homepage. - # When this is outcommented the webUI can be used to configure the banner + # Override value if you want to show a given text on homepage. + # When this is commented out the webUI can be used to configure the banner #- BANNER=This is a test banner
with a line break + + # If you need to install additional composer packages (e.g., for specific mailer transports), you can specify them here: + # - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer database: container_name: partdb_database @@ -169,6 +181,38 @@ services: ``` +### Installing additional composer packages + +If you need to use specific mailer transports or other functionality that requires additional composer packages, you can +install them automatically at container startup using the `COMPOSER_EXTRA_PACKAGES` environment variable. + +For example, if you want to use Mailgun as your email provider, you need to install the `symfony/mailgun-mailer` package. +Add the following to your docker-compose.yaml environment section: + +```yaml +environment: + - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer + - MAILER_DSN=mailgun+api://API_KEY:DOMAIN@default +``` + +You can specify multiple packages by separating them with spaces: + +```yaml +environment: + - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer +``` + +{: .info } +> The packages will be installed when the container starts. This may increase the container startup time on the first run. +> The installed packages will persist in the container until it is recreated. + +Common mailer packages you might need: +- `symfony/mailgun-mailer` - For Mailgun email service +- `symfony/sendgrid-mailer` - For SendGrid email service +- `symfony/brevo-mailer` - For Brevo (formerly Sendinblue) email service +- `symfony/amazon-mailer` - For Amazon SES email service +- `symfony/postmark-mailer` - For Postmark email service + ### Update Part-DB You can update Part-DB by pulling the latest image and restarting the container. @@ -180,6 +224,52 @@ docker-compose up -d docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate ``` +### Automatic updates via Watchtower (Web UI) + +Part-DB supports triggering Docker container updates directly from the web interface using [Watchtower](https://github.com/nicholas-fedor/watchtower). +When configured, administrators can check for and apply updates from the **System > Update Manager** page. + +{: .info } +> The original `containrrr/watchtower` project is no longer maintained (last release November 2023). These docs use the actively maintained community fork at [`nicholas-fedor/watchtower`](https://github.com/nicholas-fedor/watchtower), which is drop-in compatible with the original HTTP API. + +To enable this feature, add a Watchtower service to your `docker-compose.yaml` and configure the connection: + +```yaml +services: + partdb: + container_name: partdb + image: jbtronics/part-db1:latest + labels: + - com.centurylinklabs.watchtower.enable=true + environment: + # ... your existing environment variables ... + + # Watchtower integration for web-based updates + - WATCHTOWER_API_URL=http://watchtower:8080 + - WATCHTOWER_API_TOKEN=your-secret-token + # ... your existing ports/volumes ... + + watchtower: + image: ghcr.io/nicholas-fedor/watchtower:latest + container_name: watchtower + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - WATCHTOWER_HTTP_API_UPDATE=true + - WATCHTOWER_HTTP_API_TOKEN=your-secret-token + - WATCHTOWER_LABEL_ENABLE=true + - WATCHTOWER_CLEANUP=true +``` + +{: .important } +> Replace `your-secret-token` with a strong, unique token. The same token must be set in both the Part-DB (`WATCHTOWER_API_TOKEN`) and Watchtower (`WATCHTOWER_HTTP_API_TOKEN`) environment variables. + +{: .info } +> `WATCHTOWER_LABEL_ENABLE=true` ensures Watchtower only manages containers with the `com.centurylinklabs.watchtower.enable=true` label, preventing it from updating other containers on the same host. + +Once configured, the Update Manager page will show the Watchtower connection status and provide an **Update via Watchtower** button when a new version is available. Clicking it triggers Watchtower to pull the latest image and recreate the Part-DB container automatically. + ## Direct use of docker image You can use the `jbtronics/part-db1:master` image directly. You have to expose port 80 to a host port and configure diff --git a/docs/installation/nginx.md b/docs/installation/nginx.md index 84305975..db209d92 100644 --- a/docs/installation/nginx.md +++ b/docs/installation/nginx.md @@ -7,7 +7,7 @@ nav_order: 10 # Nginx -You can also use [nginx](https://www.nginx.com/) as webserver for Part-DB. Setup Part-DB with apache is a bit easier, so +You can also use [nginx](https://www.nginx.com/) as webserver for Part-DB. Setting up Part-DB with Apache is a bit easier, so this is the method shown in the guides. This guide assumes that you already have a working nginx installation with PHP configured. diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index d2e65e7f..f9752546 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -21,7 +21,7 @@ LDAP or Active Directory server. {: .warning } > This feature is currently in beta. Please report any bugs you find. -> So far it has only tested with Keycloak, but it should work with any SAML 2.0 compatible identity provider. +> So far it has only been tested with Keycloak, but it should work with any SAML 2.0 compatible identity provider. This guide will show you how to configure Part-DB with [Keycloak](https://www.keycloak.org/) as the SAML identity provider, but it should work with any SAML 2.0 compatible identity provider. @@ -75,8 +75,8 @@ the [Keycloak Getting Started Guide](https://www.keycloak.org/docs/latest/gettin ### Configure Part-DB to use SAML -1. Open the `.env.local` file of Part-DB (or the docker-compose.yaml) for edit -2. Set the `SAMLP_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the +1. Open the `.env.local` file of Part-DB (or the docker-compose.yaml) for editing +2. Set the `SAML_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the previous step. It should start with `MIEE` and end with `=`. 3. Set the `SAML_SP_X509_CERT` environment variable to the content of the Certificate field shown in the `Keys` tab of the SAML client in Keycloak. It should start with `MIIC` and end with `=`. diff --git a/docs/screenshots/update-manager-interface.png b/docs/screenshots/update-manager-interface.png new file mode 100644 index 00000000..62b35ffd Binary files /dev/null and b/docs/screenshots/update-manager-interface.png differ diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f20a7f22..a5b1b1c8 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -9,7 +9,7 @@ Sometimes things go wrong and Part-DB shows an error message. This page should h ## Error messages -When a common, easy fixable error occurs (like a non-up-to-date database), Part-DB will show you some short instructions +When a common, easily fixable error occurs (like a non-up-to-date database), Part-DB will show you some short instructions on how to fix the problem. If you have a problem that is not listed here, please open an issue on GitHub. ## General procedure @@ -28,9 +28,9 @@ php bin/console cache:clear php bin/console doctrine:migrations:migrate ``` -If this does not help, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-symfony). +If this does not help, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-server). -## Search for the user and reset the password: +## Search for a user and reset the password You can list all users with the following command: `php bin/console partdb:users:list` To reset the password of a user you can use the following @@ -50,6 +50,21 @@ docker-compose logs -f Please include the error logs in your issue on GitHub, if you open an issue. +## KiCad Integration Issues + +### "API responded with error code: 0: Unknown" + +If you get this error when trying to connect KiCad to Part-DB, it is most likely caused by KiCad not trusting your SSL/TLS certificate. + +**Cause:** KiCad does not trust self-signed SSL/TLS certificates. + +**Solutions:** +- Use HTTP instead of HTTPS for the `root_url` in your KiCad library configuration (only recommended for local networks) +- Use a certificate from a trusted Certificate Authority (CA) like [Let's Encrypt](https://letsencrypt.org/) +- Add your self-signed certificate to the system's trusted certificate store on the computer running KiCad (the exact steps depend on your operating system) + +For more information about KiCad integration, see the [EDA / KiCad integration](../usage/eda_integration.md) documentation. + ## Report Issue -If an error occurs, or you found a bug, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-symfony). +If an error occurs, or you found a bug, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-server). diff --git a/docs/upgrade/1_to_2.md b/docs/upgrade/1_to_2.md index c333136a..af5f1aa6 100644 --- a/docs/upgrade/1_to_2.md +++ b/docs/upgrade/1_to_2.md @@ -18,7 +18,7 @@ fulfilled by the official Part-DB docker image.* Part-DB 2.0 requires at least PHP 8.2 (newer versions are recommended). So if your existing Part-DB installation is still running PHP 8.1, you will have to upgrade your PHP version first. -The minimum required version of node.js is now 20.0 or newer, so if you are using 18.0, you will have to upgrade it too. +The minimum required version of node.js is now 22.0 or newer, so if you are using 18.0, you will have to upgrade it too. Most distributions should have the possibility to get backports for PHP 8.4 and modern nodejs, so you should be able to easily upgrade your system to the new requirements. Otherwise, you can use the official Part-DB docker image, which @@ -27,7 +27,7 @@ about the requirements at all. ## Changes * Configuration is now preferably done via a web settings interface. You can still use environment variables, these overwrite -the settings in the web interface. Existing configuration will still work, but you should consider migriting them to the +the settings in the web interface. Existing configuration will still work, but you should consider migrating them to the web interface as described below. * The `config/banner.md` file that could been used to customize the banner text, was removed. You can now set the banner text directly in the admin interface, or by setting the `BANNER` environment variable. If you want to keep your existing @@ -43,7 +43,7 @@ The upgrade process works very similar to a normal (minor release) upgrade. ### Direct installation -**Be sure to execute the following steps as the user that owns the Part-DB files (e.g. `www-data`, or your webserver user). So prepend a `sudo -u wwww-data` where necessary.** +**Be sure to execute the following steps as the user that owns the Part-DB files (e.g. `www-data`, or your webserver user). So prepend a `sudo -u www-data` where necessary.** 1. Make a backup of your existing Part-DB installation, including the database, data directories and the configuration files and `.env.local` file. The `php bin/console partdb:backup` command can help you with this. @@ -51,7 +51,7 @@ The `php bin/console partdb:backup` command can help you with this. 3. Remove the `var/cache/` directory inside the Part-DB installation to ensure that no old cache files remain. 4. Run `composer install --no-dev -o` to update the dependencies. 5. Run `yarn install` and `yarn build` to update the frontend assets. -6. Rund `php bin/console doctrine:migrations:migrate` to update the database schema. +6. Run `php bin/console doctrine:migrations:migrate` to update the database schema. 7. Clear the cache with `php bin/console cache:clear`. 8. Open your Part-DB instance in the browser and log in as an admin user. 9. Go to the user or group permissions page, and give yourself (and other administrators) the right to change system settings (under "System" and "Configuration"). @@ -60,6 +60,8 @@ The `php bin/console partdb:backup` command can help you with this. If you want to change them, you must migrate them to the settings interface as described below. ### Docker installation +**When running the console commands from inside a docker container's shell as root, be sure to use `sudo -E` to preserve the environment variables, so that they are correctly passed to the command.** + 1. Make a backup of your existing Part-DB installation, including the database, data directories and the configuration files and the file where you configure the docker environment variables. 2. Stop the existing Part-DB container with `docker compose down` 3. Ensure that your docker compose file uses the new latest images (either `latest` or `2` tag). @@ -79,7 +81,7 @@ To change it, you must migrate your environment variable configuration to the ne For this there is the new console command `settings:migrate-env-to-settings`, which reads in all environment variables used to overwrite settings and write them to the database, so that you can safely delete them from your environment variable configuration afterwards, without -loosing your configuration. +losing your configuration. To run the command, execute `php bin/console settings:migrate-env-to-settings --all` as webserver user (or run `docker exec --user=www-data -it partdb php bin/console settings:migrate-env-to-settings --all` for docker containers). It will list you all environment variables, it found and ask you for confirmation to migrate them. Answer with `yes` to migrate them and hit enter. diff --git a/docs/upgrade/index.md b/docs/upgrade/index.md index 95a9cc33..4462f0dd 100644 --- a/docs/upgrade/index.md +++ b/docs/upgrade/index.md @@ -5,5 +5,7 @@ nav_order: 7 has_children: true --- +# Upgrade + This section provides information on how to upgrade Part-DB to the latest version. -This is intended for major release upgrades, where requirements or things changes significantly. +This is intended for major release upgrades, where requirements or things change significantly. diff --git a/docs/upgrade/upgrade_legacy.md b/docs/upgrade/upgrade_legacy.md index 4dd29e4d..b83661f3 100644 --- a/docs/upgrade/upgrade_legacy.md +++ b/docs/upgrade/upgrade_legacy.md @@ -24,7 +24,7 @@ sections carefully before proceeding to upgrade. also more sensitive stuff like database migration works via CLI now, so you should have console access on your server. * Markdown/HTML is now used instead of BBCode for rich text in description and command fields. It is possible to migrate your existing BBCode to Markdown - via `php bin/console php bin/console partdb:migrations:convert-bbcode`. + via `php bin/console partdb:migrations:convert-bbcode`. * Server exceptions are not logged into event log anymore. For security reasons (exceptions can contain sensitive information) exceptions are only logged to server log (by default under './var/log'), so only the server admins can access it. * Profile labels are now saved in the database (before they were saved in a separate JSON file). **The profiles of legacy diff --git a/docs/usage/ai.md b/docs/usage/ai.md new file mode 100644 index 00000000..3a1fb419 --- /dev/null +++ b/docs/usage/ai.md @@ -0,0 +1,27 @@ +--- +layout: default +title: AI features +nav_order: 6 +parent: Usage +--- + +# AI features + +Part-DB can utilize large language Models (LLMs) to provide AI-powered features that can assist you in managing your parts and projects. +For now this is mostly the ability to extract part information from websites without any structured data. + +## AI platforms + +Part-DB is platform agnostic and can work with different AI platforms, both locally and in the cloud. They can be configured in the "AI" tab in the system settings. +Currently, the following platforms are supported: + +### OpenRouter + +[OpenRouter](https://openrouter.ai/) is a platform that provides access to various LLMs, including models from OpenAI, Anthropic, and more. +You can use OpenRouter to connect to different LLMs and use them for Part-DB's AI features. +You need to supply an API key for OpenRouter to use it as an AI platform in Part-DB. + +### LMStudio + +[LMStudio](https://lmstudio.ai/) is a local LLM hosting solution that allows you to run LLMs on your own hardware. You can use LMStudio to host your own LLM and connect it to Part-DB for AI features. +Currently only LMStudio without any authentication is supported. Supply your LMStudio instance URL (including the port) to use it as an AI platform in Part-DB. diff --git a/docs/usage/backup_restore.md b/docs/usage/backup_restore.md index bef3792d..c4444d24 100644 --- a/docs/usage/backup_restore.md +++ b/docs/usage/backup_restore.md @@ -6,7 +6,7 @@ parent: Usage # Backup and Restore Data -When working productively you should back up the data and configuration of Part-DB regularly to prevent data loss. This +When working productively, you should back up the data and configuration of Part-DB regularly to prevent data loss. This is also useful if you want to migrate your Part-DB instance from one server to another. In that case, you just have to back up the data on server 1, move the backup to server 2, install Part-DB on server 2, and restore the backup. @@ -27,7 +27,7 @@ for more info about these options. ## Backup (manual) -3 parts have to be backup-ed: The configuration files, which contain the instance-specific options, the +Three parts have to be backed up: The configuration files, which contain the instance-specific options, the uploaded files of attachments, and the database containing the most data of Part-DB. Everything else like thumbnails and cache files, are recreated automatically when needed. @@ -44,7 +44,7 @@ You have to recursively copy the `uploads/` folder and the `public/media` folder #### SQLite -If you are using sqlite, it is sufficient to just copy your `app.db` from your database location (normally `var/app.db`) +If you are using SQLite, it is sufficient to just copy your `app.db` from your database location (normally `var/app.db`) to your backup location. #### MySQL / MariaDB @@ -56,7 +56,7 @@ interface (`mysqldump -uBACKUP -pPASSWORD DATABASE`) ## Restore Install Part-DB as usual as described in the installation section, except for the database creation/migration part. You -have to use the same database type (SQLite or MySQL) as on the backuped server instance. +have to use the same database type (SQLite or MySQL) as on the backed up server instance. ### Restore configuration @@ -71,7 +71,7 @@ Copy the `uploads/` and the `public/media/` folder from your backup into your ne #### SQLite -Copy the backup-ed `app.db` into the database folder normally `var/app.db` in Part-DB root folder. +Copy the backed up `app.db` into the database folder normally `var/app.db` in Part-DB root folder. #### MySQL / MariaDB diff --git a/docs/usage/console_commands.md b/docs/usage/console_commands.md index 173f7b78..bc9bb013 100644 --- a/docs/usage/console_commands.md +++ b/docs/usage/console_commands.md @@ -8,7 +8,7 @@ parent: Usage Part-DB provides some console commands to display various information or perform some tasks. The commands are invoked from the main directory of Part-DB with the command `php bin/console [command]` in the context -of the database user (so usually the webserver user), so you maybe have to use `sudo` or `su` to execute the commands: +of the web server user (so usually the webserver user), so you may have to use `sudo` or `su` to execute the commands: ```bash sudo -u www-data php bin/console [command] @@ -17,8 +17,8 @@ sudo -u www-data php bin/console [command] You can get help for every command with the parameter `--help`. See `php bin/console` for a list of all available commands. -If you are running Part-DB in a docker container, you must either execute the commands from a shell inside a container, -or use the `docker exec` command to execute the command directly inside the container. For example if you docker container +If you are running Part-DB in a Docker container, you must either execute the commands from a shell inside the container, +or use the `docker exec` command to execute the command directly inside the container. For example, if your Docker container is named `partdb`, you can execute the command `php bin/console cache:clear` with the following command: ```bash @@ -50,6 +50,14 @@ docker exec --user=www-data partdb php bin/console cache:clear * `php bin/console partdb:currencies:update-exchange-rates`: Update the exchange rates of all currencies from the internet +## Update Manager commands + +{: .note } +> The Update Manager is an experimental feature. See the [Update Manager documentation](update_manager.md) for details. + +* `php bin/console partdb:update`: Check for and perform updates to Part-DB. Use `--check` to only check for updates without installing. +* `php bin/console partdb:maintenance-mode`: Enable, disable, or check the status of maintenance mode. Use `--enable`, `--disable`, or `--status`. + ## Installation/Maintenance commands * `php bin/console partdb:backup`: Backup the database and the attachments @@ -61,13 +69,14 @@ docker exec --user=www-data partdb php bin/console cache:clear * `partdb:attachments:clean-unused`: Remove all attachments which are not used by any database entry (e.g. orphaned attachments) * `partdb:cache:clear`: Clears all caches, so the next page load will be slower, but the cache will be rebuilt. This can - maybe fix some issues, when the cache were corrupted. This command is also needed after changing things in + maybe fix some issues when the cache was corrupted. This command is also needed after changing things in the `parameters.yaml` file or upgrading Part-DB. * `partdb:migrations:import-partkeepr`: Imports a mysqldump XML dump of a PartKeepr database into Part-DB. This is only needed for users, which want to migrate from PartKeepr to Part-DB. *All existing data in the Part-DB database is deleted!* * `settings:migrate-env-to-settings`: Migrate configuration from environment variables to the settings interface. The value of the environment variable is copied to the settings database, so the environment variable can be removed afterwards without losing the configuration. +* `partdb:migrations:convert-db-platform`: Convert the database platform (e.g. from SQLite to MySQL/MariaDB or PostgreSQL, or vice versa). ## Database commands @@ -76,6 +85,9 @@ The value of the environment variable is copied to the settings database, so the ## Attachment commands -* `php bin/console partdb:attachments:download`: Download all attachments, which are not already downloaded, to the - local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote and - also makes pictures thumbnails available for the frontend for them +* `php bin/console partdb:attachments:download`: Download all attachments that are not already downloaded to the + local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote, and + also makes picture thumbnails available for the frontend for them. + +## EDA integration commands +* `php bin/console partdb:kicad:populate`: Populate KiCad footprint paths and symbol paths for footprints and categories based on their names. Use `--dry-run` to preview changes without applying them, and `--list` to list current values. See the [EDA integration documentation](eda_integration.md) for more details. diff --git a/docs/usage/eda_integration.md b/docs/usage/eda_integration.md index 9444e55f..92b1244d 100644 --- a/docs/usage/eda_integration.md +++ b/docs/usage/eda_integration.md @@ -17,14 +17,24 @@ This also allows to configure available and usable parts and their properties in ## KiCad Setup {: .important } -> Part-DB uses the HTTP library feature of KiCad, which is experimental and not part of the stable KiCad 7 releases. If you want to use this feature, you need to install a KiCad nightly build (7.99 version). This feature will most likely also be part of KiCad 8. +> Part-DB uses the HTTP library feature of KiCad, which was experimental in earlier versions. If you want to use this feature, you need to install KiCad 8 or newer. -Part-DB should be accessible from the PCs with KiCAD. The URL should be stable (so no dynamically changing IP). -You require a user account in Part-DB, which has permission to access Part-DB API and create API tokens. Every user can have its own account, or you set up a shared read-only account. +Part-DB should be accessible from the PCs with KiCad. The URL should be stable (so no dynamically changing IP). +You require a user account in Part-DB, which has permission to access the Part-DB API and create API tokens. Every user can have their own account, or you set up a shared read-only account. + +{: .warning } +> **HTTPS with Self-Signed Certificates** +> +> KiCad does not trust self-signed SSL/TLS certificates. If your Part-DB instance uses HTTPS with a self-signed certificate, KiCad will fail to connect and show an error like: `API responded with error code: 0: Unknown`. +> +> To resolve this issue, you have the following options: +> - Use HTTP instead of HTTPS for the `root_url` (only recommended for local networks) +> - Use a certificate from a trusted Certificate Authority (CA) like [Let's Encrypt](https://letsencrypt.org/) +> - Add your self-signed certificate to the system's trusted certificate store on the computer running KiCad (the exact steps depend on your operating system) To connect KiCad with Part-DB do the following steps: -1. Create an API token on the user settings page for the KiCAD application and copy/save it, when it is shown. Currently, KiCad can only read Part-DB database, so a token with a read-only scope is enough. +1. Create an API token on the user settings page for the KiCad application and copy/save it when it is shown. Currently, KiCad can only read the Part-DB database, so a token with a read-only scope is enough. 2. Add some EDA metadata to parts, categories, or footprints. Only parts with usable info will show up in KiCad. See below for more info. 3. Create a file `partd.kicad_httplib` (or similar, only the extension is important) with the following content: ``` @@ -54,18 +64,19 @@ Part-DB doesn't save any concrete footprints or symbols for the part. Instead, P You can define this on a per-part basis using the KiCad symbol and KiCad footprint field in the EDA tab of the part editor. Or you can define it at a category (symbol) or footprint level, to assign this value to all parts with this category and footprint. -For example, to configure the values for a BC547 transistor you would put `Transistor_BJT:BC547` on the parts Kicad symbol to give it the right schematic symbol in EEschema and `Package_TO_SOT_THT:TO-92` to give it the right footprint in PcbNew. +For example, to configure the values for a BC547 transistor you would put `Transistor_BJT:BC547` in the part's KiCad symbol field to give it the right schematic symbol in Eeschema and `Package_TO_SOT_THT:TO-92` to give it the right footprint in Pcbnew. If you type in a character, you will get an autocomplete list of all symbols and footprints available in the KiCad standard library. You can also input your own value. +If you want to keep custom suggestions across updates, open the server settings page and use the "Autocomplete settings" page. There you can edit `public/kicad/footprints_custom.txt` and `public/kicad/symbols_custom.txt` and enable the "Use custom autocomplete lists" option to use those files instead of the autogenerated defaults. ### Parts and category visibility -Only parts and their categories, on which there is any kind of EDA metadata are defined show up in KiCad. So if you want to see parts in KiCad, +Only parts and their categories on which there is any kind of EDA metadata defined show up in KiCad. So if you want to see parts in KiCad, you need to define at least a symbol, footprint, reference prefix, or value on a part, category or footprint. You can use the "Force visibility" checkbox on a part or category to override this behavior and force parts to be visible or hidden in KiCad. -*Please note that KiCad caches the library categories. So if you change something, which would change the visible categories in KiCad, you have to reload EEschema to see the changes.* +*Please note that KiCad caches the library categories. So if you change something that would change the visible categories in KiCad, you have to reload Eeschema to see the changes.* ### Category depth in KiCad @@ -77,3 +88,31 @@ To show more levels of categories, you can set this value to a higher number. If you set this value to -1, all parts are shown inside a single category in KiCad, without any subcategories. You can view the "real" category path of a part in the part details dialog in KiCad. + +### Kicad:populate command + +Part-DB also provides a command that attempts to automatically populate the KiCad symbol and footprint fields based on the part's category and footprint names. +This is especially useful if you have a large database and want to quickly assign symbols and footprints to parts without doing it manually. + +For this run `bin/console partdb:kicad:populate --dry-run` in the terminal, it will show you a list of suggestions for mappings for your existing categories and footprints. +It uses names and alternative names, when the primary name doesn't match, to find the right mapping. +If you are happy with the suggestions, you can run the command without the `--dry-run` option to apply the changes to your database. By default, only empty values are updated, but you can use the `--force` option to overwrite existing values as well. + +It uses the mapping under `assets/commands/kicad_populate_default_mappings.json` by default, but you can extend/override it by providing your own mapping file +with the `--mapping-file` option. +The mapping file is a JSON file with the following structure, where the key is the name of the footprint or category, and the value is the corresponding KiCad library path: +```json +{ + "footprints": { + "MyCustomPackage": "MyLibrary:MyFootprint", + "0805": "Capacitor_SMD:C_0805_2012Metric" + }, + "categories": { + "Sensor": "Sensor:Sensor_Temperature", + "MCU": "MCU_Microchip:PIC16F877A" + } +} +``` +Its okay if the file contains just one of the `footprints` or `categories` keys, so you can choose to only provide mappings for one of them if you want. + +It is recommended to take a backup of your database before running this command. diff --git a/docs/usage/getting_started.md b/docs/usage/getting_started.md index 4b9a809a..8772130c 100644 --- a/docs/usage/getting_started.md +++ b/docs/usage/getting_started.md @@ -6,7 +6,7 @@ nav_order: 4 # Getting started -After Part-DB you should begin with customizing the settings, and setting up the basic structures. +After installing Part-DB, you should begin with customizing the settings and setting up the basic structures. Before starting, it's useful to read a bit about the [concepts of Part-DB]({% link concepts.md %}). 1. TOC @@ -14,8 +14,8 @@ Before starting, it's useful to read a bit about the [concepts of Part-DB]({% li ## Customize system settings -Before starting creating datastructures, you should check the system settings to ensure that they fit your needs. -After login as an administrator, you can find the settings in the sidebar under `Tools -> System -> Settings`. +Before starting creating data structures, you should check the system settings to ensure that they fit your needs. +After logging in as an administrator, you can find the settings in the sidebar under `Tools -> System -> Settings`. ![image]({% link assets/getting_started/system_settings.png %}) Here you can change various settings, like the name of your Part-DB instance (which is shown in the title bar of the @@ -35,9 +35,9 @@ the navigation bar drop-down with the user symbol). ![image]({% link assets/getting_started/change_password.png %}) -There you can also find the option, to set up Two-Factor Authentication methods like Google Authenticator. Using this is +There you can also find the option to set up Two-Factor Authentication methods like Google Authenticator. Using this is highly recommended (especially if you have admin permissions) to increase the security of your account. (Two-factor authentication -even can be enforced for all members of a user group) +can even be enforced for all members of a user group) In the user settings panel, you can change account info like your username, your first and last name (which will be shown alongside your username to identify you better), department information, and your email address. The email address @@ -64,7 +64,7 @@ $E=mc^2$) or `$$` (like `$$E=mc^2$$`) which will be rendered as a block, like so When logged in as administrator, you can open the users menu in the `Tools` section of the sidebar under `System -> Users`. On this page you can create new users, change their passwords and settings, and change their permissions. -For each user who should use Part-DB you should set up their own account so that tracking of what user did works +For each user who should use Part-DB, you should set up their own account so that tracking of what each user did works properly. ![image]({% link assets/getting_started/user_admin.png %}) @@ -207,7 +207,7 @@ You have to enter at least a name for the part and choose a category for it, the However, it is recommended to fill out as much information as possible, as this will make it easier to find the part later. -You can choose from your created datastructures to add manufacturer information, supplier information, etc. to the part. -You can also create new datastructures on the fly, if you want to add additional information to the part, by typing the -name of the new datastructure in the field and select the "New ..." option in the dropdown menu. See [tips]({% link +You can choose from your created data structures to add manufacturer information, supplier information, etc. to the part. +You can also create new data structures on the fly if you want to add additional information to the part, by typing the +name of the new data structure in the field and selecting the "New ..." option in the dropdown menu. See [tips]({% link usage/tips_tricks.md %}) for more information. diff --git a/docs/usage/import_export.md b/docs/usage/import_export.md index 136624e2..f4d8d91c 100644 --- a/docs/usage/import_export.md +++ b/docs/usage/import_export.md @@ -20,7 +20,7 @@ Part-DB. Data can also be exported from Part-DB into various formats. > individually in the permissions settings. If you want to import data from PartKeepr you might want to look into the [PartKeepr migration guide]({% link -upgrade/upgrade_legacy.md %}). +partkeepr_migration.md %}). ### Import parts @@ -47,9 +47,9 @@ You can upload the file that should be imported here and choose various options the import file (or the export will error, if no category is specified). * **Mark all imported parts as "Needs review"**: If this is selected, all imported parts will be marked as "Needs review" after the import. This can be useful if you want to review all imported parts before using them. -* **Create unknown data structures**: If this is selected Part-DB will create new data structures (like categories, - manufacturers, etc.) if no data structure(s) with the same name and path already exists. If this is not selected, only - existing data structures will be used and if no matching data strucure is found, the imported parts field will be empty. +* **Create unknown data structures**: If this is selected, Part-DB will create new data structures (like categories, + manufacturers, etc.) if no data structure(s) with the same name and path already exist. If this is not selected, only + existing data structures will be used, and if no matching data structure is found, the imported parts field will be empty. * **Path delimiter**: Part-DB allows you to create/select nested data structures (like categories, manufacturers, etc.) by using a path (e.g. `Category 1->Category 1.1`, which will select/create the `Category 1.1` whose parent is `Category 1`). This path is separated by the path delimiter. If you want to use a different path delimiter than the diff --git a/docs/usage/information_provider_system.md b/docs/usage/information_provider_system.md index bc6fe76e..4b5e2b22 100644 --- a/docs/usage/information_provider_system.md +++ b/docs/usage/information_provider_system.md @@ -75,10 +75,19 @@ the parts you want to update. In the bulk actions dropdown select "Bulk info pro You will be redirected to a page, where you can select how part fields should be mapped to info provider fields, and the results will be shown. +## Browser plugin +There is a browser plugin available for [Chrome](https://chromewebstore.google.com/detail/part-db-page-submitter/bckkfkpidiiibmjdhjakleoagjmepioi) and [Firefox](https://addons.mozilla.org/de/firefox/addon/part-db-page-submitter/) +that allows to submit a website from your browser with one click to Part-DB, which then utilizes the Generic Web URL or the AI Web Provider to extract the part information from the page and pre-fill the part creation form. +The advantage is that it also works for pages behind logins, CAPTCHAs, or bot-blocking sites, as the plugin sends the already loaded page HTML to Part-DB. +The plugin is open source and available on [GitHub](https://github.com/Part-DB/browser-plugin). + +To use it install it in your browser, enable one or more of the web page providers in Part-DB and allow the plugin support +in Part-DB settings. After that you can submit any product page to Part-DB with one click and the part creation form will be pre-filled with the information from the page. + ## Data providers The system tries to be as flexible as possible, so many different information sources can be used. -Each information source is called am "info provider" and handles the communication with the external source. +Each information source is called an "info provider" and handles the communication with the external source. The providers are just a driver that handles the communication with the different external sources and converts them into a common format Part-DB understands. That way it is pretty easy to create new providers as they just need to do very little work. @@ -96,6 +105,34 @@ The following providers are currently available and shipped with Part-DB: (All trademarks are property of their respective owners. Part-DB is not affiliated with any of the companies.) +### Generic Web URL Provider +The Generic Web URL Provider can extract part information from any webpage that contains structured data in the form of +[Schema.org](https://schema.org/) format. Many e-commerce websites use this format to provide detailed product information +for search engines and other services. Therefore it allows Part-DB to retrieve rudimentary part information (like name, image and price) +from a wide range of websites without the need for a dedicated API integration. +To use the Generic Web URL Provider, simply enable it in the information provider settings. No additional configuration +is required. Afterwards you can enter any product URL in the search field, and Part-DB will attempt to extract the relevant part information +from the webpage. + +Please note that if this provider is enabled, Part-DB will make HTTP requests to external websites to fetch product data, which +may have privacy and security implications. + +Following env configuration options are available: +* `PROVIDER_GENERIC_WEB_ENABLED`: Set this to `1` to enable the Generic Web URL Provider (optional, default: `0`) + +### AI Web Extractor +The AI web extractor provider can extract part information from any webpage using AI-based techniques. It is designed to handle unstructured data and can extract relevant information even from websites that do not use structured data formats like Schema.org. +This provider can be particularly useful for extracting information from websites that have complex layouts or do not follow standard e-commerce practices. +It also potentially extracts more detailed information than the Generic Web URL Provider, as it is not limited to the fields defined in the Schema.org format. + +To use the AI Web Extractor, you need to setup an AI platform, in the AI settings tab, and chose a model, which support structured output. +For many use cases a small and cheap model like `google/gemini-2.5-flash-lite` will be sufficient, coming down to costs like 0.001$ per request. +For more complex websites, or if you wanna use the LLM for translation purposes too, you should consider a more powerful model. + +You can add some additional instructions for the model, which gets added to the system prompt, to tweak the output of the model. + +The provider will download the HTML of the given URL, convert it to markdown and send it to the LLM toghether with structured data extracted from the webpage via conventional methods. + ### Octopart The Octopart provider uses the [Octopart / Nexar API](https://nexar.com/api) to search for parts and get information. @@ -157,7 +194,7 @@ again, to establish a new connection. ### TME -The TME provider uses the API of [TME](https://www.tme.eu/) to search for parts and getting shopping information from +The TME provider uses the API of [TME](https://www.tme.eu/) to search for parts and get shopping information from them. To use it you have to create an account at TME and get an API key on the [TME API page](https://developers.tme.eu/en/). You have to generate a new anonymous key there and enter the key and secret in the Part-DB env configuration (see @@ -176,10 +213,10 @@ The following env configuration options are available: ### Farnell / Element14 / Newark -The Farnell provider uses the [Farnell API](https://partner.element14.com/) to search for parts and getting shopping +The Farnell provider uses the [Farnell API](https://partner.element14.com/) to search for parts and get shopping information from [Farnell](https://www.farnell.com/). You have to create an account at Farnell and get an API key on the [Farnell API page](https://partner.element14.com/). -Register a new application there (settings does not matter, as long as you select the "Product Search API") and you will +Register a new application there (settings do not matter, as long as you select the "Product Search API") and you will get an API key. The following env configuration options are available: @@ -191,17 +228,13 @@ The following env configuration options are available: ### Mouser -The Mouser provider uses the [Mouser API](https://www.mouser.de/api-home/) to search for parts and getting shopping +The Mouser provider uses the [Mouser API](https://www.mouser.de/api-home/) to search for parts and get shopping information from [Mouser](https://www.mouser.com/). You have to create an account at Mouser and register for an API key for the Search API on the [Mouser API page](https://www.mouser.de/api-home/). You will receive an API token, which you have to put in the Part-DB env configuration (see below): At the registration you choose a country, language, and currency in which you want to get the results. -*Attention*: Currently (January 2024) the mouser API seems to be somewhat broken, in the way that it does not return any -information about datasheets and part specifications. Therefore Part-DB can not retrieve them, even if they are shown -at the mouser page. See [issue #503](https://github.com/Part-DB/Part-DB-server/issues/503) for more info. - Following env configuration options are available: * `PROVIDER_MOUSER_KEY`: The API key you got from Mouser (mandatory) @@ -217,7 +250,7 @@ Following env configuration options are available: webshop uses an internal JSON based API to render the page. Part-DB can use this inofficial API to get part information from LCSC. -**Please note, that the use of this internal API is not intended or endorsed by LCS and it could break at any time. So use it at your own risk.** +**Please note that the use of this internal API is not intended or endorsed by LCSC and it could break at any time. So use it at your own risk.** An API key is not required, it is enough to enable the provider using the following env configuration options: @@ -226,7 +259,7 @@ An API key is not required, it is enough to enable the provider using the follow ### OEMsecrets -The oemsecrets provider uses the [oemsecrets API](https://www.oemsecrets.com/) to search for parts and getting shopping +The oemsecrets provider uses the [oemsecrets API](https://www.oemsecrets.com/) to search for parts and get shopping information from them. Similar to octopart it aggregates offers from different distributors. You can apply for a free API key on the [oemsecrets API page](https://www.oemsecrets.com/api/) and put the key you get @@ -264,7 +297,45 @@ This is not an official API and could break at any time. So use it at your own r The following env configuration options are available: * `PROVIDER_POLLIN_ENABLED`: Set this to `1` to enable the Pollin provider -### Custom provider +### Buerklin + +The Buerklin provider uses the [Buerklin API](https://www.buerklin.com/en/services/eprocurement/) to search for parts and get information. +To use it you have to request access to the API. +You will get an e-mail with the client ID and client secret, which you have to put in the Part-DB configuration (see below). + +Please note that the Buerklin API is limited to 100 requests/minute per IP address and +access to the Authentication server is limited to 10 requests/minute per IP address + +The following env configuration options are available: + +* `PROVIDER_BUERKLIN_CLIENT_ID`: The client ID you got from Buerklin (mandatory) +* `PROVIDER_BUERKLIN_SECRET`: The client secret you got from Buerklin (mandatory) +* `PROVIDER_BUERKLIN_USERNAME`: The username you got from Buerklin (mandatory) +* `PROVIDER_BUERKLIN_PASSWORD`: The password you got from Buerklin (mandatory) +* `PROVIDER_BUERKLIN_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`). +* `PROVIDER_BUERKLIN_LANGUAGE`: The language you want to get the descriptions in. Possible values: `de` = German, `en` = English. (optional, default: `en`) + +### Conrad + +The conrad provider the [Conrad API](https://developer.conrad.com/) to search for parts and retried their information. +To use it you have to request access to the API, however it seems currently your mail address needs to be allowlisted before you can register for an account. +The conrad webpages uses the API key in the requests, so you might be able to extract a working API key by listening to browser requests. +That method is not officially supported nor encouraged by Part-DB, and might break at any moment. + +The following env configuration options are available: +* `PROVIDER_CONRAD_API_KEY`: The API key you got from Conrad (mandatory) + +### Canopy / Amazon +The Canopy provider uses the [Canopy API](https://www.canopyapi.co/) to search for parts and get shopping information from Amazon. +Canopy is a third-party service that provides access to Amazon product data through their API. Their trial plan offers 100 requests per month for free, +and they also offer paid plans with higher limits. To use the Canopy provider, you need to create an account on the Canopy website and obtain an API key. +Once you have the API key, you can configure the Canopy provider in Part-DB using the web UI or environment variables: + +* `PROVIDER_CANOPY_API_KEY`: The API key you got from Canopy (mandatory) + + + +### Custom providers To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long as it is a valid Symfony service, it will be automatically loaded and can be used. diff --git a/docs/usage/keybindings.md b/docs/usage/keybindings.md index 771d7684..f4b1980e 100644 --- a/docs/usage/keybindings.md +++ b/docs/usage/keybindings.md @@ -8,6 +8,21 @@ parent: Usage This page lists all the keybindings of Part-DB. Currently, there are only the special character keybindings. +## Disabling keybindings + +If you want to disable the special character keybindings (for example, because they conflict with your keyboard layout or system shortcuts), you can do so in two ways: + +### Via the System Settings UI (recommended) + +1. Navigate to **System Settings** (Tools → System Settings) +2. Go to **Behavior** → **Keybindings** +3. Uncheck **Enable special character keybindings** +4. Save the settings + +### Via Environment Variable + +Alternatively, you can set the environment variable `KEYBINDINGS_SPECIAL_CHARS_ENABLED=0` in your `.env.local` file or your server environment configuration. + ## Special characters Using the keybindings below (Alt + key) you can insert special characters into the text fields of Part-DB. This works on diff --git a/docs/usage/labels.md b/docs/usage/labels.md index e84f4d7f..c804cebb 100644 --- a/docs/usage/labels.md +++ b/docs/usage/labels.md @@ -6,10 +6,10 @@ parent: Usage # Labels -Part-DB support the generation and printing of labels for parts, part lots and storage locations. +Part-DB supports the generation and printing of labels for parts, part lots and storage locations. You can use the "Tools -> Label generator" menu entry to create labels or click the label generation link on the part. -You can define label templates by creating Label profiles. This way you can create many similar-looking labels with for +You can define label templates by creating label profiles. This way you can create many similar-looking labels for many parts. The content of the labels is defined by the template's content field. You can use the WYSIWYG editor to create and style @@ -91,18 +91,20 @@ in [official documentation](https://twig.symfony.com/doc/3.x/). Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment, -where only certain operations are allowed. Only read access to entities is allowed. However as it circumvents Part-DB normal permission system, +where only certain operations are allowed. Only read access to entities is allowed. However, as it circumvents Part-DB normal permission system, the twig mode is only available to users with the "Twig mode" permission. +It is useful to use the HTML embed feature of the editor, to have a block where you can write the twig code without worrying about the WYSIWYG editor messing with your code. + The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` ( or `{% raw %}{{ variable.property }}{% endraw %}`): | Variable name | Description | |--------------------------------------------|--------------------------------------------------------------------------------------| -| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. | +| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. | | `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in | | `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). | -| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated | +| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated ) | | `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels | | `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm | | `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm | @@ -236,12 +238,18 @@ certain data: #### Functions -| Function name | Description | -|----------------------------------------------|-----------------------------------------------------------------------------------------------| -| `placeholder(placeholder, element)` | Get the value of a placeholder of an element | -| `entity_type(element)` | Get the type of an entity as string | -| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) | -| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. | +| Function name | Description | +|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `placeholder(placeholder, element)` | Get the value of a placeholder of an element | +| `entity_type(element)` | Get the type of an entity as string | +| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) | +| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. | +| `associated_parts(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. Only the directly associated parts are returned | +| `associated_parts_r(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. including all sub-entities recursively (e.g. sub-locations) | +| `associated_parts_count(element)` | Get the count of associated parts of an element like a storagelocation, footprint, excluding sub-entities | +| `associated_parts_count_r(element)` | Get the count of associated parts of an element like a storagelocation, footprint, including all sub-entities recursively (e.g. sub-locations) | +| `type_label(element)` | Get the name of the type of an element (e.g. "Part", "Storage location", etc.) | +| `type_label_p(element)` | Get the name of the type of an element in plural form (e.g. "Parts", "Storage locations", etc.) | ### Filters @@ -285,5 +293,5 @@ If you want to use a different (more beautiful) font, you can use the [custom fo feature. There is the [Noto](https://www.google.com/get/noto/) font family from Google, which supports a lot of languages and is available in different styles (regular, bold, italic, bold-italic). -For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese, -and Korean characters. \ No newline at end of file +For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese, +and Korean characters. diff --git a/docs/usage/scanner.md b/docs/usage/scanner.md new file mode 100644 index 00000000..47b3feff --- /dev/null +++ b/docs/usage/scanner.md @@ -0,0 +1,51 @@ +--- +title: Barcode Scanner +layout: default +parent: Usage +--- + +# Barcode scanner + +When the user has the correct permission there will be a barcode scanner button in the navbar. +On this page you can either input a barcode code by hand, use an external barcode scanner, or use your devices camera to +scan a barcode. + +In info mode (when the "Info" toggle is enabled) you can scan a barcode and Part-DB will parse it and show information +about it. + +Without info mode, the barcode will directly redirect you to the corresponding page. + +### Barcode matching + +When you scan a barcode, Part-DB will try to match it to an existing part, part lot or storage location first. +For Part-DB generated barcodes, it will use the internal ID of a part. Alternatively you can also scan a barcode that contains the part's IPN. + +You can set a GTIN/EAN code in the part properties and Part-DB will open the part page when you scan the corresponding GTIN/EAN barcode. + +On a part lot you can under "Advanced" set a user barcode, that will redirect you to the part lot page when scanned. This allows to reuse +arbitrary existing barcodes that already exist on the part lots (for example, from the manufacturer) and link them to the part lot in Part-DB. + +Part-DB can also parse various distributor barcodes (for example from Digikey and Mouser) and will try to redirect you to the corresponding +part page based on the distributor part number in the barcode. + +### Part creation from barcodes +For certain barcodes Part-DB can automatically create a new part, when it cannot find a matching part. +Part-DB will try to retrieve the part information from an information provider and redirects you to the part creation page +with the retrieved information pre-filled. + +## Using an external barcode scanner + +Part-DB supports the use of external barcode scanners that emulate keyboard input. To use a barcode scanner with Part-DB, +simply connect the scanner to your computer and scan a barcode while the cursor is in a text field in Part-DB. +The scanned barcode will be entered into the text field as if you had typed it on the keyboard. + +In scanner fields, it will also try to insert special non-printable characters the scanner send via Alt + key combinations. +This is required for EIGP114 datamatrix codes. + +### Automatically redirect on barcode scanning + +If you configure your barcode scanner to send a (Start of heading, 0x01) non-printable character at the beginning +of the scanned barcode, Part-DB will automatically scan the barcode that comes afterward (and is ended with an enter key) +and redirects you to the corresponding page. +This allows you to quickly scan a barcode from anywhere in Part-DB without the need to first open the scanner page. +If an input field is focused, the barcode will be entered into the field as usual and no redirection will happen. diff --git a/docs/usage/tips_tricks.md b/docs/usage/tips_tricks.md index 6eda718d..cab05620 100644 --- a/docs/usage/tips_tricks.md +++ b/docs/usage/tips_tricks.md @@ -65,7 +65,7 @@ $$E=mc^2$$ ## Update currency exchange rates automatically Part-DB can update the currency exchange rates of all defined currencies programmatically -by calling the `php bin/console partdb:currencies:update-exchange-rates`. +by calling `php bin/console partdb:currencies:update-exchange-rates`. If you call this command regularly (e.g. with a cronjob), you can keep the exchange rates up-to-date. @@ -88,9 +88,9 @@ the user as "owner" of a part lot. This way, only he is allowed to add or remove ## Update notifications -Part-DB can show you a notification that there is a newer version than currently installed available. The notification +Part-DB can show you a notification that there is a newer version than currently installed. The notification will be shown on the homepage and the server info page. -It is only be shown to users which has the `Show available Part-DB updates` permission. +It is only shown to users which have the `Show available Part-DB updates` permission. For the notification to work, Part-DB queries the GitHub API every 2 days to check for new releases. No data is sent to GitHub besides the metadata required for the connection (so the public IP address of your computer running Part-DB). @@ -98,6 +98,6 @@ If you don't want Part-DB to query the GitHub API, or if your server can not rea update notifications by setting the `CHECK_FOR_UPDATES` option to `false`. ## Internet access via proxy -If you server running Part-DB does not have direct access to the internet, but has to use a proxy server, you can configure +If your server running Part-DB does not have direct access to the internet, but has to use a proxy server, you can configure the proxy settings in the `.env.local` file (or docker env config). You can set the `HTTP_PROXY` and `HTTPS_PROXY` environment variables to the URL of your proxy server. If your proxy server requires authentication, you can include the username and password in the URL. diff --git a/docs/usage/update_manager.md b/docs/usage/update_manager.md new file mode 100644 index 00000000..43fe2c94 --- /dev/null +++ b/docs/usage/update_manager.md @@ -0,0 +1,170 @@ +--- +title: Update Manager +layout: default +parent: Usage +--- + +# Update Manager (Experimental) + +{: .warning } +> The Update Manager is currently an **experimental feature**. It is disabled by default while user experience data is being gathered. Use with caution and always ensure you have proper backups before updating. + +Part-DB includes an Update Manager that can automatically update Git-based installations to newer versions. The Update Manager provides both a web interface and CLI commands for managing updates, backups, and maintenance mode. + +## Supported Installation Types + +The Update Manager currently supports automatic updates only for **Git clone** installations. Other installation types show manual update instructions: + +| Installation Type | Auto-Update | Instructions | +|-------------------|-------------|--------------| +| Git Clone | Yes | Automatic via CLI or Web UI | +| Docker | No | Pull new image: `docker-compose pull && docker-compose up -d` | +| ZIP Release | No | Download and extract new release manually | + +## Enabling the Update Manager + +By default, web-based updates and backup restore are **disabled** for security reasons. To enable them, add these settings to your `.env.local` file: + +```bash +# Enable web-based updates (default: disabled) +DISABLE_WEB_UPDATES=0 + +# Enable backup restore via web interface (default: disabled) +DISABLE_BACKUP_RESTORE=0 +``` + +{: .note } +> Even with web updates disabled, you can still use the CLI commands to perform updates. + +## CLI Commands + +### Update Command + +Check for updates or perform an update: + +```bash +# Check for available updates +php bin/console partdb:update --check + +# Update to the latest version +php bin/console partdb:update + +# Update to a specific version +php bin/console partdb:update v2.6.0 + +# Update without creating a backup first +php bin/console partdb:update --no-backup + +# Force update without confirmation prompt +php bin/console partdb:update --force +``` + +### Maintenance Mode Command + +Manually enable or disable maintenance mode: + +```bash +# Enable maintenance mode with default message +php bin/console partdb:maintenance-mode --enable + +# Enable with custom message +php bin/console partdb:maintenance-mode --enable "System maintenance until 6 PM" +php bin/console partdb:maintenance-mode --enable --message="Updating to v2.6.0" + +# Disable maintenance mode +php bin/console partdb:maintenance-mode --disable + +# Check current status +php bin/console partdb:maintenance-mode --status +``` + +## Web Interface + +When web updates are enabled, the Update Manager is accessible at **System > Update Manager** (URL: `/system/update-manager`). + +The web interface shows: +- Current version and installation type +- Available updates with release notes +- Precondition validation (Git, Composer, Yarn, permissions) +- Update history and logs +- Backup management + +### Required Permissions + +Users need the following permissions to access the Update Manager: + +| Permission | Description | +|------------|-------------| +| `@system.show_updates` | View update status and available versions | +| `@system.manage_updates` | Perform updates and restore backups | + +## Update Process + +When an update is performed, the following steps are executed: + +1. **Lock** - Acquire exclusive lock to prevent concurrent updates +2. **Maintenance Mode** - Enable maintenance mode to block user access +3. **Rollback Tag** - Create a Git tag for potential rollback +4. **Backup** - Create a full backup (optional but recommended) +5. **Git Fetch** - Fetch latest changes from origin +6. **Git Checkout** - Checkout the target version +7. **Composer Install** - Install/update PHP dependencies +8. **Yarn Install** - Install frontend dependencies +9. **Yarn Build** - Compile frontend assets +10. **Database Migrations** - Run any new migrations +11. **Cache Clear** - Clear the application cache +12. **Cache Warmup** - Rebuild the cache +13. **Maintenance Off** - Disable maintenance mode +14. **Unlock** - Release the update lock + +If any step fails, the system automatically attempts to rollback to the previous version. + +## Backup Management + +The Update Manager automatically creates backups before updates. These backups are stored in `var/backups/` and include: + +- Database dump (SQL file or SQLite database) +- Configuration files (`.env.local`, `parameters.yaml`, `banner.md`) +- Attachment files (`uploads/`, `public/media/`) + +### Restoring from Backup + +{: .warning } +> Backup restore is a destructive operation that will overwrite your current database. Only use this if you need to recover from a failed update. + +If web restore is enabled (`DISABLE_BACKUP_RESTORE=0`), you can restore backups from the web interface. The restore process: + +1. Enables maintenance mode +2. Extracts the backup +3. Restores the database +4. Optionally restores config and attachments +5. Clears and warms up the cache +6. Disables maintenance mode + +## Troubleshooting + +### Precondition Errors + +Before updating, the system validates: + +- **Git available**: Git must be installed and in PATH +- **No local changes**: Uncommitted changes must be committed or stashed +- **Composer available**: Composer must be installed and in PATH +- **Yarn available**: Yarn must be installed and in PATH +- **Write permissions**: `var/`, `vendor/`, and `public/` must be writable +- **Not already locked**: No other update can be in progress + +### Stale Lock + +If an update was interrupted and the lock file remains, it will automatically be removed after 1 hour. You can also manually delete `var/update.lock`. + +### Viewing Update Logs + +Update logs are stored in `var/log/updates/` and can be viewed from the web interface or directly on the server. + +## Security Considerations + +- **Disable web updates in production** unless you specifically need them +- The Update Manager requires shell access to run Git, Composer, and Yarn +- Backup files may contain sensitive data (database, config) - secure the `var/backups/` directory +- Consider running updates during maintenance windows with low user activity diff --git a/makefile b/makefile deleted file mode 100644 index bc4d0bf3..00000000 --- a/makefile +++ /dev/null @@ -1,91 +0,0 @@ -# PartDB Makefile for Test Environment Management - -.PHONY: help deps-install lint format format-check test coverage pre-commit all test-typecheck \ -test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run test-reset \ -section-dev dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset - -# Default target -help: ## Show this help - @awk 'BEGIN {FS = ":.*##"}; /^[a-zA-Z0-9][a-zA-Z0-9_-]+:.*##/ {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) - -# Dependencies -deps-install: ## Install PHP dependencies with unlimited memory - @echo "📦 Installing PHP dependencies..." - COMPOSER_MEMORY_LIMIT=-1 composer install - yarn install - @echo "✅ Dependencies installed" - -# Complete test environment setup -test-setup: test-clean test-db-create test-db-migrate test-fixtures ## Complete test setup (clean, create DB, migrate, fixtures) - @echo "✅ Test environment setup complete!" - -# Clean test environment -test-clean: ## Clean test cache and database files - @echo "🧹 Cleaning test environment..." - rm -rf var/cache/test - rm -f var/app_test.db - @echo "✅ Test environment cleaned" - -# Create test database -test-db-create: ## Create test database (if not exists) - @echo "🗄️ Creating test database..." - -php bin/console doctrine:database:create --if-not-exists --env test || echo "⚠️ Database creation failed (expected for SQLite) - continuing..." - -# Run database migrations for test environment -test-db-migrate: ## Run database migrations for test environment - @echo "🔄 Running database migrations..." - COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env test - -# Clear test cache -test-cache-clear: ## Clear test cache - @echo "🗑️ Clearing test cache..." - rm -rf var/cache/test - @echo "✅ Test cache cleared" - -# Load test fixtures -test-fixtures: ## Load test fixtures - @echo "📦 Loading test fixtures..." - php bin/console partdb:fixtures:load -n --env test - -# Run PHPUnit tests -test-run: ## Run PHPUnit tests - @echo "🧪 Running tests..." - php bin/phpunit - -# Quick test reset (clean + migrate + fixtures, skip DB creation) -test-reset: test-cache-clear test-db-migrate test-fixtures - @echo "✅ Test environment reset complete!" - -test-typecheck: ## Run static analysis (PHPStan) - @echo "🧪 Running type checks..." - COMPOSER_MEMORY_LIMIT=-1 composer phpstan - -# Development helpers -dev-setup: dev-clean dev-db-create dev-db-migrate dev-warmup ## Complete development setup (clean, create DB, migrate, warmup) - @echo "✅ Development environment setup complete!" - -dev-clean: ## Clean development cache and database files - @echo "🧹 Cleaning development environment..." - rm -rf var/cache/dev - rm -f var/app_dev.db - @echo "✅ Development environment cleaned" - -dev-db-create: ## Create development database (if not exists) - @echo "🗄️ Creating development database..." - -php bin/console doctrine:database:create --if-not-exists --env dev || echo "⚠️ Database creation failed (expected for SQLite) - continuing..." - -dev-db-migrate: ## Run database migrations for development environment - @echo "🔄 Running database migrations..." - COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env dev - -dev-cache-clear: ## Clear development cache - @echo "🗑️ Clearing development cache..." - rm -rf var/cache/dev - @echo "✅ Development cache cleared" - -dev-warmup: ## Warm up development cache - @echo "🔥 Warming up development cache..." - COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G bin/console cache:warmup --env dev -n - -dev-reset: dev-cache-clear dev-db-migrate ## Quick development reset (cache clear + migrate) - @echo "✅ Development environment reset complete!" \ No newline at end of file diff --git a/migrations/Version20250321075747.php b/migrations/Version20250321075747.php new file mode 100644 index 00000000..14bcb8a9 --- /dev/null +++ b/migrations/Version20250321075747.php @@ -0,0 +1,605 @@ +addSql(<<<'SQL' + CREATE TABLE part_custom_states ( + id INT AUTO_INCREMENT NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment LONGTEXT NOT NULL, + not_selectable TINYINT(1) NOT NULL, + alternative_names LONGTEXT DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + INDEX IDX_F552745D727ACA70 (parent_id), + INDEX IDX_F552745DEA7100A1 (id_preview_attachment), + INDEX part_custom_state_name (name), + PRIMARY KEY(id) + ) + DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states ADD CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES part_custom_states (id) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states ADD CONSTRAINT FK_F552745DEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON DELETE SET NULL + SQL); + + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL AFTER id_part_unit + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP FOREIGN KEY FK_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_6940A7FEA3ED1215 ON parts + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts DROP id_part_custom_state + SQL); + + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745D727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE part_custom_states DROP FOREIGN KEY FK_F552745DEA7100A1 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE part_custom_states + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE "part_custom_states" ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + name VARCHAR(255) NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names CLOB DEFAULT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT FK_F552745D727ACA70 FOREIGN KEY (parent_id) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_F5AF83CFEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745D727ACA70 ON "part_custom_states" (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX part_custom_state_name ON "part_custom_states" (name) + SQL); + + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE parts + SQL); + + $this->addSql(<<<'SQL' + CREATE TABLE parts ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + id_part_custom_state INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO parts ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint) + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM __temp__parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE __temp__parts + SQL); + + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON parts (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON parts (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__parts AS + SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM "parts" + SQL); + $this->addSql(<<<'SQL' + DROP TABLE "parts" + SQL); + $this->addSql(<<<'SQL' + CREATE TABLE "parts" ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + id_category INTEGER NOT NULL, + id_footprint INTEGER DEFAULT NULL, + id_part_unit INTEGER DEFAULT NULL, + id_manufacturer INTEGER DEFAULT NULL, + order_orderdetails_id INTEGER DEFAULT NULL, + built_project_id INTEGER DEFAULT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + needs_review BOOLEAN NOT NULL, + tags CLOB NOT NULL, + mass DOUBLE PRECISION DEFAULT NULL, + description CLOB NOT NULL, + comment CLOB NOT NULL, + visible BOOLEAN NOT NULL, + favorite BOOLEAN NOT NULL, + minamount DOUBLE PRECISION NOT NULL, + manufacturer_product_url CLOB NOT NULL, + manufacturer_product_number VARCHAR(255) NOT NULL, + manufacturing_status VARCHAR(255) DEFAULT NULL, + order_quantity INTEGER NOT NULL, + manual_order BOOLEAN NOT NULL, + ipn VARCHAR(100) DEFAULT NULL, + provider_reference_provider_key VARCHAR(255) DEFAULT NULL, + provider_reference_provider_id VARCHAR(255) DEFAULT NULL, + provider_reference_provider_url VARCHAR(255) DEFAULT NULL, + provider_reference_last_updated DATETIME DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_value VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + $this->addSql(<<<'SQL' + INSERT INTO "parts" ( + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + ) SELECT + id, + id_preview_attachment, + id_category, + id_footprint, + id_part_unit, + id_manufacturer, + order_orderdetails_id, + built_project_id, + datetime_added, + name, + last_modified, + needs_review, + tags, + mass, + description, + comment, + visible, + favorite, + minamount, + manufacturer_product_url, + manufacturer_product_number, + manufacturing_status, + order_quantity, + manual_order, + ipn, + provider_reference_provider_key, + provider_reference_provider_id, + provider_reference_provider_url, + provider_reference_last_updated, + eda_info_reference_prefix, + eda_info_value, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol, + eda_info_kicad_footprint + FROM __temp__parts + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE __temp__parts + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id) + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_name ON "parts" (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX parts_idx_ipn ON "parts" (ipn) + SQL); + + $this->addSql(<<<'SQL' + DROP TABLE "part_custom_states" + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TABLE "part_custom_states" ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + parent_id INT DEFAULT NULL, + id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id), + name VARCHAR(255) NOT NULL, + comment TEXT NOT NULL, + not_selectable BOOLEAN NOT NULL, + alternative_names TEXT DEFAULT NULL, + last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745D727ACA70 ON "part_custom_states" (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_F552745DEA7100A1 ON "part_custom_states" (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "part_custom_states" + ADD CONSTRAINT FK_F552745D727ACA70 + FOREIGN KEY (parent_id) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "part_custom_states" + ADD CONSTRAINT FK_F552745DEA7100A1 + FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + + + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD id_part_custom_state INT DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX IDX_6940A7FEA3ED1215 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "parts" DROP id_part_custom_state + SQL); + + $this->addSql(<<<'SQL' + ALTER TABLE "part_custom_states" DROP CONSTRAINT FK_F552745D727ACA70 + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE "part_custom_states" DROP CONSTRAINT FK_F552745DEA7100A1 + SQL); + $this->addSql(<<<'SQL' + DROP TABLE "part_custom_states" + SQL); + } +} diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php new file mode 100644 index 00000000..3bae80ab --- /dev/null +++ b/migrations/Version20250325073036.php @@ -0,0 +1,307 @@ +addSql(<<<'SQL' + ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT '' + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE categories DROP part_ipn_prefix + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__categories AS + SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM categories + SQL); + + $this->addSql('DROP TABLE categories'); + + $this->addSql(<<<'SQL' + CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + partname_hint CLOB NOT NULL, + partname_regex CLOB NOT NULL, + part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL, + disable_footprints BOOLEAN NOT NULL, + disable_manufacturers BOOLEAN NOT NULL, + disable_autodatasheets BOOLEAN NOT NULL, + disable_properties BOOLEAN NOT NULL, + default_description CLOB NOT NULL, + default_comment CLOB NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + alternative_names CLOB DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO categories ( + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + ) SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM __temp__categories + SQL); + + $this->addSql('DROP TABLE __temp__categories'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_name ON categories (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_parent_name ON categories (parent_id, name) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__categories AS + SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM categories + SQL); + + $this->addSql('DROP TABLE categories'); + + $this->addSql(<<<'SQL' + CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + partname_hint CLOB NOT NULL, + partname_regex CLOB NOT NULL, + disable_footprints BOOLEAN NOT NULL, + disable_manufacturers BOOLEAN NOT NULL, + disable_autodatasheets BOOLEAN NOT NULL, + disable_properties BOOLEAN NOT NULL, + default_description CLOB NOT NULL, + default_comment CLOB NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + alternative_names CLOB DEFAULT NULL, + eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, + eda_info_invisible BOOLEAN DEFAULT NULL, + eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, + eda_info_exclude_from_board BOOLEAN DEFAULT NULL, + eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, + eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, + CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO categories ( + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + ) SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM __temp__categories + SQL); + + $this->addSql('DROP TABLE __temp__categories'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_name ON categories (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_parent_name ON categories (parent_id, name) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE "categories" DROP part_ipn_prefix + SQL); + } +} diff --git a/migrations/Version20251204215443.php b/migrations/Version20251204215443.php new file mode 100644 index 00000000..3cee0035 --- /dev/null +++ b/migrations/Version20251204215443.php @@ -0,0 +1,156 @@ +addSql('ALTER TABLE attachments CHANGE external_path external_path VARCHAR(2048) DEFAULT NULL'); + $this->addSql('ALTER TABLE manufacturers CHANGE website website VARCHAR(2048) NOT NULL, CHANGE auto_product_url auto_product_url VARCHAR(2048) NOT NULL'); + $this->addSql('ALTER TABLE parts CHANGE provider_reference_provider_url provider_reference_provider_url VARCHAR(2048) DEFAULT NULL'); + $this->addSql('ALTER TABLE suppliers CHANGE website website VARCHAR(2048) NOT NULL, CHANGE auto_product_url auto_product_url VARCHAR(2048) NOT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE `attachments` CHANGE external_path external_path VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE `manufacturers` CHANGE website website VARCHAR(255) NOT NULL, CHANGE auto_product_url auto_product_url VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE `parts` CHANGE provider_reference_provider_url provider_reference_provider_url VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE `suppliers` CHANGE website website VARCHAR(255) NOT NULL, CHANGE auto_product_url auto_product_url VARCHAR(255) NOT NULL'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__attachments AS SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, internal_path, external_path FROM attachments'); + $this->addSql('DROP TABLE attachments'); + $this->addSql('CREATE TABLE attachments (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, type_id INTEGER NOT NULL, original_filename VARCHAR(255) DEFAULT NULL, show_in_table BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, class_name VARCHAR(255) NOT NULL, element_id INTEGER NOT NULL, internal_path VARCHAR(255) DEFAULT NULL, external_path VARCHAR(2048) DEFAULT NULL, CONSTRAINT FK_47C4FAD6C54C8C93 FOREIGN KEY (type_id) REFERENCES attachment_types (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO attachments (id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, internal_path, external_path) SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, internal_path, external_path FROM __temp__attachments'); + $this->addSql('DROP TABLE __temp__attachments'); + $this->addSql('CREATE INDEX attachment_element_idx ON attachments (class_name, element_id)'); + $this->addSql('CREATE INDEX attachment_name_idx ON attachments (name)'); + $this->addSql('CREATE INDEX attachments_idx_class_name_id ON attachments (class_name, id)'); + $this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON attachments (id, element_id, class_name)'); + $this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON attachments (type_id)'); + $this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON attachments (element_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__manufacturers AS SELECT id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM manufacturers'); + $this->addSql('DROP TABLE manufacturers'); + $this->addSql('CREATE TABLE manufacturers (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(2048) NOT NULL, auto_product_url VARCHAR(2048) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_94565B12727ACA70 FOREIGN KEY (parent_id) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_94565B12EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO manufacturers (id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names) SELECT id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM __temp__manufacturers'); + $this->addSql('DROP TABLE __temp__manufacturers'); + $this->addSql('CREATE INDEX IDX_94565B12EA7100A1 ON manufacturers (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_94565B12727ACA70 ON manufacturers (parent_id)'); + $this->addSql('CREATE INDEX manufacturer_name ON manufacturers (name)'); + $this->addSql('CREATE INDEX manufacturer_idx_parent_name ON manufacturers (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM parts'); + $this->addSql('DROP TABLE parts'); + $this->addSql('CREATE TABLE parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, ipn VARCHAR(100) DEFAULT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(2048) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO parts (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)'); + $this->addSql('CREATE INDEX parts_idx_name ON parts (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM suppliers'); + $this->addSql('DROP TABLE suppliers'); + $this->addSql('CREATE TABLE suppliers (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(2048) NOT NULL, auto_product_url VARCHAR(2048) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES suppliers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO suppliers (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM __temp__suppliers'); + $this->addSql('DROP TABLE __temp__suppliers'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON suppliers (default_currency_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON suppliers (parent_id)'); + $this->addSql('CREATE INDEX supplier_idx_name ON suppliers (name)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON suppliers (parent_id, name)'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON suppliers (id_preview_attachment)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__attachments AS SELECT id, name, last_modified, datetime_added, original_filename, internal_path, external_path, show_in_table, type_id, class_name, element_id FROM "attachments"'); + $this->addSql('DROP TABLE "attachments"'); + $this->addSql('CREATE TABLE "attachments" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, original_filename VARCHAR(255) DEFAULT NULL, internal_path VARCHAR(255) DEFAULT NULL, external_path VARCHAR(255) DEFAULT NULL, show_in_table BOOLEAN NOT NULL, type_id INTEGER NOT NULL, class_name VARCHAR(255) NOT NULL, element_id INTEGER NOT NULL, CONSTRAINT FK_47C4FAD6C54C8C93 FOREIGN KEY (type_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "attachments" (id, name, last_modified, datetime_added, original_filename, internal_path, external_path, show_in_table, type_id, class_name, element_id) SELECT id, name, last_modified, datetime_added, original_filename, internal_path, external_path, show_in_table, type_id, class_name, element_id FROM __temp__attachments'); + $this->addSql('DROP TABLE __temp__attachments'); + $this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON "attachments" (type_id)'); + $this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON "attachments" (element_id)'); + $this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON "attachments" (id, element_id, class_name)'); + $this->addSql('CREATE INDEX attachments_idx_class_name_id ON "attachments" (class_name, id)'); + $this->addSql('CREATE INDEX attachment_name_idx ON "attachments" (name)'); + $this->addSql('CREATE INDEX attachment_element_idx ON "attachments" (class_name, element_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__manufacturers AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, parent_id, id_preview_attachment FROM "manufacturers"'); + $this->addSql('DROP TABLE "manufacturers"'); + $this->addSql('CREATE TABLE "manufacturers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_94565B12727ACA70 FOREIGN KEY (parent_id) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_94565B12EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "manufacturers" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, parent_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, parent_id, id_preview_attachment FROM __temp__manufacturers'); + $this->addSql('DROP TABLE __temp__manufacturers'); + $this->addSql('CREATE INDEX IDX_94565B12727ACA70 ON "manufacturers" (parent_id)'); + $this->addSql('CREATE INDEX IDX_94565B12EA7100A1 ON "manufacturers" (id_preview_attachment)'); + $this->addSql('CREATE INDEX manufacturer_name ON "manufacturers" (name)'); + $this->addSql('CREATE INDEX manufacturer_idx_parent_name ON "manufacturers" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM "parts"'); + $this->addSql('DROP TABLE "parts"'); + $this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(255) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "parts" (id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id) SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON "parts" (id_part_custom_state)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment FROM "suppliers"'); + $this->addSql('DROP TABLE "suppliers"'); + $this->addSql('CREATE TABLE "suppliers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "suppliers" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment FROM __temp__suppliers'); + $this->addSql('DROP TABLE __temp__suppliers'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON "suppliers" (parent_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON "suppliers" (default_currency_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON "suppliers" (id_preview_attachment)'); + $this->addSql('CREATE INDEX supplier_idx_name ON "suppliers" (name)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON "suppliers" (parent_id, name)'); + } + + public function postgreSQLUp(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE attachments ALTER external_path TYPE VARCHAR(2048)'); + $this->addSql('ALTER TABLE manufacturers ALTER website TYPE VARCHAR(2048)'); + $this->addSql('ALTER TABLE manufacturers ALTER auto_product_url TYPE VARCHAR(2048)'); + $this->addSql('ALTER TABLE parts ALTER provider_reference_provider_url TYPE VARCHAR(2048)'); + $this->addSql('ALTER TABLE suppliers ALTER website TYPE VARCHAR(2048)'); + $this->addSql('ALTER TABLE suppliers ALTER auto_product_url TYPE VARCHAR(2048)'); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE "attachments" ALTER external_path TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE "manufacturers" ALTER website TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE "manufacturers" ALTER auto_product_url TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE "parts" ALTER provider_reference_provider_url TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE "suppliers" ALTER website TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE "suppliers" ALTER auto_product_url TYPE VARCHAR(255)'); + } +} diff --git a/migrations/Version20260208131116.php b/migrations/Version20260208131116.php new file mode 100644 index 00000000..d05d3e4c --- /dev/null +++ b/migrations/Version20260208131116.php @@ -0,0 +1,129 @@ +addSql('ALTER TABLE attachment_types ADD allowed_targets LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE part_lots ADD last_stocktake_at DATETIME DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD gtin VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)'); + $this->addSql('ALTER TABLE orderdetails ADD prices_includes_vat TINYINT DEFAULT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `attachment_types` DROP allowed_targets'); + $this->addSql('DROP INDEX parts_idx_gtin ON `parts`'); + $this->addSql('ALTER TABLE `parts` DROP gtin'); + $this->addSql('ALTER TABLE part_lots DROP last_stocktake_at'); + $this->addSql('ALTER TABLE `orderdetails` DROP prices_includes_vat'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('ALTER TABLE attachment_types ADD COLUMN allowed_targets CLOB DEFAULT NULL'); + $this->addSql('ALTER TABLE part_lots ADD COLUMN last_stocktake_at DATETIME DEFAULT NULL'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM parts'); + $this->addSql('DROP TABLE parts'); + $this->addSql('CREATE TABLE parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, ipn VARCHAR(100) DEFAULT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(2048) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, gtin VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO parts (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE INDEX parts_idx_name ON parts (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)'); + $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)'); + $this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)'); + $this->addSql('ALTER TABLE orderdetails ADD COLUMN prices_includes_vat BOOLEAN DEFAULT NULL'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__attachment_types AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment FROM "attachment_types"'); + $this->addSql('DROP TABLE "attachment_types"'); + $this->addSql('CREATE TABLE "attachment_types" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, filetype_filter CLOB NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_EFAED719727ACA70 FOREIGN KEY (parent_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EFAED719EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "attachment_types" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment FROM __temp__attachment_types'); + $this->addSql('DROP TABLE __temp__attachment_types'); + $this->addSql('CREATE INDEX IDX_EFAED719727ACA70 ON "attachment_types" (parent_id)'); + $this->addSql('CREATE INDEX IDX_EFAED719EA7100A1 ON "attachment_types" (id_preview_attachment)'); + $this->addSql('CREATE INDEX attachment_types_idx_name ON "attachment_types" (name)'); + $this->addSql('CREATE INDEX attachment_types_idx_parent_name ON "attachment_types" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner FROM part_lots'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO part_lots (id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner) SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner FROM __temp__part_lots'); + $this->addSql('DROP TABLE __temp__part_lots'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM "parts"'); + $this->addSql('DROP TABLE "parts"'); + $this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(2048) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "parts" (id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id) SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON "parts" (id_part_custom_state)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__orderdetails AS SELECT id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier FROM "orderdetails"'); + $this->addSql('DROP TABLE "orderdetails"'); + $this->addSql('CREATE TABLE "orderdetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, supplierpartnr VARCHAR(255) NOT NULL, obsolete BOOLEAN NOT NULL, supplier_product_url CLOB NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, part_id INTEGER NOT NULL, id_supplier INTEGER DEFAULT NULL, CONSTRAINT FK_489AFCDC4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_489AFCDCCBF180EB FOREIGN KEY (id_supplier) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "orderdetails" (id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier) SELECT id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier FROM __temp__orderdetails'); + $this->addSql('DROP TABLE __temp__orderdetails'); + $this->addSql('CREATE INDEX IDX_489AFCDC4CE34BEC ON "orderdetails" (part_id)'); + $this->addSql('CREATE INDEX IDX_489AFCDCCBF180EB ON "orderdetails" (id_supplier)'); + $this->addSql('CREATE INDEX orderdetails_supplier_part_nr ON "orderdetails" (supplierpartnr)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql('ALTER TABLE attachment_types ADD allowed_targets TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE part_lots ADD last_stocktake_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD gtin VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)'); + $this->addSql('ALTER TABLE orderdetails ADD prices_includes_vat BOOLEAN DEFAULT NULL'); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE "attachment_types" DROP allowed_targets'); + $this->addSql('ALTER TABLE part_lots DROP last_stocktake_at'); + $this->addSql('DROP INDEX parts_idx_gtin'); + $this->addSql('ALTER TABLE "parts" DROP gtin'); + $this->addSql('ALTER TABLE "orderdetails" DROP prices_includes_vat'); + } +} diff --git a/migrations/Version20260211000000.php b/migrations/Version20260211000000.php new file mode 100644 index 00000000..33f3db57 --- /dev/null +++ b/migrations/Version20260211000000.php @@ -0,0 +1,52 @@ +addSql('ALTER TABLE parameters ADD eda_visibility TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE `orderdetails` ADD eda_visibility TINYINT(1) DEFAULT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility'); + $this->addSql('ALTER TABLE `orderdetails` DROP COLUMN eda_visibility'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters ADD COLUMN eda_visibility BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE orderdetails ADD COLUMN eda_visibility BOOLEAN DEFAULT NULL'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility'); + $this->addSql('ALTER TABLE orderdetails DROP COLUMN eda_visibility'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters ADD eda_visibility BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE orderdetails ADD eda_visibility BOOLEAN DEFAULT NULL'); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility'); + $this->addSql('ALTER TABLE orderdetails DROP COLUMN eda_visibility'); + } +} diff --git a/migrations/Version20260307204859.php b/migrations/Version20260307204859.php new file mode 100644 index 00000000..325f41ab --- /dev/null +++ b/migrations/Version20260307204859.php @@ -0,0 +1,73 @@ +addSql('ALTER TABLE part_lots DROP INDEX part_lots_idx_barcode, ADD INDEX part_lots_idx_barcode (vendor_barcode(100))'); + $this->addSql('ALTER TABLE part_lots CHANGE vendor_barcode vendor_barcode LONGTEXT DEFAULT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE part_lots DROP INDEX part_lots_idx_barcode, ADD INDEX part_lots_idx_barcode (vendor_barcode)'); + $this->addSql('ALTER TABLE part_lots CHANGE vendor_barcode vendor_barcode VARCHAR(255) DEFAULT NULL'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at FROM part_lots'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, vendor_barcode CLOB DEFAULT NULL, last_stocktake_at DATETIME DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES storelocations (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at) SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added, vendor_barcode, last_stocktake_at FROM __temp__part_lots'); + $this->addSql('DROP TABLE __temp__part_lots'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner FROM part_lots'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, last_stocktake_at DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO part_lots (id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner) SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_stocktake_at, last_modified, datetime_added, id_store_location, id_part, id_owner FROM __temp__part_lots'); + $this->addSql('DROP TABLE __temp__part_lots'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql('DROP INDEX part_lots_idx_barcode'); + $this->addSql('ALTER TABLE part_lots ALTER vendor_barcode TYPE TEXT'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql('DROP INDEX part_lots_idx_barcode'); + $this->addSql('ALTER TABLE part_lots ALTER vendor_barcode TYPE VARCHAR(255)'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + } +} diff --git a/package.json b/package.json index f2fbebcd..f846f1d1 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,16 @@ "@symfony/stimulus-bridge": "^4.0.0", "@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets", "@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets", - "@symfony/webpack-encore": "^5.0.0", + "@symfony/webpack-encore": "^6.0.0", "bootstrap": "^5.1.3", - "core-js": "^3.23.0", - "intl-messageformat": "^10.2.5", + "core-js": "^3.38.0", + "intl-messageformat": "^10.5.11", "jquery": "^3.5.1", "popper.js": "^1.14.7", - "regenerator-runtime": "^0.13.9", + "regenerator-runtime": "^0.14.1", "webpack": "^5.74.0", - "webpack-bundle-analyzer": "^4.3.0", - "webpack-cli": "^5.1.0", + "webpack-bundle-analyzer": "^5.1.1", + "webpack-cli": "^6.0.0", "webpack-notifier": "^1.15.0" }, "license": "AGPL-3.0-or-later", @@ -30,14 +30,12 @@ "build": "encore production --progress" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" }, "dependencies": { "@algolia/autocomplete-js": "^1.17.0", "@algolia/autocomplete-plugin-recent-searches": "^1.17.0", "@algolia/autocomplete-theme-classic": "^1.17.0", - "@ckeditor/ckeditor5-dev-translations": "^43.0.1", - "@ckeditor/ckeditor5-dev-utils": "^43.0.1", "@jbtronics/bs-treeview": "^1.0.1", "@part-db/html5-qrcode": "^4.0.0", "@zxcvbn-ts/core": "^3.0.2", @@ -46,11 +44,12 @@ "@zxcvbn-ts/language-en": "^3.0.1", "@zxcvbn-ts/language-fr": "^3.0.1", "@zxcvbn-ts/language-ja": "^3.0.1", + "attr-accept": "^2.2.5", "barcode-detector": "^3.0.5", "bootbox": "^6.0.0", "bootswatch": "^5.1.3", "bs-custom-file-input": "^1.3.4", - "ckeditor5": "^47.0.0", + "ckeditor5": "^48.0.0", "clipboard": "^2.0.4", "compression-webpack-plugin": "^11.1.0", "datatables.net": "^2.0.0", @@ -64,14 +63,17 @@ "exports-loader": "^5.0.0", "json-formatter-js": "^2.3.4", "jszip": "^3.2.0", - "katex": "^0.16.0", - "marked": "^16.1.1", + "katex": "^0.17.0", + "marked": "^18.0.0", "marked-gfm-heading-id": "^4.1.1", "marked-mangle": "^1.0.1", - "pdfmake": "^0.2.2", + "pdfmake": "^0.3.7", "stimulus-use": "^0.52.0", "tom-select": "^2.1.0", "ts-loader": "^9.2.6", - "typescript": "^5.7.2" + "typescript": "^6.0.2" + }, + "resolutions": { + "jquery": "^3.5.1" } } diff --git a/phpstan.banned_code.neon b/phpstan.banned_code.neon new file mode 100644 index 00000000..3099c384 --- /dev/null +++ b/phpstan.banned_code.neon @@ -0,0 +1,86 @@ +# Manually configure ekino/phpstan-banned-code to detect usage of echo, eval, die/exit, print, shell execution and a set of functions that should not be used in production code. + +parametersSchema: + banned_code: structure([ + nodes: listOf(structure([ + type: string() + functions: schema(listOf(string()), nullable()) + ])) + use_from_tests: bool() + non_ignorable: bool() + ]) + +parameters: + banned_code: + nodes: + # enable detection of echo + - + type: Stmt_Echo + functions: null + + # enable detection of eval + - + type: Expr_Eval + functions: null + + # enable detection of die/exit + - + type: Expr_Exit + functions: null + + # enable detection of a set of functions + - + type: Expr_FuncCall + functions: + - dd + - debug_backtrace + - dump + - exec + - passthru + - phpinfo + - print_r + - proc_open + - shell_exec + - system + - var_dump + + # enable detection of print statements + - + type: Expr_Print + functions: null + + # enable detection of shell execution by backticks + - + type: Expr_ShellExec + functions: null + + # enable detection of empty() + #- + # type: Expr_Empty + # functions: null + + # enable detection of `use Tests\Foo\Bar` in a non-test file + use_from_tests: true + + # when true, errors cannot be excluded + non_ignorable: false + +services: + - + class: Ekino\PHPStanBannedCode\Rules\BannedNodesRule + tags: + - phpstan.rules.rule + arguments: + - '%banned_code.nodes%' + + - + class: Ekino\PHPStanBannedCode\Rules\BannedUseTestRule + tags: + - phpstan.rules.rule + arguments: + - '%banned_code.use_from_tests%' + + - + class: Ekino\PHPStanBannedCode\Rules\BannedNodesErrorBuilder + arguments: + - '%banned_code.non_ignorable%' diff --git a/phpstan.dist.neon b/phpstan.dist.neon index fc7b3524..c7da636f 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,3 +1,6 @@ +includes: + - phpstan.banned_code.neon + parameters: level: 5 @@ -56,8 +59,20 @@ parameters: - '#expects .*PartParameter, .*AbstractParameter given.#' - '#Part::getParameters\(\) should return .*AbstractParameter#' + # Fix some weird issue with how covariance with collections is solved + - '#Method App\\Entity\\Base\\AbstractStructuralDBElement::getParameters\(\) should return Doctrine\\Common\\Collections\\Collection but returns#' + # Ignore doctrine type mapping mismatch - '#Property .* type mapping mismatch: property can contain .* but database expects .*#' # Ignore error of unused WithPermPresetsTrait, as it is used in the migrations which are not analyzed by Phpstan - '#Trait App\\Migration\\WithPermPresetsTrait is used zero times and is not analysed#' + - + message: '#Should not use function "shell_exec"#' + path: src/Services/System/UpdateExecutor.php + + - message: '#Access to an undefined property Brick\\Schema\\Interfaces\\#' + path: src/Services/InfoProviderSystem/Providers/GenericWebProvider.php + + - + identifier: nullCoalesce.property diff --git a/public/kicad/.gitignore b/public/kicad/.gitignore new file mode 100644 index 00000000..1f2ab53d --- /dev/null +++ b/public/kicad/.gitignore @@ -0,0 +1,3 @@ +# They are user generated and should not be tracked by git +footprints_custom.txt +symbols_custom.txt diff --git a/public/kicad/footprints.txt b/public/kicad/footprints.txt index 8f0f944c..779a7751 100644 --- a/public/kicad/footprints.txt +++ b/public/kicad/footprints.txt @@ -1,10 +1,11 @@ -# This file contains all the KiCad footprints available in the official library -# Generated by footprints.sh -# on Sun Feb 16 21:19:56 CET 2025 +# Generated on Mon May 25 06:41:46 UTC 2026 +# This file contains all footprints available in the offical KiCAD library Audio_Module:Reverb_BTDR-1H Audio_Module:Reverb_BTDR-1V Battery:BatteryClip_Keystone_54_D16-19mm +Battery:BatteryHolder_Bulgin_BX0034_1xAAA Battery:BatteryHolder_Bulgin_BX0036_1xC +Battery:BatteryHolder_Bulgin_BX0123_1xCR123 Battery:BatteryHolder_ComfortableElectronic_CH273-2450_1x2450 Battery:BatteryHolder_Eagle_12BH611-GR Battery:BatteryHolder_Keystone_103_1x20mm @@ -36,9 +37,10 @@ Battery:BatteryHolder_MPD_BA9VPC_1xPP3 Battery:BatteryHolder_MPD_BC12AAPC_2xAA Battery:BatteryHolder_MPD_BC2003_1x2032 Battery:BatteryHolder_MPD_BC2AAPC_2xAA -Battery:BatteryHolder_MPD_BH-18650-PC2 -Battery:BatteryHolder_Multicomp_BC-2001_1x2032 +Battery:BatteryHolder_MPD_BH-18650-PC +Battery:BatteryHolder_MPD_BK-18650-PC2 Battery:BatteryHolder_MYOUNG_BS-07-A1BJ001_CR2032 +Battery:BatteryHolder_Multicomp_BC-2001_1x2032 Battery:BatteryHolder_Renata_SMTU2032-LF_1x2032 Battery:BatteryHolder_Seiko_MS621F Battery:BatteryHolder_TruPower_BH-331P_3xAA @@ -181,9 +183,18 @@ Button_Switch_SMD:SW_MEC_5GSH9 Button_Switch_SMD:SW_Push_1P1T-MP_NO_Horizontal_Alps_SKRTLAE010 Button_Switch_SMD:SW_Push_1P1T-SH_NO_CK_KMR2xxG Button_Switch_SMD:SW_Push_1P1T_NO_CK_KMR2 +Button_Switch_SMD:SW_Push_1P1T_NO_CK_KSC6xxG Button_Switch_SMD:SW_Push_1P1T_NO_CK_KSC6xxJ Button_Switch_SMD:SW_Push_1P1T_NO_CK_KSC7xxJ +Button_Switch_SMD:SW_Push_1P1T_NO_CK_KSC9xxG +Button_Switch_SMD:SW_Push_1P1T_NO_CK_KSC9xxJ Button_Switch_SMD:SW_Push_1P1T_NO_CK_PTS125Sx43PSMTR +Button_Switch_SMD:SW_Push_1P1T_NO_CK_PTS125Sx43SMTR +Button_Switch_SMD:SW_Push_1P1T_NO_CK_PTS125Sx73PSMTR +Button_Switch_SMD:SW_Push_1P1T_NO_CK_PTS125Sx73SMTR +Button_Switch_SMD:SW_Push_1P1T_NO_CK_PTS125Sx85PSMTR +Button_Switch_SMD:SW_Push_1P1T_NO_CK_PTS125Sx85SMTR +Button_Switch_SMD:SW_Push_1P1T_NO_E-Switch_TL3301NxxxxxG Button_Switch_SMD:SW_Push_1P1T_NO_Vertical_Wuerth_434133025816 Button_Switch_SMD:SW_Push_1P1T_XKB_TS-1187A Button_Switch_SMD:SW_Push_1TS009xxxx-xxxx-xxxx_6x6x5mm @@ -192,51 +203,57 @@ Button_Switch_SMD:SW_SP3T_PCM13 Button_Switch_SMD:SW_SPDT_CK_JS102011SAQN Button_Switch_SMD:SW_SPDT_PCM12 Button_Switch_SMD:SW_SPDT_REED_MSDM-DT +Button_Switch_SMD:SW_SPDT_Shouhan_MSK12C02 Button_Switch_SMD:SW_SPST_B3S-1000 Button_Switch_SMD:SW_SPST_B3S-1100 Button_Switch_SMD:SW_SPST_B3SL-1002P Button_Switch_SMD:SW_SPST_B3SL-1022P -Button_Switch_SMD:SW_SPST_B3U-1000P-B Button_Switch_SMD:SW_SPST_B3U-1000P -Button_Switch_SMD:SW_SPST_B3U-1100P-B +Button_Switch_SMD:SW_SPST_B3U-1000P-B Button_Switch_SMD:SW_SPST_B3U-1100P -Button_Switch_SMD:SW_SPST_B3U-3000P-B +Button_Switch_SMD:SW_SPST_B3U-1100P-B Button_Switch_SMD:SW_SPST_B3U-3000P -Button_Switch_SMD:SW_SPST_B3U-3100P-B +Button_Switch_SMD:SW_SPST_B3U-3000P-B Button_Switch_SMD:SW_SPST_B3U-3100P +Button_Switch_SMD:SW_SPST_B3U-3100P-B Button_Switch_SMD:SW_SPST_CK_KMS2xxG Button_Switch_SMD:SW_SPST_CK_KMS2xxGP Button_Switch_SMD:SW_SPST_CK_KXT3 Button_Switch_SMD:SW_SPST_CK_RS282G05A3 Button_Switch_SMD:SW_SPST_EVPBF Button_Switch_SMD:SW_SPST_EVQP0 -Button_Switch_SMD:SW_SPST_EVQP2 +Button_Switch_SMD:SW_SPST_EVQP2_MiddlePushTravel_H2.5mm +Button_Switch_SMD:SW_SPST_EVQP2_ShortPushTravel_H2.1mm +Button_Switch_SMD:SW_SPST_EVQP2_ShortPushTravel_H2.5mm Button_Switch_SMD:SW_SPST_EVQP7A Button_Switch_SMD:SW_SPST_EVQP7C Button_Switch_SMD:SW_SPST_EVQPE1 Button_Switch_SMD:SW_SPST_EVQQ2 Button_Switch_SMD:SW_SPST_FSMSM +Button_Switch_SMD:SW_SPST_GT-TC155X Button_Switch_SMD:SW_SPST_Omron_B3FS-100xP Button_Switch_SMD:SW_SPST_Omron_B3FS-101xP Button_Switch_SMD:SW_SPST_Omron_B3FS-105xP -Button_Switch_SMD:SW_SPST_Panasonic_EVQPL_3PL_5PL_PT_A08 -Button_Switch_SMD:SW_SPST_Panasonic_EVQPL_3PL_5PL_PT_A15 -Button_Switch_SMD:SW_SPST_PTS645 +Button_Switch_SMD:SW_SPST_PTS645Sx43SMTR92 Button_Switch_SMD:SW_SPST_PTS647_Sx38 Button_Switch_SMD:SW_SPST_PTS647_Sx50 Button_Switch_SMD:SW_SPST_PTS647_Sx70 Button_Switch_SMD:SW_SPST_PTS810 +Button_Switch_SMD:SW_SPST_Panasonic_EVQPL_3PL_5PL_PT_A08 +Button_Switch_SMD:SW_SPST_Panasonic_EVQPL_3PL_5PL_PT_A15 Button_Switch_SMD:SW_SPST_REED_CT05-XXXX-G1 Button_Switch_SMD:SW_SPST_REED_CT05-XXXX-J1 Button_Switch_SMD:SW_SPST_REED_CT10-XXXX-G1 Button_Switch_SMD:SW_SPST_REED_CT10-XXXX-G2 Button_Switch_SMD:SW_SPST_REED_CT10-XXXX-G4 -Button_Switch_SMD:SW_SPST_SKQG_WithoutStem Button_Switch_SMD:SW_SPST_SKQG_WithStem +Button_Switch_SMD:SW_SPST_SKQG_WithoutStem Button_Switch_SMD:SW_SPST_TL3305A Button_Switch_SMD:SW_SPST_TL3305B Button_Switch_SMD:SW_SPST_TL3305C Button_Switch_SMD:SW_SPST_TL3342 +Button_Switch_SMD:SW_SPST_TS-1088-xR020 +Button_Switch_SMD:SW_SPST_TS-1088-xR025 Button_Switch_SMD:SW_Tactile_SPST_NO_Straight_CK_PTS636Sx25SMTRLFS Button_Switch_THT:KSA_Tactile_SPST Button_Switch_THT:Nidec_Copal_SH-7010C @@ -304,6 +321,17 @@ Button_Switch_THT:SW_PUSH-12mm Button_Switch_THT:SW_PUSH-12mm_Wuerth-430476085716 Button_Switch_THT:SW_PUSH_1P1T_6x3.5mm_H4.3_APEM_MJTP1243 Button_Switch_THT:SW_PUSH_1P1T_6x3.5mm_H5.0_APEM_MJTP1250 +Button_Switch_THT:SW_PUSH_6mm +Button_Switch_THT:SW_PUSH_6mm_H13mm +Button_Switch_THT:SW_PUSH_6mm_H4.3mm +Button_Switch_THT:SW_PUSH_6mm_H5mm +Button_Switch_THT:SW_PUSH_6mm_H7.3mm +Button_Switch_THT:SW_PUSH_6mm_H8.5mm +Button_Switch_THT:SW_PUSH_6mm_H8mm +Button_Switch_THT:SW_PUSH_6mm_H9.5mm +Button_Switch_THT:SW_PUSH_E-Switch_FS5700DP_DPDT +Button_Switch_THT:SW_PUSH_LCD_E3_SAxxxx +Button_Switch_THT:SW_PUSH_LCD_E3_SAxxxx_SocketPins Button_Switch_THT:SW_Push_1P1T_NO_LED_E-Switch_TL1250 Button_Switch_THT:SW_Push_1P2T_Vertical_E-Switch_800UDP8P1A1M6 Button_Switch_THT:SW_Push_2P1T_Toggle_CK_PVA1xxH1xxxxxxV2 @@ -316,23 +344,20 @@ Button_Switch_THT:SW_Push_2P2T_Toggle_CK_PVA2xxH2xxxxxxV2 Button_Switch_THT:SW_Push_2P2T_Toggle_CK_PVA2xxH3xxxxxxV2 Button_Switch_THT:SW_Push_2P2T_Toggle_CK_PVA2xxH4xxxxxxV2 Button_Switch_THT:SW_Push_2P2T_Vertical_E-Switch_800UDP8P1A1M6 -Button_Switch_THT:SW_PUSH_6mm -Button_Switch_THT:SW_PUSH_6mm_H13mm -Button_Switch_THT:SW_PUSH_6mm_H4.3mm -Button_Switch_THT:SW_PUSH_6mm_H5mm -Button_Switch_THT:SW_PUSH_6mm_H7.3mm -Button_Switch_THT:SW_PUSH_6mm_H8.5mm -Button_Switch_THT:SW_PUSH_6mm_H8mm -Button_Switch_THT:SW_PUSH_6mm_H9.5mm -Button_Switch_THT:SW_PUSH_E-Switch_FS5700DP_DPDT -Button_Switch_THT:SW_PUSH_LCD_E3_SAxxxx -Button_Switch_THT:SW_PUSH_LCD_E3_SAxxxx_SocketPins -Button_Switch_THT:SW_Slide-03_Wuerth-WS-SLTV_10x2.5x6.4_P2.54mm -Button_Switch_THT:SW_Slide_SPDT_Angled_CK_OS102011MA1Q -Button_Switch_THT:SW_Slide_SPDT_Straight_CK_OS102011MS2Q Button_Switch_THT:SW_SPST_Omron_B3F-315x_Angled Button_Switch_THT:SW_SPST_Omron_B3F-40xx Button_Switch_THT:SW_SPST_Omron_B3F-50xx +Button_Switch_THT:SW_Slide-03_Wuerth-WS-SLTV_10x2.5x6.4_P2.54mm +Button_Switch_THT:SW_Slide_SP3T_Straight_CK_OS103012MU1QP1 +Button_Switch_THT:SW_Slide_SPDT_Angled_CK_OS102011MA1Q +Button_Switch_THT:SW_Slide_SPDT_Straight_CK_OS102011MS2Q +Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x +Button_Switch_THT:SW_TH_Tactile_Omron_B3F-102x +Button_Switch_THT:SW_TH_Tactile_Omron_B3F-106x +Button_Switch_THT:SW_TH_Tactile_Omron_B3F-107x +Button_Switch_THT:SW_TH_Tactile_Omron_B3F-110x +Button_Switch_THT:SW_TH_Tactile_Omron_B3F-1110 +Button_Switch_THT:SW_TH_Tactile_Omron_B3F-112x Button_Switch_THT:SW_Tactile_SKHH_Angled Button_Switch_THT:SW_Tactile_SPST_Angled_PTS645Vx31-2LFS Button_Switch_THT:SW_Tactile_SPST_Angled_PTS645Vx39-2LFS @@ -340,7 +365,6 @@ Button_Switch_THT:SW_Tactile_SPST_Angled_PTS645Vx58-2LFS Button_Switch_THT:SW_Tactile_SPST_Angled_PTS645Vx83-2LFS Button_Switch_THT:SW_Tactile_Straight_KSA0Axx1LFTR Button_Switch_THT:SW_Tactile_Straight_KSL0Axx1LFTR -Button_Switch_THT:SW_TH_Tactile_Omron_B3F-10xx Button_Switch_THT:SW_XKB_DM1-16UC-1 Button_Switch_THT:SW_XKB_DM1-16UD-1 Button_Switch_THT:SW_XKB_DM1-16UP-1 @@ -359,13 +383,24 @@ Buzzer_Beeper:MagneticBuzzer_CUI_CST-931RP-A Buzzer_Beeper:MagneticBuzzer_Kingstate_KCG0601 Buzzer_Beeper:MagneticBuzzer_Kobitone_254-EMB73-RO Buzzer_Beeper:MagneticBuzzer_Kobitone_254-EMB84Q-RO -Buzzer_Beeper:MagneticBuzzer_ProjectsUnlimited_AI-4228-TWT-R +Buzzer_Beeper:MagneticBuzzer_PUI_AT-0927-TT-6-R +Buzzer_Beeper:MagneticBuzzer_PUI_SMT-1028-T-2-R Buzzer_Beeper:MagneticBuzzer_ProSignal_ABI-009-RC Buzzer_Beeper:MagneticBuzzer_ProSignal_ABI-010-RC Buzzer_Beeper:MagneticBuzzer_ProSignal_ABT-410-RC -Buzzer_Beeper:MagneticBuzzer_PUI_AT-0927-TT-6-R -Buzzer_Beeper:MagneticBuzzer_PUI_SMT-1028-T-2-R +Buzzer_Beeper:MagneticBuzzer_ProjectsUnlimited_AI-4228-TWT-R +Buzzer_Beeper:MagneticBuzzer_StarMicronics_HGP +Buzzer_Beeper:MagneticBuzzer_StarMicronics_HMB Buzzer_Beeper:MagneticBuzzer_StarMicronics_HMB-06_HMB-12 +Buzzer_Beeper:MagneticBuzzer_StarMicronics_QMB +Buzzer_Beeper:MagneticBuzzer_StarMicronics_QMB-105 +Buzzer_Beeper:MagneticBuzzer_StarMicronics_QMB-108 +Buzzer_Beeper:MagneticBuzzer_StarMicronics_QMB-111 +Buzzer_Beeper:MagneticBuzzer_StarMicronics_QMX +Buzzer_Beeper:MagneticBuzzer_StarMicronics_RMX +Buzzer_Beeper:MagneticBuzzer_StarMicronics_TMB +Buzzer_Beeper:MagneticBuzzer_StarMicronics_TMX-F +Buzzer_Beeper:MagneticBuzzer_StarMicronics_TMX-H Buzzer_Beeper:PUIAudio_SMT_0825_S_4_R Buzzer_Beeper:Speaker_CUI_CMR-1206S-67 Calibration_Scale:Gauge_100mm_Grid_Type1_CopperTop @@ -387,8 +422,8 @@ Calibration_Scale:Gauge_50mm_Type1_CopperTop Calibration_Scale:Gauge_50mm_Type1_SilkScreenTop Calibration_Scale:Gauge_50mm_Type2_CopperTop Calibration_Scale:Gauge_50mm_Type2_SilkScreenTop -Capacitor_SMD:CP_Elec_10x10.5 Capacitor_SMD:CP_Elec_10x10 +Capacitor_SMD:CP_Elec_10x10.5 Capacitor_SMD:CP_Elec_10x12.5 Capacitor_SMD:CP_Elec_10x12.6 Capacitor_SMD:CP_Elec_10x14.3 @@ -400,15 +435,15 @@ Capacitor_SMD:CP_Elec_18x17.5 Capacitor_SMD:CP_Elec_18x22 Capacitor_SMD:CP_Elec_3x5.3 Capacitor_SMD:CP_Elec_3x5.4 -Capacitor_SMD:CP_Elec_4x3.9 Capacitor_SMD:CP_Elec_4x3 +Capacitor_SMD:CP_Elec_4x3.9 Capacitor_SMD:CP_Elec_4x4.5 Capacitor_SMD:CP_Elec_4x5.3 Capacitor_SMD:CP_Elec_4x5.4 Capacitor_SMD:CP_Elec_4x5.7 Capacitor_SMD:CP_Elec_4x5.8 -Capacitor_SMD:CP_Elec_5x3.9 Capacitor_SMD:CP_Elec_5x3 +Capacitor_SMD:CP_Elec_5x3.9 Capacitor_SMD:CP_Elec_5x4.4 Capacitor_SMD:CP_Elec_5x4.5 Capacitor_SMD:CP_Elec_5x5.3 @@ -416,8 +451,8 @@ Capacitor_SMD:CP_Elec_5x5.4 Capacitor_SMD:CP_Elec_5x5.7 Capacitor_SMD:CP_Elec_5x5.8 Capacitor_SMD:CP_Elec_5x5.9 -Capacitor_SMD:CP_Elec_6.3x3.9 Capacitor_SMD:CP_Elec_6.3x3 +Capacitor_SMD:CP_Elec_6.3x3.9 Capacitor_SMD:CP_Elec_6.3x4.5 Capacitor_SMD:CP_Elec_6.3x4.9 Capacitor_SMD:CP_Elec_6.3x5.2 @@ -429,8 +464,8 @@ Capacitor_SMD:CP_Elec_6.3x5.8 Capacitor_SMD:CP_Elec_6.3x5.9 Capacitor_SMD:CP_Elec_6.3x7.7 Capacitor_SMD:CP_Elec_6.3x9.9 -Capacitor_SMD:CP_Elec_8x10.5 Capacitor_SMD:CP_Elec_8x10 +Capacitor_SMD:CP_Elec_8x10.5 Capacitor_SMD:CP_Elec_8x11.9 Capacitor_SMD:CP_Elec_8x5.4 Capacitor_SMD:CP_Elec_8x6.2 @@ -490,62 +525,6 @@ Capacitor_SMD:C_Trimmer_Voltronics_JQ Capacitor_SMD:C_Trimmer_Voltronics_JR Capacitor_SMD:C_Trimmer_Voltronics_JV Capacitor_SMD:C_Trimmer_Voltronics_JZ -Capacitor_Tantalum_SMD:CP_EIA-1608-08_AVX-J -Capacitor_Tantalum_SMD:CP_EIA-1608-08_AVX-J_Pad1.25x1.05mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-1608-10_AVX-L -Capacitor_Tantalum_SMD:CP_EIA-1608-10_AVX-L_Pad1.25x1.05mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-2012-12_Kemet-R -Capacitor_Tantalum_SMD:CP_EIA-2012-12_Kemet-R_Pad1.30x1.05mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-2012-15_AVX-P -Capacitor_Tantalum_SMD:CP_EIA-2012-15_AVX-P_Pad1.30x1.05mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-3216-10_Kemet-I -Capacitor_Tantalum_SMD:CP_EIA-3216-10_Kemet-I_Pad1.58x1.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-3216-12_Kemet-S -Capacitor_Tantalum_SMD:CP_EIA-3216-12_Kemet-S_Pad1.58x1.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A -Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A_Pad1.58x1.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-3528-12_Kemet-T -Capacitor_Tantalum_SMD:CP_EIA-3528-12_Kemet-T_Pad1.50x2.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-3528-15_AVX-H -Capacitor_Tantalum_SMD:CP_EIA-3528-15_AVX-H_Pad1.50x2.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B -Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B_Pad1.50x2.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-6032-15_Kemet-U -Capacitor_Tantalum_SMD:CP_EIA-6032-15_Kemet-U_Pad2.25x2.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-6032-20_AVX-F -Capacitor_Tantalum_SMD:CP_EIA-6032-20_AVX-F_Pad2.25x2.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C -Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C_Pad2.25x2.35mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7132-20_AVX-U -Capacitor_Tantalum_SMD:CP_EIA-7132-20_AVX-U_Pad2.72x3.50mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7132-28_AVX-C -Capacitor_Tantalum_SMD:CP_EIA-7132-28_AVX-C_Pad2.72x3.50mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7260-15_AVX-R -Capacitor_Tantalum_SMD:CP_EIA-7260-15_AVX-R_Pad2.68x6.30mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7260-20_AVX-M -Capacitor_Tantalum_SMD:CP_EIA-7260-20_AVX-M_Pad2.68x6.30mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7260-28_AVX-M -Capacitor_Tantalum_SMD:CP_EIA-7260-28_AVX-M_Pad2.68x6.30mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7260-38_AVX-R -Capacitor_Tantalum_SMD:CP_EIA-7260-38_AVX-R_Pad2.68x6.30mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7343-15_Kemet-W -Capacitor_Tantalum_SMD:CP_EIA-7343-15_Kemet-W_Pad2.25x2.55mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7343-20_Kemet-V -Capacitor_Tantalum_SMD:CP_EIA-7343-20_Kemet-V_Pad2.25x2.55mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7343-30_AVX-N -Capacitor_Tantalum_SMD:CP_EIA-7343-30_AVX-N_Pad2.25x2.55mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D -Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D_Pad2.25x2.55mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7343-40_Kemet-Y -Capacitor_Tantalum_SMD:CP_EIA-7343-40_Kemet-Y_Pad2.25x2.55mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-X -Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-X_Pad2.25x2.55mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7360-38_Kemet-E -Capacitor_Tantalum_SMD:CP_EIA-7360-38_Kemet-E_Pad2.25x4.25mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7361-38_AVX-V -Capacitor_Tantalum_SMD:CP_EIA-7361-38_AVX-V_Pad2.18x3.30mm_HandSolder -Capacitor_Tantalum_SMD:CP_EIA-7361-438_AVX-U -Capacitor_Tantalum_SMD:CP_EIA-7361-438_AVX-U_Pad2.18x3.30mm_HandSolder Capacitor_THT:CP_Axial_L10.0mm_D4.5mm_P15.00mm_Horizontal Capacitor_THT:CP_Axial_L10.0mm_D6.0mm_P15.00mm_Horizontal Capacitor_THT:CP_Axial_L11.0mm_D5.0mm_P18.00mm_Horizontal @@ -893,11 +872,17 @@ Capacitor_THT:C_Rect_L7.0mm_W4.5mm_P5.00mm Capacitor_THT:C_Rect_L7.0mm_W6.0mm_P5.00mm Capacitor_THT:C_Rect_L7.0mm_W6.5mm_P5.00mm Capacitor_THT:C_Rect_L7.2mm_W11.0mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W2.5mm_P5.00mm Capacitor_THT:C_Rect_L7.2mm_W2.5mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 Capacitor_THT:C_Rect_L7.2mm_W3.0mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W3.5mm_P5.00mm Capacitor_THT:C_Rect_L7.2mm_W3.5mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W4.5mm_P5.00mm Capacitor_THT:C_Rect_L7.2mm_W4.5mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W5.0mm_P5.00mm Capacitor_THT:C_Rect_L7.2mm_W5.5mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W6.0mm_P5.00mm +Capacitor_THT:C_Rect_L7.2mm_W7.2mm_P5.00mm Capacitor_THT:C_Rect_L7.2mm_W7.2mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 Capacitor_THT:C_Rect_L7.2mm_W8.5mm_P5.00mm_FKP2_FKP2_MKS2_MKP2 Capacitor_THT:C_Rect_L7.5mm_W6.5mm_P5.00mm @@ -924,6 +909,63 @@ Capacitor_THT:C_Rect_L9.0mm_W9.8mm_P7.50mm_MKT Capacitor_THT:DX_5R5HxxxxU_D11.5mm_P10.00mm Capacitor_THT:DX_5R5VxxxxU_D11.5mm_P5.00mm Capacitor_THT:DX_5R5VxxxxU_D19.0mm_P5.00mm +Capacitor_Tantalum_SMD:CP_EIA-1608-08_AVX-J +Capacitor_Tantalum_SMD:CP_EIA-1608-08_AVX-J_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-1608-10_AVX-L +Capacitor_Tantalum_SMD:CP_EIA-1608-10_AVX-L_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-2012-12_Kemet-R +Capacitor_Tantalum_SMD:CP_EIA-2012-12_Kemet-R_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-2012-15_AVX-P +Capacitor_Tantalum_SMD:CP_EIA-2012-15_AVX-P_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3216-10_Kemet-I +Capacitor_Tantalum_SMD:CP_EIA-3216-10_Kemet-I_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3216-12_Kemet-S +Capacitor_Tantalum_SMD:CP_EIA-3216-12_Kemet-S_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A +Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3528-12_Kemet-T +Capacitor_Tantalum_SMD:CP_EIA-3528-12_Kemet-T_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3528-15_AVX-H +Capacitor_Tantalum_SMD:CP_EIA-3528-15_AVX-H_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B +Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-6032-15_Kemet-U +Capacitor_Tantalum_SMD:CP_EIA-6032-15_Kemet-U_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-6032-20_AVX-F +Capacitor_Tantalum_SMD:CP_EIA-6032-20_AVX-F_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C +Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7132-20_AVX-U +Capacitor_Tantalum_SMD:CP_EIA-7132-20_AVX-U_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7132-28_AVX-C +Capacitor_Tantalum_SMD:CP_EIA-7132-28_AVX-C_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7260-15_AVX-R +Capacitor_Tantalum_SMD:CP_EIA-7260-15_AVX-R_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7260-20_AVX-M +Capacitor_Tantalum_SMD:CP_EIA-7260-20_AVX-M_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7260-28_AVX-M +Capacitor_Tantalum_SMD:CP_EIA-7260-28_AVX-M_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7260-38_AVX-R +Capacitor_Tantalum_SMD:CP_EIA-7260-38_AVX-R_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-15_Kemet-W +Capacitor_Tantalum_SMD:CP_EIA-7343-15_Kemet-W_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-20_Kemet-V +Capacitor_Tantalum_SMD:CP_EIA-7343-20_Kemet-V_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-30_AVX-N +Capacitor_Tantalum_SMD:CP_EIA-7343-30_AVX-N_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D +Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-40_Kemet-Y +Capacitor_Tantalum_SMD:CP_EIA-7343-40_Kemet-Y_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-X +Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-X_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7360-38_Kemet-E +Capacitor_Tantalum_SMD:CP_EIA-7360-38_Kemet-E_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7361-38_AVX-V +Capacitor_Tantalum_SMD:CP_EIA-7361-38_AVX-V_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7361-438_AVX-U +Capacitor_Tantalum_SMD:CP_EIA-7361-438_AVX-U_HandSolder +Connector:BJB_Pico_46.110.1001_Receptacle_Horizontal Connector:Banana_Cliff_FCR7350B_S16N-PC_Horizontal Connector:Banana_Cliff_FCR7350G_S16N-PC_Horizontal Connector:Banana_Cliff_FCR7350L_S16N-PC_Horizontal @@ -933,18 +975,19 @@ Connector:Banana_Cliff_FCR7350Y_S16N-PC_Horizontal Connector:Banana_Jack_1Pin Connector:Banana_Jack_2Pin Connector:Banana_Jack_3Pin -Connector:CalTest_CT3151 -Connector:Connector_SFP_and_Cage Connector:CUI_PD-30 Connector:CUI_PD-30S Connector:CUI_PD-30S_CircularHoles +Connector:CalTest_CT3151 +Connector:Conn_C14_Receptacle_RightAngle_Schurter_DD21.01xx +Connector:Connector_SFP_and_Cage Connector:DTF13-12Px Connector:FanPinHeader_1x03_P2.54mm_Vertical Connector:FanPinHeader_1x04_P2.54mm_Vertical Connector:GB042-34S-H10 Connector:IHI_B6A-PCB-45_Vertical -Connector:Joint-Tech_C5080WR-04P_1x04_P5.08mm_Vertical Connector:JWT_A3963_1x02_P3.96mm_Vertical +Connector:Joint-Tech_C5080WR-04P_1x04_P5.08mm_Vertical Connector:NS-Tech_Grove_1x04_P2mm_Vertical Connector:OCN_OK-01GM030-04_2x15_P0.4mm_Vertical Connector:SpringContact_Harwin_S1941-46R @@ -1031,18 +1074,6 @@ Connector_Audio:Jack_6.35mm_Neutrik_NSJ12HH-1_Horizontal Connector_Audio:Jack_6.35mm_Neutrik_NSJ12HL_Horizontal Connector_Audio:Jack_6.35mm_Neutrik_NSJ8HC_Horizontal Connector_Audio:Jack_6.35mm_Neutrik_NSJ8HL_Horizontal -Connector_Audio:Jack_speakON-6.35mm_Neutrik_NLJ2MDXX-H_Horizontal -Connector_Audio:Jack_speakON-6.35mm_Neutrik_NLJ2MDXX-V_Vertical -Connector_Audio:Jack_speakON_Neutrik_NL2MDXX-H-3_Horizontal -Connector_Audio:Jack_speakON_Neutrik_NL2MDXX-V_Vertical -Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-H-2_Horizontal -Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-H-3_Horizontal -Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V-2_Vertical -Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V-3_Vertical -Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V_Vertical -Connector_Audio:Jack_speakON_Neutrik_NL8MDXX-V-3_Vertical -Connector_Audio:Jack_speakON_Neutrik_NL8MDXX-V_Vertical -Connector_Audio:Jack_speakON_Neutrik_NLT4MD-V_Vertical Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ10FI-H-0_Horizontal Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ10FI-H_Horizontal Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ10FI-V-0_Vertical @@ -1178,6 +1209,18 @@ Connector_Audio:Jack_XLR_Neutrik_NC5MBH_Horizontal Connector_Audio:Jack_XLR_Neutrik_NC5MBV-B_Vertical Connector_Audio:Jack_XLR_Neutrik_NC5MBV-SW_Vertical Connector_Audio:Jack_XLR_Neutrik_NC5MBV_Vertical +Connector_Audio:Jack_speakON-6.35mm_Neutrik_NLJ2MDXX-H_Horizontal +Connector_Audio:Jack_speakON-6.35mm_Neutrik_NLJ2MDXX-V_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL2MDXX-H-3_Horizontal +Connector_Audio:Jack_speakON_Neutrik_NL2MDXX-V_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-H-2_Horizontal +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-H-3_Horizontal +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V-2_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V-3_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL8MDXX-V-3_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL8MDXX-V_Vertical +Connector_Audio:Jack_speakON_Neutrik_NLT4MD-V_Vertical Connector_Audio:MiniXLR-5_Switchcraft_TRAPC_Horizontal Connector_Audio:Plug_3.5mm_CUI_SP-3541 Connector_BarrelJack:BarrelJack_CLIFF_FC681465S_SMT_Horizontal @@ -1197,6 +1240,14 @@ Connector_BarrelJack:BarrelJack_Wuerth_694108106102_2.5x5.5mm Connector_BarrelJack:BarrelJack_Wuerth_6941xx301002 Connector_Card:CF-Card_3M_N7E50-A516xx-30 Connector_Card:CF-Card_3M_N7E50-E516xx-30 +Connector_Card:SD-SIM_microSD-microSIM_Molex_104168-1620 +Connector_Card:SD_Card_Device_16mm_SlotDepth +Connector_Card:SD_Hirose_DM1AA_SF_PEJ82 +Connector_Card:SD_Kyocera_145638009211859+ +Connector_Card:SD_Kyocera_145638009511859+ +Connector_Card:SD_Kyocera_145638109211859+ +Connector_Card:SD_Kyocera_145638109511859+ +Connector_Card:SD_TE_2041021 Connector_Card:microSD_HC_Hirose_DM3AT-SF-PEJM5 Connector_Card:microSD_HC_Hirose_DM3BT-DSF-PEJS Connector_Card:microSD_HC_Hirose_DM3D-SF @@ -1207,14 +1258,6 @@ Connector_Card:microSIM_JAE_SF53S006VCBR2000 Connector_Card:nanoSIM_GCT_SIM8060-6-0-14-00 Connector_Card:nanoSIM_GCT_SIM8060-6-1-14-00 Connector_Card:nanoSIM_Hinged_CUI_NSIM-2-C -Connector_Card:SD-SIM_microSD-microSIM_Molex_104168-1620 -Connector_Card:SD_Card_Device_16mm_SlotDepth -Connector_Card:SD_Hirose_DM1AA_SF_PEJ82 -Connector_Card:SD_Kyocera_145638009211859+ -Connector_Card:SD_Kyocera_145638009511859+ -Connector_Card:SD_Kyocera_145638109211859+ -Connector_Card:SD_Kyocera_145638109511859+ -Connector_Card:SD_TE_2041021 Connector_Coaxial:BNC_Amphenol_031-5539_Vertical Connector_Coaxial:BNC_Amphenol_031-6575_Horizontal Connector_Coaxial:BNC_Amphenol_B6252HB-NPP3G-50_Horizontal @@ -1300,6 +1343,28 @@ Connector_DIN:DIN41612_F_2x16_RowsZD_Female_Vertical_THT Connector_DIN:DIN41612_F_2x16_RowsZD_Male_Horizontal_THT Connector_DIN:DIN41612_F_3x16_Female_Vertical_THT Connector_DIN:DIN41612_F_3x16_Male_Horizontal_THT +Connector_DIN:DIN41612_M-flat_3x14+6_Female_Vertical_THT +Connector_DIN:DIN41612_M-flat_3x20+4_Female_Vertical_THT +Connector_DIN:DIN41612_M-flat_3x26+2_Female_Vertical_THT +Connector_DIN:DIN41612_M-flat_3x8+8_Female_Vertical_THT +Connector_DIN:DIN41612_M-invers_3x14+6_Female_Horizontal_THT +Connector_DIN:DIN41612_M-invers_3x14+6_Male_Vertical_THT +Connector_DIN:DIN41612_M-invers_3x2+10_Female_Horizontal_THT +Connector_DIN:DIN41612_M-invers_3x2+10_Male_Vertical_THT +Connector_DIN:DIN41612_M-invers_3x20+4_Female_Horizontal_THT +Connector_DIN:DIN41612_M-invers_3x20+4_Male_Vertical_THT +Connector_DIN:DIN41612_M-invers_3x26+2_Female_Horizontal_THT +Connector_DIN:DIN41612_M-invers_3x26+2_Male_Vertical_THT +Connector_DIN:DIN41612_M-invers_3x8+8_Female_Horizontal_THT +Connector_DIN:DIN41612_M-invers_3x8+8_Male_Vertical_THT +Connector_DIN:DIN41612_M_3x14+6_Female_Vertical_THT +Connector_DIN:DIN41612_M_3x14+6_Male_Horizontal_THT +Connector_DIN:DIN41612_M_3x20+4_Female_Vertical_THT +Connector_DIN:DIN41612_M_3x20+4_Male_Horizontal_THT +Connector_DIN:DIN41612_M_3x26+2_Female_Vertical_THT +Connector_DIN:DIN41612_M_3x26+2_Male_Horizontal_THT +Connector_DIN:DIN41612_M_3x8+8_Female_Vertical_THT +Connector_DIN:DIN41612_M_3x8+8_Male_Horizontal_THT Connector_DIN:DIN41612_Q2_2x16_Female_Horizontal_THT Connector_DIN:DIN41612_Q2_2x16_Male_Vertical_THT Connector_DIN:DIN41612_Q3_2x10_Female_Horizontal_THT @@ -1428,6 +1493,120 @@ Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm Connector_Dsub:DSUB-9_Socket_Vertical_P2.77x2.84mm Connector_Dsub:DSUB-9_Socket_Vertical_P2.77x2.84mm_MountingHoles +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11004_1x04-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11005_1x05-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11006_1x06-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11007_1x07-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11008_1x08-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11009_1x09-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11010_1x10-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11011_1x11-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11012_1x12-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11013_1x13-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11014_1x14-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11015_1x15-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11016_1x16-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11017_1x17-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11018_1x18-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11019_1x19-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11020_1x20-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11021_1x21-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11022_1x22-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11023_1x23-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11024_1x24-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11025_1x25-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11026_1x26-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11027_1x27-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11028_1x28-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11029_1x29-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11030_1x30-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11031_1x31-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11032_1x32-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11033_1x33-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11034_1x34-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11035_1x35-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11036_1x36-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11037_1x37-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11038_1x38-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11039_1x39-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11040_1x40-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11041_1x41-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11042_1x42-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11043_1x43-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11044_1x44-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11045_1x45-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11046_1x46-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11047_1x47-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11048_1x48-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11049_1x49-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11050_1x50-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11051_1x51-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11052_1x52-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11053_1x53-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11054_1x54-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11055_1x55-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11056_1x56-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11057_1x57-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11058_1x58-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11059_1x59-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32Q-1A7x1-11060_1x60-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11004_1x04-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11005_1x05-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11006_1x06-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11007_1x07-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11008_1x08-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11009_1x09-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11010_1x10-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11011_1x11-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11012_1x12-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11013_1x13-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11014_1x14-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11015_1x15-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11016_1x16-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11017_1x17-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11018_1x18-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11019_1x19-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11020_1x20-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11021_1x21-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11022_1x22-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11023_1x23-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11024_1x24-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11025_1x25-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11026_1x26-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11027_1x27-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11028_1x28-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11029_1x29-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11030_1x30-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11031_1x31-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11032_1x32-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11033_1x33-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11034_1x34-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11035_1x35-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11036_1x36-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11037_1x37-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11038_1x38-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11039_1x39-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11040_1x40-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11041_1x41-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11042_1x42-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11043_1x43-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11044_1x44-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11045_1x45-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11046_1x46-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11047_1x47-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11048_1x48-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11049_1x49-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11050_1x50-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11051_1x51-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11052_1x52-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11053_1x53-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11054_1x54-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11055_1x55-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11056_1x56-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11057_1x57-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11058_1x58-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11059_1x59-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Amphenol_F32R-1A7x1-11060_1x60-1MP_P0.5mm_Horizontal Connector_FFC-FPC:Hirose_FH12-10S-0.5SH_1x10-1MP_P0.50mm_Horizontal Connector_FFC-FPC:Hirose_FH12-11S-0.5SH_1x11-1MP_P0.50mm_Horizontal Connector_FFC-FPC:Hirose_FH12-12S-0.5SH_1x12-1MP_P0.50mm_Horizontal @@ -1593,6 +1772,16 @@ Connector_FFC-FPC:TE_1-84953-6_1x16-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_1-84953-7_1x17-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_1-84953-8_1x18-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_1-84953-9_1x19-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84982-0_2Rows-10Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-1_2Rows-11Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-2_2Rows-12Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-3_2Rows-13Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-4_2Rows-14Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-5_2Rows-15Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-6_2Rows-16Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-7_2Rows-17Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-8_2Rows-18Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_1-84982-9_2Rows-19Pins-P1.0mm_Vertical Connector_FFC-FPC:TE_2-1734839-0_1x20-1MP_P0.5mm_Horizontal Connector_FFC-FPC:TE_2-1734839-1_1x21-1MP_P0.5mm_Horizontal Connector_FFC-FPC:TE_2-1734839-2_1x22-1MP_P0.5mm_Horizontal @@ -1623,6 +1812,16 @@ Connector_FFC-FPC:TE_2-84953-6_1x26-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_2-84953-7_1x27-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_2-84953-8_1x28-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_2-84953-9_1x29-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84982-0_2Rows-20Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-1_2Rows-21Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-2_2Rows-22Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-3_2Rows-23Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-4_2Rows-24Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-5_2Rows-25Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-6_2Rows-26Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-7_2Rows-27Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-8_2Rows-28Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_2-84982-9_2Rows-29Pins-P1.0mm_Vertical Connector_FFC-FPC:TE_3-1734839-0_1x30-1MP_P0.5mm_Horizontal Connector_FFC-FPC:TE_3-1734839-1_1x31-1MP_P0.5mm_Horizontal Connector_FFC-FPC:TE_3-1734839-2_1x32-1MP_P0.5mm_Horizontal @@ -1635,6 +1834,7 @@ Connector_FFC-FPC:TE_3-1734839-8_1x38-1MP_P0.5mm_Horizontal Connector_FFC-FPC:TE_3-1734839-9_1x39-1MP_P0.5mm_Horizontal Connector_FFC-FPC:TE_3-84952-0_1x30-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_3-84953-0_1x30-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_3-84982-0_2Rows-30Pins-P1.0mm_Vertical Connector_FFC-FPC:TE_4-1734839-0_1x40-1MP_P0.5mm_Horizontal Connector_FFC-FPC:TE_4-1734839-1_1x41-1MP_P0.5mm_Horizontal Connector_FFC-FPC:TE_4-1734839-2_1x42-1MP_P0.5mm_Horizontal @@ -1658,6 +1858,12 @@ Connector_FFC-FPC:TE_84953-6_1x06-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_84953-7_1x07-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_84953-8_1x08-1MP_P1.0mm_Horizontal Connector_FFC-FPC:TE_84953-9_1x09-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84982-4_2Rows-04Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_84982-5_2Rows-05Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_84982-6_2Rows-06Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_84982-7_2Rows-07Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_84982-8_2Rows-08Pins-P1.0mm_Vertical +Connector_FFC-FPC:TE_84982-9_2Rows-09Pins-P1.0mm_Vertical Connector_FFC-FPC:Wuerth_68611214422_1x12-1MP_P1.0mm_Horizontal Connector_Harting:Harting_har-flexicon_14110213001xxx_1x02-MP_P2.54mm_Vertical Connector_Harting:Harting_har-flexicon_14110213002xxx_1x02-MP_P2.54mm_Horizontal @@ -1716,6 +1922,14 @@ Connector_Harwin:Harwin_Gecko-G125-MVX3405L0X_2x17_P1.25mm_Vertical Connector_Harwin:Harwin_Gecko-G125-MVX3405L1X_2x17_P1.25mm_Vertical Connector_Harwin:Harwin_Gecko-G125-MVX5005L0X_2x25_P1.25mm_Vertical Connector_Harwin:Harwin_Gecko-G125-MVX5005L1X_2x25_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko_G125-MS10605M2P_2x03_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko_G125-MS11005M2P_2x05_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko_G125-MS11205M2P_2x06_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko_G125-MS11605M2P_2x08_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko_G125-MS12005M2P_2x10_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko_G125-MS12605M2P_2x13_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko_G125-MS13405M2P_2x17_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko_G125-MS15005M2P_2x25_P1.25mm_Vertical Connector_Harwin:Harwin_LTek-Male_02_P2.00mm_Vertical Connector_Harwin:Harwin_LTek-Male_02_P2.00mm_Vertical_StrainRelief Connector_Harwin:Harwin_LTek-Male_03_P2.00mm_Vertical @@ -1865,19 +2079,33 @@ Connector_Hirose:Hirose_DF13C_CL535-0412-6-51_1x12-1MP_P1.25mm_Vertical Connector_Hirose:Hirose_DF13C_CL535-0414-1-51_1x14-1MP_P1.25mm_Vertical Connector_Hirose:Hirose_DF13C_CL535-0415-4-51_1x15-1MP_P1.25mm_Vertical Connector_Hirose:Hirose_DF3EA-02P-2H_1x02-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-02P-2V_1x02-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-03P-2H_1x03-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-03P-2V_1x03-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-04P-2H_1x04-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-04P-2V_1x04-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-05P-2H_1x05-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-05P-2V_1x05-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-06P-2H_1x06-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-06P-2V_1x06-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-07P-2H_1x07-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-07P-2V_1x07-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-08P-2H_1x08-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-08P-2V_1x08-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-09P-2H_1x09-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-09P-2V_1x09-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-10P-2H_1x10-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-10P-2V_1x10-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-11P-2H_1x11-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-11P-2V_1x11-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-12P-2H_1x12-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-12P-2V_1x12-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-13P-2H_1x13-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-13P-2V_1x13-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-14P-2H_1x14-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-14P-2V_1x14-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF3EA-15P-2H_1x15-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-15P-2V_1x15-1MP_P2.00mm_Vertical Connector_Hirose:Hirose_DF52-10S-0.8H_1x10-1MP_P0.80mm_Horizontal Connector_Hirose:Hirose_DF52-11S-0.8H_1x11-1MP_P0.80mm_Horizontal Connector_Hirose:Hirose_DF52-12S-0.8H_1x12-1MP_P0.80mm_Horizontal @@ -1891,6 +2119,12 @@ Connector_Hirose:Hirose_DF52-6S-0.8H_1x06-1MP_P0.80mm_Horizontal Connector_Hirose:Hirose_DF52-7S-0.8H_1x07-1MP_P0.80mm_Horizontal Connector_Hirose:Hirose_DF52-8S-0.8H_1x08-1MP_P0.80mm_Horizontal Connector_Hirose:Hirose_DF52-9S-0.8H_1x09-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF57H-2P-1.2V_1x02_P1.2mm_Socket +Connector_Hirose:Hirose_DF57H-2P-2.4V_1x02_P2.4mm_Socket +Connector_Hirose:Hirose_DF57H-3P-1.2V_1x03_P1.2mm_Socket +Connector_Hirose:Hirose_DF57H-4P-1.2V_1x04_P1.2mm_Socket +Connector_Hirose:Hirose_DF57H-5P-1.2V_1x05_P1.2mm_Socket +Connector_Hirose:Hirose_DF57H-6P-1.2V_1x06_P1.2mm_Socket Connector_Hirose:Hirose_DF63-5P-3.96DSA_1x05_P3.96mm_Vertical Connector_Hirose:Hirose_DF63-6P-3.96DSA_1x06_P3.96mm_Vertical Connector_Hirose:Hirose_DF63M-1P-3.96DSA_1x01_P3.96mm_Vertical @@ -1902,6 +2136,92 @@ Connector_Hirose:Hirose_DF63R-2P-3.96DSA_1x02_P3.96mm_Vertical Connector_Hirose:Hirose_DF63R-3P-3.96DSA_1x03_P3.96mm_Vertical Connector_Hirose:Hirose_DF63R-4P-3.96DSA_1x04_P3.96mm_Vertical Connector_Hirose:Hirose_DF63R-5P-3.96DSA_1x05_P3.96mm_Vertical +Connector_Hirose:Hirose_FX10A-120P-SV_2x60-1MP_P0.5mm +Connector_Hirose:Hirose_FX10A-120S-SV_2x60-1MP_P0.5mm +Connector_Hirose:Hirose_FX10A-144P-SV_2x72-1MP_P0.5mm +Connector_Hirose:Hirose_FX10A-144S-SV_2x72-1MP_P0.5mm +Connector_Hirose:Hirose_FX10A-168P-SV_2x84-1MP_P0.5mm +Connector_Hirose:Hirose_FX10A-168S-SV_2x84-1MP_P0.5mm +Connector_Hirose:Hirose_FX10A-96P-SV_2x48-1MP_P0.5mm +Connector_Hirose:Hirose_FX10A-96S-SV_2x48-1MP_P0.5mm +Connector_Hirose:Hirose_FX10B-120P-SV_2x60-1MP_P0.5mm +Connector_Hirose:Hirose_FX10B-120S-SV_2x60-1MP_P0.5mm +Connector_Hirose:Hirose_FX10B-144P-SV_2x72-1MP_P0.5mm +Connector_Hirose:Hirose_FX10B-144S-SV_2x72-1MP_P0.5mm +Connector_Hirose:Hirose_FX10B-168P-SV_2x84-1MP_P0.5mm +Connector_Hirose:Hirose_FX10B-168S-SV_2x84-1MP_P0.5mm +Connector_Hirose:Hirose_FX10B-96P-SV_2x48-1MP_P0.5mm +Connector_Hirose:Hirose_FX10B-96S-SV_2x48-1MP_P0.5mm +Connector_Hirose_DF40:Hirose_DF40B(2.0)-12DS-0.4V_2x06-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40B(2.0)-80DS-0.4V_2x40-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40B-10DS-0.4V_2x05-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40B-12DS-0.4V_2x06-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40B-30DS-0.4V_2x15-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40B-50DS-0.4V_2x25-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40B-60DS-0.4V_2x30-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40B-80DS-0.4V_2x40-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C(2.0)-20DS-0.4V_2x10_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C(2.0)-24DS-0.4V_2x12_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C(2.0)-30DS-0.4V_2x15_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C(2.0)-40DS-0.4V_2x20_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C(2.0)-50DS-0.4V_2x25_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C(2.0)-60DS-0.4V_2x30_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C(2.0)-70DS-0.4V_2x35_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C(2.0)-80DS-0.4V_2x40_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-100DP-0.4V_2x50-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-100DS-0.4V_2x50_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-10DP-0.4V_2x05-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-120DP-0.4V_2x60-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-120DS-0.4V_2x60_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-12DP-0.4V_2x06-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-20DP-0.4V_2x10-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-20DS-0.4V_2x10_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-24DP-0.4V_2x12-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-24DS-0.4V_2x12_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-30DP-0.4V_2x15-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-30DS-0.4V_2x15_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-34DP-0.4V_2x17-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-34DS-0.4V_2x17_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-40DP-0.4V_2x20-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-40DS-0.4V_2x20_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-44DP-0.4V_2x22-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-50DP-0.4V_2x25-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-50DS-0.4V_2x25_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-60DP-0.4V_2x30-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-60DS-0.4V_2x30_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-70DP-0.4V_2x35-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-70DS-0.4V_2x35_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-80DP-0.4V_2x40-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-80DS-0.4V_2x40_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-90DP-0.4V_2x45-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40C-90DS-0.4V_2x45_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HB(2.5)-10DS-0.4V_2x05-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HB(4.0)-50DS-0.4V_2x25-1MP_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(2.5)-20DS-0.4V_2x10_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(2.5)-30DS-0.4V_2x15_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(2.5)-40DS-0.4V_2x20_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(2.5)-50DS-0.4V_2x25_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(2.5)-60DS-0.4V_2x30_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-100DS-0.4V_2x50_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-30DS-0.4V_2x15_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-40DS-0.4V_2x20_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-44DS-0.4V_2x22_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-50DS-0.4V_2x25_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-60DS-0.4V_2x30_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-70DS-0.4V_2x35_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-80DS-0.4V_2x40_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.0)-90DS-0.4V_2x45_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.5)-20DS-0.4V_2x10_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.5)-30DS-0.4V_2x15_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.5)-40DS-0.4V_2x20_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.5)-50DS-0.4V_2x25_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.5)-60DS-0.4V_2x30_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(3.5)-80DS-0.4V_2x40_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(4.0)-40DS-0.4V_2x20_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(4.0)-50DS-0.4V_2x25_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(4.0)-60DS-0.4V_2x30_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(4.0)-80DS-0.4V_2x40_P0.4mm +Connector_Hirose_DF40:Hirose_DF40HC(4.0)-90DS-0.4V_2x45_P0.4mm Connector_Hirose_FX8:Hirose_FX8-100P-SV_2x50_P0.6mm Connector_Hirose_FX8:Hirose_FX8-100S-SV_2x50_P0.6mm Connector_Hirose_FX8:Hirose_FX8-120P-SV_2x60_P0.6mm @@ -2366,6 +2686,18 @@ Connector_JST:JST_PUD_S36B-PUDSS-1_2x18_P2.00mm_Horizontal Connector_JST:JST_PUD_S38B-PUDSS-1_2x19_P2.00mm_Horizontal Connector_JST:JST_PUD_S40B-PUDSS-1_2x20_P2.00mm_Horizontal Connector_JST:JST_SFH_SM02B-SFHRS-TF_1x02-1MP_P4.20mm_Horizontal +Connector_JST:JST_SHD_BM20B-SRDS-A-G-TF_2x10-1MP_P1.0mm_Vertical +Connector_JST:JST_SHD_BM20B-SRDS-G-TF_2x10-1MP_P1.0mm_Vertical +Connector_JST:JST_SHD_BM30B-SRDS-A-G-TF_2x15-1MP_P1.0mm_Vertical +Connector_JST:JST_SHD_BM30B-SRDS-G-TF_2x15-1MP_P1.0mm_Vertical +Connector_JST:JST_SHD_BM40B-SRDS-A-G-TF_2x20-1MP_P1.0mm_Vertical +Connector_JST:JST_SHD_BM40B-SRDS-G-TF_2x20-1MP_P1.0mm_Vertical +Connector_JST:JST_SHD_BM50B-SRDS-A-G-TF_2x25-1MP_P1.0mm_Vertical +Connector_JST:JST_SHD_BM50B-SRDS-G-TF_2x25-1MP_P1.0mm_Vertical +Connector_JST:JST_SHD_SM20B-SRDS-G-TF_2x10-1MP_P1.0mm_Horizontal +Connector_JST:JST_SHD_SM30B-SRDS-G-TF_2x15-1MP_P1.0mm_Horizontal +Connector_JST:JST_SHD_SM40B-SRDS-G-TF_2x20-1MP_P1.0mm_Horizontal +Connector_JST:JST_SHD_SM50B-SRDS-G-TF_2x25-1MP_P1.0mm_Horizontal Connector_JST:JST_SHL_SM02B-SHLS-TF_1x02-1MP_P1.00mm_Horizontal Connector_JST:JST_SHL_SM05B-SHLS-TF_1x05-1MP_P1.00mm_Horizontal Connector_JST:JST_SHL_SM06B-SHLS-TF_1x06-1MP_P1.00mm_Horizontal @@ -2784,6 +3116,20 @@ Connector_Molex:Molex_CLIK-Mate_505405-1270_1x12-1MP_P1.50mm_Vertical Connector_Molex:Molex_CLIK-Mate_505405-1370_1x13-1MP_P1.50mm_Vertical Connector_Molex:Molex_CLIK-Mate_505405-1470_1x14-1MP_P1.50mm_Vertical Connector_Molex:Molex_CLIK-Mate_505405-1570_1x15-1MP_P1.50mm_Vertical +Connector_Molex:Molex_DuraClik_502352-0200_1x02-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-0300_1x03-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-0400_1x04-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-0500_1x05-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-0600_1x06-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-0700_1x07-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-0800_1x08-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-0900_1x09-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-1000_1x10-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-1100_1x11-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-1200_1x12-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-1300_1x13-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-1400_1x14-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_DuraClik_502352-1500_1x15-1MP_P2.00mm_Horizontal Connector_Molex:Molex_KK-254_AE-6410-02A_1x02_P2.54mm_Vertical Connector_Molex:Molex_KK-254_AE-6410-03A_1x03_P2.54mm_Vertical Connector_Molex:Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical @@ -2937,66 +3283,77 @@ Connector_Molex:Molex_Micro-Fit_3.0_43045-2421_2x12-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0200_1x02_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0210_1x02-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0210_1x02-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0212_1x02-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0215_1x02_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0221_1x02_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0224_1x02-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0300_1x03_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0310_1x03-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0310_1x03-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0312_1x03-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0315_1x03_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0321_1x03_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0324_1x03-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0400_1x04_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0410_1x04-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0410_1x04-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0412_1x04-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0415_1x04_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0421_1x04_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0424_1x04-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0500_1x05_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0510_1x05-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0510_1x05-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0512_1x05-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0515_1x05_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0521_1x05_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0524_1x05-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0600_1x06_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0610_1x06-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0610_1x06-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0612_1x06-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0615_1x06_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0621_1x06_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0624_1x06-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0700_1x07_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0710_1x07-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0710_1x07-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0712_1x07-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0715_1x07_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0721_1x07_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0724_1x07-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0800_1x08_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0810_1x08-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0810_1x08-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0812_1x08-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0815_1x08_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0821_1x08_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0824_1x08-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0900_1x09_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0910_1x09-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0910_1x09-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0912_1x09-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-0915_1x09_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0921_1x09_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-0924_1x09-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1000_1x10_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1010_1x10-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1010_1x10-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-1012_1x10-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1015_1x10_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1021_1x10_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1024_1x10-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1100_1x11_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1110_1x11-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1110_1x11-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-1112_1x11-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1115_1x11_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1121_1x11_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1124_1x11-1MP_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1200_1x12_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1210_1x12-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1210_1x12-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-1212_1x12-1MP_P3.00mm_Horizontal Connector_Molex:Molex_Micro-Fit_3.0_43650-1215_1x12_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1221_1x12_P3.00mm_Vertical Connector_Molex:Molex_Micro-Fit_3.0_43650-1224_1x12-1MP_P3.00mm_Vertical @@ -3335,6 +3692,58 @@ Connector_Molex:Molex_Picoflex_90814-0020_2x10_P1.27mm_Vertical Connector_Molex:Molex_Picoflex_90814-0022_2x11_P1.27mm_Vertical Connector_Molex:Molex_Picoflex_90814-0024_2x12_P1.27mm_Vertical Connector_Molex:Molex_Picoflex_90814-0026_2x13_P1.27mm_Vertical +Connector_Molex:Molex_SL_171971-0002_1x02_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0003_1x03_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0004_1x04_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0005_1x05_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0006_1x06_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0007_1x07_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0008_1x08_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0009_1x09_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0010_1x10_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0011_1x11_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0012_1x12_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0013_1x13_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0014_1x14_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0015_1x15_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0016_1x16_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0017_1x17_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0018_1x18_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0019_1x19_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0020_1x20_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0021_1x21_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0022_1x22_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0023_1x23_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0024_1x24_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0025_1x25_P2.54mm_Vertical +Connector_Molex:Molex_SPOX_5267-02A_1x02_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-03A_1x03_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-04A_1x04_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-05A_1x05_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-06A_1x06_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-07A_1x07_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-08A_1x08_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-09A_1x09_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-10A_1x10_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-11A_1x11_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-12A_1x12_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-13A_1x13_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-14A_1x14_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-15A_1x15_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5268-02A_1x02_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-03A_1x03_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-04A_1x04_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-05A_1x05_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-06A_1x06_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-07A_1x07_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-08A_1x08_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-09A_1x09_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-10A_1x10_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-11A_1x11_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-12A_1x12_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-13A_1x13_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-14A_1x14_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-15A_1x15_P2.50mm_Horizontal Connector_Molex:Molex_Sabre_43160-0102_1x02_P7.49mm_Vertical Connector_Molex:Molex_Sabre_43160-0102_1x02_P7.49mm_Vertical_ThermalVias Connector_Molex:Molex_Sabre_43160-0103_1x03_P7.49mm_Vertical @@ -3441,75 +3850,98 @@ Connector_Molex:Molex_SlimStack_55560-0401_2x20_P0.50mm_Vertical Connector_Molex:Molex_SlimStack_55560-0501_2x25_P0.50mm_Vertical Connector_Molex:Molex_SlimStack_55560-0601_2x30_P0.50mm_Vertical Connector_Molex:Molex_SlimStack_55560-0801_2x40_P0.50mm_Vertical -Connector_Molex:Molex_SL_171971-0002_1x02_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0003_1x03_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0004_1x04_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0005_1x05_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0006_1x06_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0007_1x07_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0008_1x08_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0009_1x09_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0010_1x10_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0011_1x11_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0012_1x12_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0013_1x13_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0014_1x14_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0015_1x15_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0016_1x16_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0017_1x17_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0018_1x18_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0019_1x19_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0020_1x20_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0021_1x21_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0022_1x22_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0023_1x23_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0024_1x24_P2.54mm_Vertical -Connector_Molex:Molex_SL_171971-0025_1x25_P2.54mm_Vertical -Connector_Molex:Molex_SPOX_5267-02A_1x02_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-03A_1x03_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-04A_1x04_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-05A_1x05_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-06A_1x06_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-07A_1x07_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-08A_1x08_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-09A_1x09_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-10A_1x10_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-11A_1x11_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-12A_1x12_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-13A_1x13_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-14A_1x14_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5267-15A_1x15_P2.50mm_Vertical -Connector_Molex:Molex_SPOX_5268-02A_1x02_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-03A_1x03_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-04A_1x04_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-05A_1x05_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-06A_1x06_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-07A_1x07_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-08A_1x08_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-09A_1x09_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-10A_1x10_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-11A_1x11_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-12A_1x12_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-13A_1x13_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-14A_1x14_P2.50mm_Horizontal -Connector_Molex:Molex_SPOX_5268-15A_1x15_P2.50mm_Horizontal +Connector_Molex_Milligrid:Molex_8783204xx_2x02_P2.0mm_Header_Vertical +Connector_Molex_Milligrid:Molex_8783206xx_2x03_P2.0mm_Header_Vertical +Connector_Molex_Milligrid:Molex_8783206xx_2x03_P2.0mm_Header_Vertical_MountingPegs +Connector_Molex_Milligrid:Molex_8783206xx_2x03_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783206xx_2x03_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783208xx_2x04_P2.0mm_Header_Vertical +Connector_Molex_Milligrid:Molex_8783208xx_2x04_P2.0mm_Header_Vertical_MountingPegs +Connector_Molex_Milligrid:Molex_8783208xx_2x04_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783208xx_2x04_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783210xx_2x05_P2.0mm_Header_Vertical +Connector_Molex_Milligrid:Molex_8783210xx_2x05_P2.0mm_Header_Vertical_MountingPegs +Connector_Molex_Milligrid:Molex_8783210xx_2x05_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783210xx_2x05_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783212xx_2x06_P2.0mm_Header_Vertical +Connector_Molex_Milligrid:Molex_8783212xx_2x06_P2.0mm_Header_Vertical_MountingPegs +Connector_Molex_Milligrid:Molex_8783212xx_2x06_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783212xx_2x06_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783214xx_2x07_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783214xx_2x07_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783216xx_2x08_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783216xx_2x08_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783218xx_2x09_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783218xx_2x09_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783220xx_2x10_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783220xx_2x10_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783222xx_2x11_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783222xx_2x11_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783224xx_2x12_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783224xx_2x12_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783226xx_2x13_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783226xx_2x13_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783228xx_2x14_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783228xx_2x14_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783230xx_2x15_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783230xx_2x15_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783232xx_2x16_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783232xx_2x16_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783234xx_2x17_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783234xx_2x17_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783236xx_2x18_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783236xx_2x18_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783238xx_2x19_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783238xx_2x19_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783240xx_2x20_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783240xx_2x20_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783242xx_2x21_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783242xx_2x21_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783244xx_2x22_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783244xx_2x22_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783246xx_2x23_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783246xx_2x23_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783248xx_2x24_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783248xx_2x24_P2.0mm_Header_Vertical_Polarized_MountingPegs +Connector_Molex_Milligrid:Molex_8783250xx_2x25_P2.0mm_Header_Vertical_Polarized +Connector_Molex_Milligrid:Molex_8783250xx_2x25_P2.0mm_Header_Vertical_Polarized_MountingPegs Connector_PCBEdge:4UCON_10156_2x40_P1.27mm_Socket_Horizontal Connector_PCBEdge:BUS_AT Connector_PCBEdge:BUS_PCI -Connector_PCBEdge:BUS_PCIexpress_x1 -Connector_PCBEdge:BUS_PCIexpress_x16 -Connector_PCBEdge:BUS_PCIexpress_x4 -Connector_PCBEdge:BUS_PCIexpress_x8 Connector_PCBEdge:BUS_PCI_Express_Mini Connector_PCBEdge:BUS_PCI_Express_Mini_Dual Connector_PCBEdge:BUS_PCI_Express_Mini_Full Connector_PCBEdge:BUS_PCI_Express_Mini_Half +Connector_PCBEdge:BUS_PCIexpress_x1 +Connector_PCBEdge:BUS_PCIexpress_x16 +Connector_PCBEdge:BUS_PCIexpress_x4 +Connector_PCBEdge:BUS_PCIexpress_x8 +Connector_PCBEdge:DEC_double_long +Connector_PCBEdge:DEC_double_short +Connector_PCBEdge:DEC_quad_long +Connector_PCBEdge:DEC_quad_short +Connector_PCBEdge:DEC_single_long +Connector_PCBEdge:DEC_single_short Connector_PCBEdge:JAE_MM60-EZH039-Bx_BUS_PCI_Express_Holder Connector_PCBEdge:JAE_MM60-EZH059-Bx_BUS_PCI_Express_Holder -Connector_PCBEdge:molex_EDGELOCK_2-CKT -Connector_PCBEdge:molex_EDGELOCK_4-CKT -Connector_PCBEdge:molex_EDGELOCK_6-CKT -Connector_PCBEdge:molex_EDGELOCK_8-CKT +Connector_PCBEdge:M.2_22110-xx-B +Connector_PCBEdge:M.2_22110-xx-M +Connector_PCBEdge:M.2_2230-xx-A +Connector_PCBEdge:M.2_2230-xx-B +Connector_PCBEdge:M.2_2230-xx-E +Connector_PCBEdge:M.2_2230-xx-M +Connector_PCBEdge:M.2_2242-xx-B +Connector_PCBEdge:M.2_2242-xx-M +Connector_PCBEdge:M.2_2260-xx-B +Connector_PCBEdge:M.2_2260-xx-M +Connector_PCBEdge:M.2_2280-xx-B +Connector_PCBEdge:M.2_2280-xx-M +Connector_PCBEdge:M.2_3030-xx-A +Connector_PCBEdge:M.2_3030-xx-E +Connector_PCBEdge:M.2_3042-xx-B +Connector_PCBEdge:SODIMM-200_1.8V_Card_edge +Connector_PCBEdge:SODIMM-200_2.5V_Card_edge +Connector_PCBEdge:SODIMM-260_DDR4_H4.0-5.2_OrientationStd_Socket Connector_PCBEdge:Samtec_MECF-05-01-L-DV-WT_2x05_P1.27mm_Polarized_Socket_Horizontal Connector_PCBEdge:Samtec_MECF-05-01-L-DV_2x05_P1.27mm_Polarized_Socket_Horizontal Connector_PCBEdge:Samtec_MECF-05-01-NP-L-DV-WT_2x05_P1.27mm_Socket_Horizontal @@ -3590,9 +4022,10 @@ Connector_PCBEdge:Samtec_MECF-70-02-NP-L-DV-WT_2x70_P1.27mm_Socket_Horizontal Connector_PCBEdge:Samtec_MECF-70-02-NP-L-DV_2x70_P1.27mm_Socket_Horizontal Connector_PCBEdge:Samtec_MECF-70-0_-L-DV_2x70_P1.27mm_Polarized_Edge Connector_PCBEdge:Samtec_MECF-70-0_-NP-L-DV_2x70_P1.27mm_Edge -Connector_PCBEdge:SODIMM-200_1.8V_Card_edge -Connector_PCBEdge:SODIMM-200_2.5V_Card_edge -Connector_PCBEdge:SODIMM-260_DDR4_H4.0-5.2_OrientationStd_Socket +Connector_PCBEdge:molex_EDGELOCK_2-CKT +Connector_PCBEdge:molex_EDGELOCK_4-CKT +Connector_PCBEdge:molex_EDGELOCK_6-CKT +Connector_PCBEdge:molex_EDGELOCK_8-CKT Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_10-G-7,62_1x10_P7.62mm_Horizontal Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_10-G_1x10_P7.50mm_Horizontal Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_11-G-7,62_1x11_P7.62mm_Horizontal @@ -6264,14 +6697,14 @@ Connector_RJ:RJ14_Connfly_DS1133-S4_Horizontal Connector_RJ:RJ25_Wayconn_MJEA-660X1_Horizontal Connector_RJ:RJ45_Abracon_ARJP11A-MA_Horizontal Connector_RJ:RJ45_Amphenol_54602-x08_Horizontal -Connector_RJ:RJ45_Amphenol_RJHSE5380-08 Connector_RJ:RJ45_Amphenol_RJHSE5380 +Connector_RJ:RJ45_Amphenol_RJHSE5380-08 +Connector_RJ:RJ45_Amphenol_RJHSE538X Connector_RJ:RJ45_Amphenol_RJHSE538X-02 Connector_RJ:RJ45_Amphenol_RJHSE538X-04 -Connector_RJ:RJ45_Amphenol_RJHSE538X Connector_RJ:RJ45_Amphenol_RJMG1BD3B8K1ANR -Connector_RJ:RJ45_Bel_SI-60062-F Connector_RJ:RJ45_BEL_SS74301-00x_Vertical +Connector_RJ:RJ45_Bel_SI-60062-F Connector_RJ:RJ45_Bel_V895-1001-AW_Vertical Connector_RJ:RJ45_Cetus_J1B1211CCD_Horizontal Connector_RJ:RJ45_Connfly_DS1128-09-S8xx-S_Horizontal @@ -6297,6 +6730,8 @@ Connector_RJ:RJ45_Wuerth_7499010211A_Horizontal Connector_RJ:RJ45_Wuerth_7499111446_Horizontal Connector_RJ:RJ45_Wuerth_7499151120_Horizontal Connector_RJ:RJ9_Evercom_5301-440xxx_Horizontal +Connector_SATA_SAS:SAS-mini_TEConnectivity_1888174_Vertical +Connector_SATA_SAS:SATA_Amphenol_10029364-001LF_Horizontal Connector_Samtec:Samtec_FMC_ASP-134486-01_10x40_P1.27mm_Vertical Connector_Samtec:Samtec_FMC_ASP-134602-01_10x40_P1.27mm_Vertical Connector_Samtec:Samtec_FMC_ASP-134604-01_4x40_Vertical @@ -6312,6 +6747,168 @@ Connector_Samtec:Samtec_LSHM-140-xx.x-x-DV-N_2x40_P0.50mm_Vertical Connector_Samtec:Samtec_LSHM-140-xx.x-x-DV-S_2x40-1SH_P0.50mm_Vertical Connector_Samtec:Samtec_LSHM-150-xx.x-x-DV-N_2x50_P0.50mm_Vertical Connector_Samtec:Samtec_LSHM-150-xx.x-x-DV-S_2x50-1SH_P0.50mm_Vertical +Connector_Samtec:Samtec_SS4-10-3.00-x-D-K-xR_2x10_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-10-3.50-x-D-K-xR_2x10_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-15-3.00-x-D-K-xR_2x15_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-15-3.50-x-D-K-xR_2x15_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-20-3.00-x-D-K-xR_2x20_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-20-3.50-x-D-K-xR_2x20_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-30-3.00-x-D-K-xR_2x30_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-30-3.50-x-D-K-xR_2x30_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-40-3.00-x-D-K-xR_2x40_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-40-3.50-x-D-K-xR_2x40_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-50-3.00-x-D-K-xR_2x50_P0.4mm_Socket +Connector_Samtec:Samtec_SS4-50-3.50-x-D-K-xR_2x50_P0.4mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-005-XX.X-DV-L_2x05_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-005-XX.X-DV_2x05_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-010-XX.X-DV-L_2x10_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-010-XX.X-DV_2x10_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-011-XX.X-DV-L_2x11_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-011-XX.X-DV_2x11_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-013-XX.X-DV-L_2x13_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-013-XX.X-DV_2x13_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-020-XX.X-DV-L_2x20_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-020-XX.X-DV_2x20_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-025-XX.X-DV-L_2x25_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-025-XX.X-DV_2x25_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-030-XX.X-DV-L_2x30_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-030-XX.X-DV_2x30_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-035-XX.X-DV-L_2x35_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-035-XX.X-DV_2x35_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-040-XX.X-DV-L_2x40_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-040-XX.X-DV_2x40_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-049-XX.X-DV-L_2x49_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-049-XX.X-DV_2x49_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-050-XX.X-DV-L_2x50_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-050-XX.X-DV_2x50_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-060-XX.X-DV-L_2x60_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-060-XX.X-DV_2x60_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-070-XX.X-DV-L_2x70_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-070-XX.X-DV_2x70_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-075-XX.X-DV-L_2x75_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-075-XX.X-DV_2x75_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERF8-100-XX.X-DV-L_2x100_P0.8mm_Socket_Latch +Connector_Samtec_EdgeRate:Samtec_ERF8-100-XX.X-DV_2x100_P0.8mm_Socket +Connector_Samtec_EdgeRate:Samtec_ERM8-005-XX.X-DV-L_2x05_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-005-XX.X-DV_2x05_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-010-XX.X-DV-L_2x10_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-010-XX.X-DV_2x10_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-011-XX.X-DV-L_2x11_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-011-XX.X-DV_2x11_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-013-XX.X-DV-L_2x13_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-013-XX.X-DV_2x13_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-020-XX.X-DV-L_2x20_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-020-XX.X-DV_2x20_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-025-XX.X-DV-L_2x25_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-025-XX.X-DV_2x25_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-030-XX.X-DV-L_2x30_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-030-XX.X-DV_2x30_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-035-XX.X-DV-L_2x35_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-035-XX.X-DV_2x35_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-040-XX.X-DV-L_2x40_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-040-XX.X-DV_2x40_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-049-XX.X-DV-L_2x49_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-049-XX.X-DV_2x49_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-050-XX.X-DV-L_2x50_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-050-XX.X-DV_2x50_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-060-XX.X-DV-L_2x60_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-060-XX.X-DV_2x60_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-070-XX.X-DV-L_2x70_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-070-XX.X-DV_2x70_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-075-XX.X-DV-L_2x75_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-075-XX.X-DV_2x75_P0.8mm_Terminal +Connector_Samtec_EdgeRate:Samtec_ERM8-100-XX.X-DV-L_2x100_P0.8mm_Terminal_Latch +Connector_Samtec_EdgeRate:Samtec_ERM8-100-XX.X-DV_2x100_P0.8mm_Terminal +Connector_Samtec_FSI:Samtec_FSI-105-03-X-D-AD_2x05_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-105-03-X-D_2x05_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-105-XX-X-D-AD_2x05_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-105-XX-X-D-AD_2x05_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-105-XX-X-D_2x05_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-105-XX-X-D_2x05_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-110-03-X-D-AD_2x10_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-110-03-X-D-M-AD_2x10_P1.0mm_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-110-03-X-D-M_2x10_P1.0mm_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-110-03-X-D_2x10_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-110-XX-X-D-AD_2x10_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-110-XX-X-D-AD_2x10_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-110-XX-X-D-M-AD_2x10_P1.0mm_Mate_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-110-XX-X-D-M-AD_2x10_P1.0mm_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-110-XX-X-D-M_2x10_P1.0mm_Mate_Threaded +Connector_Samtec_FSI:Samtec_FSI-110-XX-X-D-M_2x10_P1.0mm_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-110-XX-X-D_2x10_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-110-XX-X-D_2x10_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-115-03-X-D-AD_2x15_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-115-03-X-D_2x15_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-115-XX-X-D-AD_2x15_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-115-XX-X-D-AD_2x15_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-115-XX-X-D_2x15_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-115-XX-X-D_2x15_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-120-03-X-D-AD_2x20_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-120-03-X-D-M-AD_2x20_P1.0mm_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-120-03-X-D-M_2x20_P1.0mm_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-120-03-X-D_2x20_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-120-XX-X-D-AD_2x20_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-120-XX-X-D-AD_2x20_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-120-XX-X-D-M-AD_2x20_P1.0mm_Mate_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-120-XX-X-D-M-AD_2x20_P1.0mm_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-120-XX-X-D-M_2x20_P1.0mm_Mate_Threaded +Connector_Samtec_FSI:Samtec_FSI-120-XX-X-D-M_2x20_P1.0mm_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-120-XX-X-D_2x20_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-120-XX-X-D_2x20_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-125-03-X-D-AD_2x25_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-125-03-X-D_2x25_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-125-XX-X-D-AD_2x25_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-125-XX-X-D-AD_2x25_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-125-XX-X-D_2x25_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-125-XX-X-D_2x25_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-130-03-X-D-AD_2x30_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-130-03-X-D-M-AD_2x30_P1.0mm_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-130-03-X-D-M_2x30_P1.0mm_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-130-03-X-D_2x30_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-130-XX-X-D-AD_2x30_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-130-XX-X-D-AD_2x30_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-130-XX-X-D-M-AD_2x30_P1.0mm_Mate_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-130-XX-X-D-M-AD_2x30_P1.0mm_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-130-XX-X-D-M_2x30_P1.0mm_Mate_Threaded +Connector_Samtec_FSI:Samtec_FSI-130-XX-X-D-M_2x30_P1.0mm_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-130-XX-X-D_2x30_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-130-XX-X-D_2x30_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-135-03-X-D-AD_2x35_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-135-03-X-D_2x35_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-135-XX-X-D-AD_2x35_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-135-XX-X-D-AD_2x35_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-135-XX-X-D_2x35_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-135-XX-X-D_2x35_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-140-03-X-D-AD_2x40_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-140-03-X-D-M-AD_2x40_P1.0mm_Pol20_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-140-03-X-D-M_2x40_P1.0mm_Pol20_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-140-03-X-D_2x40_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-140-XX-X-D-AD_2x40_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-140-XX-X-D-AD_2x40_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-140-XX-X-D-M-AD_2x40_P1.0mm_Pol20_Mate_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-140-XX-X-D-M-AD_2x40_P1.0mm_Pol20_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-140-XX-X-D-M_2x40_P1.0mm_Pol20_Mate_Threaded +Connector_Samtec_FSI:Samtec_FSI-140-XX-X-D-M_2x40_P1.0mm_Pol20_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-140-XX-X-D_2x40_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-140-XX-X-D_2x40_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-145-03-X-D-AD_2x45_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-145-03-X-D_2x45_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-145-XX-X-D-AD_2x45_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-145-XX-X-D-AD_2x45_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-145-XX-X-D_2x45_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-145-XX-X-D_2x45_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-150-03-X-D-AD_2x50_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-150-03-X-D-M-AD_2x50_P1.0mm_Pol25_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-150-03-X-D-M_2x50_P1.0mm_Pol25_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-150-03-X-D_2x50_P1.0mm_Socket +Connector_Samtec_FSI:Samtec_FSI-150-XX-X-D-AD_2x50_P1.0mm_Mate_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-150-XX-X-D-AD_2x50_P1.0mm_Socket_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-150-XX-X-D-M-AD_2x50_P1.0mm_Pol25_Mate_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-150-XX-X-D-M-AD_2x50_P1.0mm_Pol25_Socket_Threaded_AlignmentPin +Connector_Samtec_FSI:Samtec_FSI-150-XX-X-D-M_2x50_P1.0mm_Pol25_Mate_Threaded +Connector_Samtec_FSI:Samtec_FSI-150-XX-X-D-M_2x50_P1.0mm_Pol25_Socket_Threaded +Connector_Samtec_FSI:Samtec_FSI-150-XX-X-D_2x50_P1.0mm_Mate +Connector_Samtec_FSI:Samtec_FSI-150-XX-X-D_2x50_P1.0mm_Socket Connector_Samtec_HLE_SMD:Samtec_HLE-102-02-xxx-DV-BE-LC_2x02_P2.54mm_Horizontal Connector_Samtec_HLE_SMD:Samtec_HLE-102-02-xxx-DV-BE_2x02_P2.54mm_Horizontal Connector_Samtec_HLE_SMD:Samtec_HLE-102-02-xxx-DV-LC_2x02_P2.54mm_Horizontal @@ -6914,6 +7511,32 @@ Connector_Samtec_HSEC8:Samtec_HSEC8-190-03-X-DV_2x90_P0.8mm_Pol32_Socket Connector_Samtec_HSEC8:Samtec_HSEC8-190-X-X-DV-BL_2x90_P0.8mm_Edge Connector_Samtec_HSEC8:Samtec_HSEC8-190-X-X-DV_2x90_P0.8mm_Edge Connector_Samtec_HSEC8:Samtec_HSEC8-190-X-X-DV_2x90_P0.8mm_Wing_Edge +Connector_Samtec_LTMM:Samtec_LTMM-104-02-X-D-SM-LC_2x04_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-104-02-X-D-SM_2x04_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-105-02-X-D-SM-LC_2x05_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-105-02-X-D-SM_2x05_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-106-02-X-D-SM-LC_2x06_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-106-02-X-D-SM_2x06_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-107-02-X-D-SM-LC_2x07_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-107-02-X-D-SM_2x07_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-108-02-X-D-SM-LC_2x08_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-108-02-X-D-SM_2x08_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-110-02-X-D-SM-LC_2x10_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-110-02-X-D-SM_2x10_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-112-02-X-D-SM-LC_2x12_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-112-02-X-D-SM_2x12_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-113-02-X-D-SM-LC_2x13_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-113-02-X-D-SM_2x13_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-115-02-X-D-SM-LC_2x15_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-115-02-X-D-SM_2x15_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-117-02-X-D-SM-LC_2x17_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-117-02-X-D-SM_2x17_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-120-02-X-D-SM-LC_2x20_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-120-02-X-D-SM_2x20_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-122-02-X-D-SM-LC_2x22_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-122-02-X-D-SM_2x22_P2.0mm_Header_Vertical +Connector_Samtec_LTMM:Samtec_LTMM-125-02-X-D-SM-LC_2x25_P2.0mm_Header_Vertical_LockingClip +Connector_Samtec_LTMM:Samtec_LTMM-125-02-X-D-SM_2x25_P2.0mm_Header_Vertical Connector_Samtec_MicroMate:Samtec_T1M-02-X-S-RA_1x02-1MP_P1.0mm_Terminal_Horizontal Connector_Samtec_MicroMate:Samtec_T1M-02-X-S-V_1x02-1MP_P1.0mm_Terminal_Vertical Connector_Samtec_MicroMate:Samtec_T1M-02-X-SH-L_1x02-1MP_P1.0mm_Terminal_Horizontal_Latch @@ -7026,8 +7649,6 @@ Connector_Samtec_MicroPower:Samtec_UMPT-09-XX.X-X-V-S-W_1x09-1MP_P2.0mm_Terminal Connector_Samtec_MicroPower:Samtec_UMPT-09-XX.X-X-V-S_1x09_P2.0mm_Terminal Connector_Samtec_MicroPower:Samtec_UMPT-10-XX.X-X-V-S-W_1x10-1MP_P2.0mm_Terminal_WeldTab Connector_Samtec_MicroPower:Samtec_UMPT-10-XX.X-X-V-S_1x10_P2.0mm_Terminal -Connector_SATA_SAS:SAS-mini_TEConnectivity_1888174_Vertical -Connector_SATA_SAS:SATA_Amphenol_10029364-001LF_Horizontal Connector_Stocko:Stocko_MKS_1651-6-0-202_1x2_P2.50mm_Vertical Connector_Stocko:Stocko_MKS_1652-6-0-202_1x2_P2.50mm_Vertical Connector_Stocko:Stocko_MKS_1653-6-0-303_1x3_P2.50mm_Vertical @@ -7119,9 +7740,9 @@ Connector_USB:USB3_A_Plug_Wuerth_692112030100_Horizontal Connector_USB:USB3_A_Receptacle_Wuerth_692122030100 Connector_USB:USB3_Micro-B_Connfly_DS1104-01 Connector_USB:USB_A_CNCTech_1001-011-01101_Horizontal +Connector_USB:USB_A_CUI_UJ2-ADH-TH_Horizontal_Stacked Connector_USB:USB_A_Connfly_DS1095 Connector_USB:USB_A_Connfly_DS1098_Horizontal -Connector_USB:USB_A_CUI_UJ2-ADH-TH_Horizontal_Stacked Connector_USB:USB_A_Kycon_KUSBX-AS1N-B_Horizontal Connector_USB:USB_A_Molex_105057_Vertical Connector_USB:USB_A_Molex_48037-2200_Horizontal @@ -7191,12 +7812,12 @@ Connector_USB:USB_Mini-B_Wuerth_65100516121_Horizontal Connector_Video:DVI-D_Molex_74320-4004_Horizontal Connector_Video:DVI-I_Molex_74320-1004_Horizontal Connector_Video:HDMI_A_Amphenol_10029449-x01xLF_Horizontal -Connector_Video:HDMI_A_Contact_Technology_HDMI-19APL2_Horizontal +Connector_Video:HDMI_A_Contact_Technology_19APL2_Horizontal Connector_Video:HDMI_A_Kycon_KDMIX-SL1-NS-WS-B15_VerticalRightAngle Connector_Video:HDMI_A_Molex_208658-1001_Horizontal -Connector_Video:HDMI_Micro-D_Molex_46765-0x01 -Connector_Video:HDMI_Micro-D_Molex_46765-1x01 -Connector_Video:HDMI_Micro-D_Molex_46765-2x0x +Connector_Video:HDMI_Micro-D_Molex_46765-0xxx +Connector_Video:HDMI_Micro-D_Molex_46765-1xxx +Connector_Video:HDMI_Micro-D_Molex_46765-2xxx Connector_Wago:Wago_734-132_1x02_P3.50mm_Vertical Connector_Wago:Wago_734-133_1x03_P3.50mm_Vertical Connector_Wago:Wago_734-134_1x04_P3.50mm_Vertical @@ -7519,7 +8140,47 @@ Connector_Wire:SolderWire-2sqmm_1x05_P7.8mm_D2mm_OD3.9mm_Relief2x Connector_Wire:SolderWire-2sqmm_1x06_P7.8mm_D2mm_OD3.9mm Connector_Wire:SolderWire-2sqmm_1x06_P7.8mm_D2mm_OD3.9mm_Relief Connector_Wire:SolderWire-2sqmm_1x06_P7.8mm_D2mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-4sqmm_1x01_D3mm_OD6mm +Connector_Wire:SolderWire-4sqmm_1x01_D3mm_OD6mm_Relief +Connector_Wire:SolderWire-4sqmm_1x01_D3mm_OD6mm_Relief2x +Connector_Wire:SolderWire-4sqmm_1x02_P12mm_D3mm_OD6mm +Connector_Wire:SolderWire-4sqmm_1x02_P12mm_D3mm_OD6mm_Relief +Connector_Wire:SolderWire-4sqmm_1x02_P12mm_D3mm_OD6mm_Relief2x +Connector_Wire:SolderWire-4sqmm_1x03_P12mm_D3mm_OD6mm +Connector_Wire:SolderWire-4sqmm_1x03_P12mm_D3mm_OD6mm_Relief +Connector_Wire:SolderWire-4sqmm_1x03_P12mm_D3mm_OD6mm_Relief2x +Connector_Wire:SolderWire-4sqmm_1x04_P12mm_D3mm_OD6mm +Connector_Wire:SolderWire-4sqmm_1x04_P12mm_D3mm_OD6mm_Relief +Connector_Wire:SolderWire-4sqmm_1x04_P12mm_D3mm_OD6mm_Relief2x +Connector_Wire:SolderWire-4sqmm_1x05_P12mm_D3mm_OD6mm +Connector_Wire:SolderWire-4sqmm_1x05_P12mm_D3mm_OD6mm_Relief +Connector_Wire:SolderWire-4sqmm_1x05_P12mm_D3mm_OD6mm_Relief2x +Connector_Wire:SolderWire-4sqmm_1x06_P12mm_D3mm_OD6mm +Connector_Wire:SolderWire-4sqmm_1x06_P12mm_D3mm_OD6mm_Relief +Connector_Wire:SolderWire-4sqmm_1x06_P12mm_D3mm_OD6mm_Relief2x +Connector_Wire:SolderWire-6sqmm_1x01_D3.5mm_OD7mm +Connector_Wire:SolderWire-6sqmm_1x01_D3.5mm_OD7mm_Relief +Connector_Wire:SolderWire-6sqmm_1x01_D3.5mm_OD7mm_Relief2x +Connector_Wire:SolderWire-6sqmm_1x02_P14mm_D3.5mm_OD7mm +Connector_Wire:SolderWire-6sqmm_1x02_P14mm_D3.5mm_OD7mm_Relief +Connector_Wire:SolderWire-6sqmm_1x02_P14mm_D3.5mm_OD7mm_Relief2x +Connector_Wire:SolderWire-6sqmm_1x03_P14mm_D3.5mm_OD7mm +Connector_Wire:SolderWire-6sqmm_1x03_P14mm_D3.5mm_OD7mm_Relief +Connector_Wire:SolderWire-6sqmm_1x03_P14mm_D3.5mm_OD7mm_Relief2x +Connector_Wire:SolderWire-6sqmm_1x04_P14mm_D3.5mm_OD7mm +Connector_Wire:SolderWire-6sqmm_1x04_P14mm_D3.5mm_OD7mm_Relief +Connector_Wire:SolderWire-6sqmm_1x04_P14mm_D3.5mm_OD7mm_Relief2x +Connector_Wire:SolderWire-6sqmm_1x05_P14mm_D3.5mm_OD7mm +Connector_Wire:SolderWire-6sqmm_1x05_P14mm_D3.5mm_OD7mm_Relief +Connector_Wire:SolderWire-6sqmm_1x05_P14mm_D3.5mm_OD7mm_Relief2x +Connector_Wire:SolderWire-6sqmm_1x06_P14mm_D3.5mm_OD7mm +Connector_Wire:SolderWire-6sqmm_1x06_P14mm_D3.5mm_OD7mm_Relief +Connector_Wire:SolderWire-6sqmm_1x06_P14mm_D3.5mm_OD7mm_Relief2x +Connector_Wire:SolderWirePad_1x01_SMD_1.5x3mm Connector_Wire:SolderWirePad_1x01_SMD_1x2mm +Connector_Wire:SolderWirePad_1x01_SMD_2x4mm +Connector_Wire:SolderWirePad_1x01_SMD_3x6mm +Connector_Wire:SolderWirePad_1x01_SMD_4x8mm Connector_Wire:SolderWirePad_1x01_SMD_5x10mm Connector_Wuerth:Wuerth_WR-PHD_610004243021_SMD_2x02_P2.54mm_Vertical Connector_Wuerth:Wuerth_WR-PHD_610006243021_SMD_2x03_P2.54mm_Vertical @@ -7567,6 +8228,15 @@ Connector_Wuerth:Wuerth_WR-WTB_64800711622_1x07_P1.50mm_Vertical Connector_Wuerth:Wuerth_WR-WTB_64800811622_1x08_P1.50mm_Vertical Connector_Wuerth:Wuerth_WR-WTB_64800911622_1x09_P1.50mm_Vertical Connector_Wuerth:Wuerth_WR-WTB_64801011622_1x10_P1.50mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-10PLT_1x10-1MP_P3.96mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-2PLT_1x02-1MP_P3.96mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-3PLT_1x03-1MP_P3.96mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-4PLT_1x04-1MP_P3.96mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-5PLT_1x05-1MP_P3.96mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-6PLT_1x06-1MP_P3.96mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-7PLT_1x07-1MP_P3.96mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-8PLT_1x08-1MP_P3.96mm_Vertical +Connector_Zhaoxing:Zhaoxing_VH_ZX-VH3.96-9PLT_1x09-1MP_P3.96mm_Vertical Converter_ACDC:Converter_ACDC_CUI_PBO-3-Sxx_THT_Vertical Converter_ACDC:Converter_ACDC_Hahn_HS-400xx_THT Converter_ACDC:Converter_ACDC_Hi-Link_HLK-10Mxx @@ -7596,12 +8266,17 @@ Converter_ACDC:Converter_ACDC_TRACO_TMF_051xx_THT Converter_ACDC:Converter_ACDC_TRACO_TMF_101xx_THT Converter_ACDC:Converter_ACDC_TRACO_TMF_201xx_THT Converter_ACDC:Converter_ACDC_TRACO_TMF_301xx_THT -Converter_ACDC:Converter_ACDC_TRACO_TMG-15_THT +Converter_ACDC:Converter_ACDC_TRACO_TMG_071xx_THT +Converter_ACDC:Converter_ACDC_TRACO_TMG_151xx_THT Converter_ACDC:Converter_ACDC_TRACO_TMLM-04_THT Converter_ACDC:Converter_ACDC_TRACO_TMLM-05_THT Converter_ACDC:Converter_ACDC_TRACO_TMLM-10-20_THT +Converter_ACDC:Converter_ACDC_TRACO_TMPW10_1xx_THT +Converter_ACDC:Converter_ACDC_TRACO_TMPW25_1xx_THT +Converter_ACDC:Converter_ACDC_TRACO_TMPW5-1xx_THT Converter_ACDC:Converter_ACDC_TRACO_TPP-15-1xx-D_THT -Converter_ACDC:Converter_ACDC_Vigortronix_VTX-214-010-xxx_THT +Converter_ACDC:Converter_ACDC_Vigortronix_VTX-214-010-1xx_THT +Converter_ACDC:Converter_ACDC_Vigortronix_VTX-214-010-2xx_THT Converter_ACDC:Converter_ACDC_Vigortronix_VTX-214-015-1xx_THT Converter_ACDC:Converter_ACDC_ZETTLER_ZPI03Sxx00WC_THT Converter_DCDC:Converter_DCDC_Artesyn_ATA_SMD @@ -7614,6 +8289,7 @@ Converter_DCDC:Converter_DCDC_Cincon_EC6Cxx_Dual-Triple_THT Converter_DCDC:Converter_DCDC_Cincon_EC6Cxx_Single_THT Converter_DCDC:Converter_DCDC_Cyntec_MUN12AD01-SH Converter_DCDC:Converter_DCDC_Cyntec_MUN12AD03-SH +Converter_DCDC:Converter_DCDC_Hamamatsu_C11204-1_THT Converter_DCDC:Converter_DCDC_MeanWell_NID30_THT Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT Converter_DCDC:Converter_DCDC_MeanWell_NSD10_THT @@ -7622,8 +8298,6 @@ Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxSC_THT Converter_DCDC:Converter_DCDC_Murata_MEE1SxxxxSC_THT Converter_DCDC:Converter_DCDC_Murata_MEE3SxxxxSC_THT -Converter_DCDC:Converter_DCDC_muRata_MEJ1DxxxxSC_THT -Converter_DCDC:Converter_DCDC_muRata_MEJ1SxxxxSC_THT Converter_DCDC:Converter_DCDC_Murata_MGJ2DxxxxxxSC_THT Converter_DCDC:Converter_DCDC_Murata_MGJ3 Converter_DCDC:Converter_DCDC_Murata_MYRxP @@ -7648,6 +8322,7 @@ Converter_DCDC:Converter_DCDC_Silvertel_Ag5810 Converter_DCDC:Converter_DCDC_Silvertel_Ag99xxLP_THT Converter_DCDC:Converter_DCDC_TRACO_TBA1-xxxxE_Dual_THT Converter_DCDC:Converter_DCDC_TRACO_TBA1-xxxxE_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TBA1-xxxx_THT Converter_DCDC:Converter_DCDC_TRACO_TBA2-xxxx_Dual_THT Converter_DCDC:Converter_DCDC_TRACO_TBA2-xxxx_Single_THT Converter_DCDC:Converter_DCDC_TRACO_TDN_5-xxxxWISM_SMD @@ -7656,6 +8331,7 @@ Converter_DCDC:Converter_DCDC_TRACO_TDU1-xxxx_THT Converter_DCDC:Converter_DCDC_TRACO_TEA1-xxxxE_THT Converter_DCDC:Converter_DCDC_TRACO_TEA1-xxxxHI_THT Converter_DCDC:Converter_DCDC_TRACO_TEA1-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TEC2-12xxWI_24xxWI_48xxWI_THT Converter_DCDC:Converter_DCDC_TRACO_TEC3-24xxUI_THT Converter_DCDC:Converter_DCDC_TRACO_TEL12-xxxx_THT Converter_DCDC:Converter_DCDC_TRACO_TEN10-110xxWIRH_THT @@ -7665,10 +8341,17 @@ Converter_DCDC:Converter_DCDC_TRACO_TEN10-xxxx_THT Converter_DCDC:Converter_DCDC_TRACO_TEN20-110xxWIRH_THT Converter_DCDC:Converter_DCDC_TRACO_TEN20-xxxx-N4_THT Converter_DCDC:Converter_DCDC_TRACO_TEN20-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN30-xxxxUIR_THT Converter_DCDC:Converter_DCDC_TRACO_TEN40-110xxWIRH_THT +Converter_DCDC:Converter_DCDC_TRACO_TEP150-721xUIR_THT +Converter_DCDC:Converter_DCDC_TRACO_TES1-051x_121x_241x_Single_SMD +Converter_DCDC:Converter_DCDC_TRACO_TES1-052x_122x_242x_Dual_SMD Converter_DCDC:Converter_DCDC_TRACO_THB10-xxxx_Dual_THT Converter_DCDC:Converter_DCDC_TRACO_THB10-xxxx_Single_THT Converter_DCDC:Converter_DCDC_TRACO_THD_15-xxxxWIN_THT +Converter_DCDC:Converter_DCDC_TRACO_THL30-xxxxWI_THT +Converter_DCDC:Converter_DCDC_TRACO_THL40-xxxxWI_THT +Converter_DCDC:Converter_DCDC_TRACO_THN10-xxxxUIR_THT Converter_DCDC:Converter_DCDC_TRACO_THN30-xxxx_THT Converter_DCDC:Converter_DCDC_TRACO_THR40-72xxWI_THT Converter_DCDC:Converter_DCDC_TRACO_TMA-05xxD_12xxD_Dual_THT @@ -7680,18 +8363,31 @@ Converter_DCDC:Converter_DCDC_TRACO_TME_24xxS_Single_THT Converter_DCDC:Converter_DCDC_TRACO_TMR-1-xxxx_Dual_THT Converter_DCDC:Converter_DCDC_TRACO_TMR-1-xxxx_Single_THT Converter_DCDC:Converter_DCDC_TRACO_TMR-1SM_SMD -Converter_DCDC:Converter_DCDC_TRACO_TMR-2xxxxWI_THT -Converter_DCDC:Converter_DCDC_TRACO_TMR-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TMR10-24xxWIR_48xxWIR_72xxWIR_THT +Converter_DCDC:Converter_DCDC_TRACO_TMR2-xxxxWI_THT Converter_DCDC:Converter_DCDC_TRACO_TMR4-xxxxWI_THT +Converter_DCDC:Converter_DCDC_TRACO_TMR8-xxxxWI_THT Converter_DCDC:Converter_DCDC_TRACO_TMU3-05xx_12xx_THT Converter_DCDC:Converter_DCDC_TRACO_TMU3-24xx_THT +Converter_DCDC:Converter_DCDC_TRACO_TMV-051xD_121xD_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TMV-051xS_121xS_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TMV-241xD_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TMV-241xS_Single_THT Converter_DCDC:Converter_DCDC_TRACO_TOS06-05SIL_THT Converter_DCDC:Converter_DCDC_TRACO_TOS06-12SIL_THT Converter_DCDC:Converter_DCDC_TRACO_TRA3-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TRI1-xxxxSM_SMD Converter_DCDC:Converter_DCDC_TRACO_TRI1-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TRI10_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TRI10_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TRN3-xx1x_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TRN3-xx2x_Dual_THT Converter_DCDC:Converter_DCDC_TRACO_TSR-1_THT +Converter_DCDC:Converter_DCDC_TRACO_TSR0.5-24xxSM_241xxSM_SMD +Converter_DCDC:Converter_DCDC_TRACO_TSR0.5-24xx_TSR0.5-24xxx_THT Converter_DCDC:Converter_DCDC_TRACO_TSR0.6-48xxWI_TSR0.6-48xxxWI_THT Converter_DCDC:Converter_DCDC_TRACO_TSR1-xxxxE_THT +Converter_DCDC:Converter_DCDC_TRACO_TSR1.5-24xxE_24120E_THT Converter_DCDC:Converter_DCDC_TRACO_TSR2-24xxN_TSR2-24xxxN_THT Converter_DCDC:Converter_DCDC_TRACO_TSR2-xxxx_THT Converter_DCDC:Converter_DCDC_XP_POWER-IA48xxD_THT @@ -7708,6 +8404,8 @@ Converter_DCDC:Converter_DCDC_XP_POWER-ITXxxxxSA_THT Converter_DCDC:Converter_DCDC_XP_POWER-ITxxxxxS_THT Converter_DCDC:Converter_DCDC_XP_POWER_JTDxxxxxxx_THT Converter_DCDC:Converter_DCDC_XP_POWER_JTExxxxDxx_THT +Converter_DCDC:Converter_DCDC_muRata_MEJ1DxxxxSC_THT +Converter_DCDC:Converter_DCDC_muRata_MEJ1SxxxxSC_THT Crystal:Crystal_AT310_D3.0mm_L10.0mm_Horizontal Crystal:Crystal_AT310_D3.0mm_L10.0mm_Horizontal_1EP_style1 Crystal:Crystal_AT310_D3.0mm_L10.0mm_Horizontal_1EP_style2 @@ -7777,6 +8475,7 @@ Crystal:Crystal_SMD_0603-2Pin_6.0x3.5mm_HandSoldering Crystal:Crystal_SMD_0603-4Pin_6.0x3.5mm Crystal:Crystal_SMD_0603-4Pin_6.0x3.5mm_HandSoldering Crystal:Crystal_SMD_1210-4Pin_1.2x1.0mm +Crystal:Crystal_SMD_1210-4Pin_1.2x1.0mm_RotB Crystal:Crystal_SMD_2012-2Pin_2.0x1.2mm Crystal:Crystal_SMD_2012-2Pin_2.0x1.2mm_HandSoldering Crystal:Crystal_SMD_2016-4Pin_2.0x1.6mm @@ -7799,6 +8498,7 @@ Crystal:Crystal_SMD_Abracon_ABM7-2Pin_6.0x3.5mm Crystal:Crystal_SMD_Abracon_ABM8AIG-4Pin_3.2x2.5mm Crystal:Crystal_SMD_Abracon_ABM8G-4Pin_3.2x2.5mm Crystal:Crystal_SMD_Abracon_ABS25-4Pin_8.0x3.8mm +Crystal:Crystal_SMD_Citizen_CS325S-4Pin_3.2x2.5mm Crystal:Crystal_SMD_ECS_CSM3X-2Pin_7.6x4.1mm Crystal:Crystal_SMD_EuroQuartz_EQ161-2Pin_3.2x1.5mm Crystal:Crystal_SMD_EuroQuartz_EQ161-2Pin_3.2x1.5mm_HandSoldering @@ -7866,6 +8566,13 @@ Crystal:Crystal_SMD_TXC_7M-4Pin_3.2x2.5mm_HandSoldering Crystal:Crystal_SMD_TXC_9HT11-2Pin_2.0x1.2mm Crystal:Crystal_SMD_TXC_9HT11-2Pin_2.0x1.2mm_HandSoldering Crystal:Crystal_SMD_TXC_AX_8045-2Pin_8.0x4.5mm +Crystal:Crystal_SMD_WE_12SMX-4Pin_7.0x5.0mm +Crystal:Crystal_SMD_WE_CFPX-104-4Pin_5.0x3.2mm +Crystal:Crystal_SMD_WE_CFPX-180-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_WE_CFPX-218-4Pin_2.5x2.0mm +Crystal:Crystal_SMD_WE_IQXC-240-4Pin_1.2x1.0mm +Crystal:Crystal_SMD_WE_IQXC-26-4Pin_1.6x1.2mm +Crystal:Crystal_SMD_WE_IQXC-42-4Pin_2.0x1.6mm Crystal:Resonator-2Pin_W10.0mm_H5.0mm Crystal:Resonator-2Pin_W6.0mm_H3.0mm Crystal:Resonator-2Pin_W7.0mm_H2.5mm @@ -7891,14 +8598,6 @@ Crystal:Resonator_SMD_Murata_SFSKA-3Pin_7.9x3.8mm Crystal:Resonator_SMD_Murata_SFSKA-3Pin_7.9x3.8mm_HandSoldering Crystal:Resonator_SMD_Murata_TPSKA-3Pin_7.9x3.8mm Crystal:Resonator_SMD_Murata_TPSKA-3Pin_7.9x3.8mm_HandSoldering -Diode_SMD:Diode_Bridge_Bourns_CD-DF4xxS -Diode_SMD:Diode_Bridge_Diotec_ABS -Diode_SMD:Diode_Bridge_Diotec_MicroDil_3.0x3.0x1.8mm -Diode_SMD:Diode_Bridge_Diotec_SO-DIL-Slim -Diode_SMD:Diode_Bridge_OnSemi_SDIP-4L -Diode_SMD:Diode_Bridge_Vishay_DFS -Diode_SMD:Diode_Bridge_Vishay_DFSFlat -Diode_SMD:Diode_Bridge_Vishay_MBLS Diode_SMD:D_01005_0402Metric Diode_SMD:D_01005_0402Metric_Pad0.57x0.30mm_HandSolder Diode_SMD:D_0201_0603Metric @@ -7923,34 +8622,34 @@ Diode_SMD:D_2512_6332Metric Diode_SMD:D_2512_6332Metric_Pad1.52x3.35mm_HandSolder Diode_SMD:D_3220_8050Metric Diode_SMD:D_3220_8050Metric_Pad2.65x5.15mm_HandSolder -Diode_SMD:D_MELF-RM10_Universal_Handsoldering Diode_SMD:D_MELF +Diode_SMD:D_MELF-RM10_Universal_Handsoldering Diode_SMD:D_MELF_Handsoldering Diode_SMD:D_MicroMELF Diode_SMD:D_MicroMELF_Handsoldering -Diode_SMD:D_MicroSMP_AK -Diode_SMD:D_MicroSMP_KA +Diode_SMD:D_MicroSMP_LargeAnode +Diode_SMD:D_MicroSMP_LargeCathode Diode_SMD:D_MiniMELF Diode_SMD:D_MiniMELF_Handsoldering Diode_SMD:D_PowerDI-123 Diode_SMD:D_PowerDI-5 -Diode_SMD:D_Powermite2_AK -Diode_SMD:D_Powermite2_KA +Diode_SMD:D_Powermite2_LargeAnode +Diode_SMD:D_Powermite2_LargeCathode Diode_SMD:D_Powermite3 -Diode_SMD:D_Powermite_AK -Diode_SMD:D_Powermite_KA +Diode_SMD:D_Powermite_LargeAnode +Diode_SMD:D_Powermite_LargeCathode Diode_SMD:D_QFN_3.3x3.3mm_P0.65mm Diode_SMD:D_SC-80 Diode_SMD:D_SC-80_HandSoldering -Diode_SMD:D_SMA-SMB_Universal_Handsoldering Diode_SMD:D_SMA +Diode_SMD:D_SMA-SMB_Universal_Handsoldering Diode_SMD:D_SMA_Handsoldering -Diode_SMD:D_SMB-SMC_Universal_Handsoldering Diode_SMD:D_SMB +Diode_SMD:D_SMB-SMC_Universal_Handsoldering Diode_SMD:D_SMB_Handsoldering Diode_SMD:D_SMB_Modified -Diode_SMD:D_SMC-RM10_Universal_Handsoldering Diode_SMD:D_SMC +Diode_SMD:D_SMC-RM10_Universal_Handsoldering Diode_SMD:D_SMC_Handsoldering Diode_SMD:D_SMF Diode_SMD:D_SMP_DO-220AA @@ -7966,15 +8665,108 @@ Diode_SMD:D_SOD-882 Diode_SMD:D_SOD-882D Diode_SMD:D_SOD-923 Diode_SMD:D_TUMD2 +Diode_SMD:Diode_Bridge_Bourns_CD-DF4xxS +Diode_SMD:Diode_Bridge_Diotec_ABS +Diode_SMD:Diode_Bridge_Diotec_MicroDil_3.0x3.0x1.8mm +Diode_SMD:Diode_Bridge_Diotec_SO-DIL-Slim +Diode_SMD:Diode_Bridge_OnSemi_SDIP-4L +Diode_SMD:Diode_Bridge_Vishay_DFS +Diode_SMD:Diode_Bridge_Vishay_DFSFlat +Diode_SMD:Diode_Bridge_Vishay_MBLS Diode_SMD:Infineon_SG-WLL-2-3_0.58x0.28_P0.36mm Diode_SMD:Littelfuse_PolyZen-LS Diode_SMD:Nexperia_CFP3_SOD-123W Diode_SMD:Nexperia_DSN0603-2_0.6x0.3mm_P0.4mm Diode_SMD:Nexperia_DSN1608-2_1.6x0.8mm -Diode_SMD:OnSemi_751EP_SOIC-4_3.9x4.725mm_P2.54mm Diode_SMD:ST_D_SMC Diode_SMD:ST_QFN-2L_1.6x1.0mm Diode_SMD:Vishay_SMPA +Diode_THT:D_5KPW_P12.70mm_Horizontal +Diode_THT:D_5KPW_P7.62mm_Vertical_AnodeUp +Diode_THT:D_5KPW_P7.62mm_Vertical_CathodeUp +Diode_THT:D_5KP_P10.16mm_Horizontal +Diode_THT:D_5KP_P12.70mm_Horizontal +Diode_THT:D_5KP_P7.62mm_Vertical_AnodeUp +Diode_THT:D_5KP_P7.62mm_Vertical_CathodeUp +Diode_THT:D_5W_P10.16mm_Horizontal +Diode_THT:D_5W_P12.70mm_Horizontal +Diode_THT:D_5W_P5.08mm_Vertical_AnodeUp +Diode_THT:D_5W_P5.08mm_Vertical_CathodeUp +Diode_THT:D_A-405_P10.16mm_Horizontal +Diode_THT:D_A-405_P12.70mm_Horizontal +Diode_THT:D_A-405_P2.54mm_Vertical_AnodeUp +Diode_THT:D_A-405_P2.54mm_Vertical_CathodeUp +Diode_THT:D_A-405_P5.08mm_Vertical_AnodeUp +Diode_THT:D_A-405_P5.08mm_Vertical_CathodeUp +Diode_THT:D_A-405_P7.62mm_Horizontal +Diode_THT:D_DO-15_P10.16mm_Horizontal +Diode_THT:D_DO-15_P12.70mm_Horizontal +Diode_THT:D_DO-15_P15.24mm_Horizontal +Diode_THT:D_DO-15_P2.54mm_Vertical_AnodeUp +Diode_THT:D_DO-15_P2.54mm_Vertical_CathodeUp +Diode_THT:D_DO-15_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-15_P3.81mm_Vertical_CathodeUp +Diode_THT:D_DO-15_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-15_P5.08mm_Vertical_CathodeUp +Diode_THT:D_DO-201AD_P12.70mm_Horizontal +Diode_THT:D_DO-201AD_P15.24mm_Horizontal +Diode_THT:D_DO-201AD_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-201AD_P3.81mm_Vertical_CathodeUp +Diode_THT:D_DO-201AD_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-201AD_P5.08mm_Vertical_CathodeUp +Diode_THT:D_DO-201AE_P12.70mm_Horizontal +Diode_THT:D_DO-201AE_P15.24mm_Horizontal +Diode_THT:D_DO-201AE_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-201AE_P3.81mm_Vertical_CathodeUp +Diode_THT:D_DO-201AE_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-201AE_P5.08mm_Vertical_CathodeUp +Diode_THT:D_DO-201_P12.70mm_Horizontal +Diode_THT:D_DO-201_P15.24mm_Horizontal +Diode_THT:D_DO-201_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-201_P3.81mm_Vertical_CathodeUp +Diode_THT:D_DO-201_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-201_P5.08mm_Vertical_CathodeUp +Diode_THT:D_DO-247_Horizontal_TabDown +Diode_THT:D_DO-247_Horizontal_TabUp +Diode_THT:D_DO-247_Vertical +Diode_THT:D_DO-27_P12.70mm_Horizontal +Diode_THT:D_DO-27_P15.24mm_Horizontal +Diode_THT:D_DO-27_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-27_P5.08mm_Vertical_CathodeUp +Diode_THT:D_DO-34_SOD68_P10.16mm_Horizontal +Diode_THT:D_DO-34_SOD68_P12.70mm_Horizontal +Diode_THT:D_DO-34_SOD68_P2.54mm_Vertical_AnodeUp +Diode_THT:D_DO-34_SOD68_P2.54mm_Vertical_CathodeUp +Diode_THT:D_DO-34_SOD68_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-34_SOD68_P5.08mm_Vertical_CathodeUp +Diode_THT:D_DO-34_SOD68_P7.62mm_Horizontal +Diode_THT:D_DO-35_SOD27_P10.16mm_Horizontal +Diode_THT:D_DO-35_SOD27_P12.70mm_Horizontal +Diode_THT:D_DO-35_SOD27_P2.54mm_Vertical_AnodeUp +Diode_THT:D_DO-35_SOD27_P2.54mm_Vertical_CathodeUp +Diode_THT:D_DO-35_SOD27_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-35_SOD27_P3.81mm_Vertical_CathodeUp +Diode_THT:D_DO-35_SOD27_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-35_SOD27_P5.08mm_Vertical_CathodeUp +Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal +Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal +Diode_THT:D_DO-41_SOD81_P12.70mm_Horizontal +Diode_THT:D_DO-41_SOD81_P2.54mm_Vertical_AnodeUp +Diode_THT:D_DO-41_SOD81_P2.54mm_Vertical_CathodeUp +Diode_THT:D_DO-41_SOD81_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-41_SOD81_P3.81mm_Vertical_CathodeUp +Diode_THT:D_DO-41_SOD81_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-41_SOD81_P5.08mm_Vertical_CathodeUp +Diode_THT:D_DO-41_SOD81_P7.62mm_Horizontal +Diode_THT:D_P600_R-6_P12.70mm_Horizontal +Diode_THT:D_P600_R-6_P20.00mm_Horizontal +Diode_THT:D_P600_R-6_P7.62mm_Vertical_AnodeUp +Diode_THT:D_P600_R-6_P7.62mm_Vertical_CathodeUp +Diode_THT:D_T-1_P10.16mm_Horizontal +Diode_THT:D_T-1_P12.70mm_Horizontal +Diode_THT:D_T-1_P2.54mm_Vertical_AnodeUp +Diode_THT:D_T-1_P2.54mm_Vertical_CathodeUp +Diode_THT:D_T-1_P5.08mm_Horizontal Diode_THT:Diode_Bridge_15.1x15.1x6.3mm_P10.9mm Diode_THT:Diode_Bridge_15.2x15.2x6.3mm_P10.9mm Diode_THT:Diode_Bridge_15.7x15.7x6.3mm_P10.8mm @@ -8000,102 +8792,18 @@ Diode_THT:Diode_Bridge_Vishay_KBPC1 Diode_THT:Diode_Bridge_Vishay_KBPC6 Diode_THT:Diode_Bridge_Vishay_KBPM Diode_THT:Diode_Bridge_Vishay_KBU -Diode_THT:D_5KPW_P12.70mm_Horizontal -Diode_THT:D_5KPW_P7.62mm_Vertical_AnodeUp -Diode_THT:D_5KPW_P7.62mm_Vertical_KathodeUp -Diode_THT:D_5KP_P10.16mm_Horizontal -Diode_THT:D_5KP_P12.70mm_Horizontal -Diode_THT:D_5KP_P7.62mm_Vertical_AnodeUp -Diode_THT:D_5KP_P7.62mm_Vertical_KathodeUp -Diode_THT:D_5W_P10.16mm_Horizontal -Diode_THT:D_5W_P12.70mm_Horizontal -Diode_THT:D_5W_P5.08mm_Vertical_AnodeUp -Diode_THT:D_5W_P5.08mm_Vertical_KathodeUp -Diode_THT:D_A-405_P10.16mm_Horizontal -Diode_THT:D_A-405_P12.70mm_Horizontal -Diode_THT:D_A-405_P2.54mm_Vertical_AnodeUp -Diode_THT:D_A-405_P2.54mm_Vertical_KathodeUp -Diode_THT:D_A-405_P5.08mm_Vertical_AnodeUp -Diode_THT:D_A-405_P5.08mm_Vertical_KathodeUp -Diode_THT:D_A-405_P7.62mm_Horizontal -Diode_THT:D_DO-15_P10.16mm_Horizontal -Diode_THT:D_DO-15_P12.70mm_Horizontal -Diode_THT:D_DO-15_P15.24mm_Horizontal -Diode_THT:D_DO-15_P2.54mm_Vertical_AnodeUp -Diode_THT:D_DO-15_P2.54mm_Vertical_KathodeUp -Diode_THT:D_DO-15_P3.81mm_Vertical_AnodeUp -Diode_THT:D_DO-15_P3.81mm_Vertical_KathodeUp -Diode_THT:D_DO-15_P5.08mm_Vertical_AnodeUp -Diode_THT:D_DO-15_P5.08mm_Vertical_KathodeUp -Diode_THT:D_DO-201AD_P12.70mm_Horizontal -Diode_THT:D_DO-201AD_P15.24mm_Horizontal -Diode_THT:D_DO-201AD_P3.81mm_Vertical_AnodeUp -Diode_THT:D_DO-201AD_P3.81mm_Vertical_KathodeUp -Diode_THT:D_DO-201AD_P5.08mm_Vertical_AnodeUp -Diode_THT:D_DO-201AD_P5.08mm_Vertical_KathodeUp -Diode_THT:D_DO-201AE_P12.70mm_Horizontal -Diode_THT:D_DO-201AE_P15.24mm_Horizontal -Diode_THT:D_DO-201AE_P3.81mm_Vertical_AnodeUp -Diode_THT:D_DO-201AE_P3.81mm_Vertical_KathodeUp -Diode_THT:D_DO-201AE_P5.08mm_Vertical_AnodeUp -Diode_THT:D_DO-201AE_P5.08mm_Vertical_KathodeUp -Diode_THT:D_DO-201_P12.70mm_Horizontal -Diode_THT:D_DO-201_P15.24mm_Horizontal -Diode_THT:D_DO-201_P3.81mm_Vertical_AnodeUp -Diode_THT:D_DO-201_P3.81mm_Vertical_KathodeUp -Diode_THT:D_DO-201_P5.08mm_Vertical_AnodeUp -Diode_THT:D_DO-201_P5.08mm_Vertical_KathodeUp -Diode_THT:D_DO-247_Horizontal_TabDown -Diode_THT:D_DO-247_Horizontal_TabUp -Diode_THT:D_DO-247_Vertical -Diode_THT:D_DO-27_P12.70mm_Horizontal -Diode_THT:D_DO-27_P15.24mm_Horizontal -Diode_THT:D_DO-27_P5.08mm_Vertical_AnodeUp -Diode_THT:D_DO-27_P5.08mm_Vertical_KathodeUp -Diode_THT:D_DO-34_SOD68_P10.16mm_Horizontal -Diode_THT:D_DO-34_SOD68_P12.70mm_Horizontal -Diode_THT:D_DO-34_SOD68_P2.54mm_Vertical_AnodeUp -Diode_THT:D_DO-34_SOD68_P2.54mm_Vertical_KathodeUp -Diode_THT:D_DO-34_SOD68_P5.08mm_Vertical_AnodeUp -Diode_THT:D_DO-34_SOD68_P5.08mm_Vertical_KathodeUp -Diode_THT:D_DO-34_SOD68_P7.62mm_Horizontal -Diode_THT:D_DO-35_SOD27_P10.16mm_Horizontal -Diode_THT:D_DO-35_SOD27_P12.70mm_Horizontal -Diode_THT:D_DO-35_SOD27_P2.54mm_Vertical_AnodeUp -Diode_THT:D_DO-35_SOD27_P2.54mm_Vertical_KathodeUp -Diode_THT:D_DO-35_SOD27_P3.81mm_Vertical_AnodeUp -Diode_THT:D_DO-35_SOD27_P3.81mm_Vertical_KathodeUp -Diode_THT:D_DO-35_SOD27_P5.08mm_Vertical_AnodeUp -Diode_THT:D_DO-35_SOD27_P5.08mm_Vertical_KathodeUp -Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal -Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal -Diode_THT:D_DO-41_SOD81_P12.70mm_Horizontal -Diode_THT:D_DO-41_SOD81_P2.54mm_Vertical_AnodeUp -Diode_THT:D_DO-41_SOD81_P2.54mm_Vertical_KathodeUp -Diode_THT:D_DO-41_SOD81_P3.81mm_Vertical_AnodeUp -Diode_THT:D_DO-41_SOD81_P3.81mm_Vertical_KathodeUp -Diode_THT:D_DO-41_SOD81_P5.08mm_Vertical_AnodeUp -Diode_THT:D_DO-41_SOD81_P5.08mm_Vertical_KathodeUp -Diode_THT:D_DO-41_SOD81_P7.62mm_Horizontal -Diode_THT:D_P600_R-6_P12.70mm_Horizontal -Diode_THT:D_P600_R-6_P20.00mm_Horizontal -Diode_THT:D_P600_R-6_P7.62mm_Vertical_AnodeUp -Diode_THT:D_P600_R-6_P7.62mm_Vertical_KathodeUp -Diode_THT:D_T-1_P10.16mm_Horizontal -Diode_THT:D_T-1_P12.70mm_Horizontal -Diode_THT:D_T-1_P2.54mm_Vertical_AnodeUp -Diode_THT:D_T-1_P2.54mm_Vertical_KathodeUp -Diode_THT:D_T-1_P5.08mm_Horizontal +Display:AG12864E Display:Adafruit_SSD1306 Display:Adafruit_SSD1306_No_Mounting_Holes -Display:AG12864E Display:CR2013-MI2120 +Display:DL1416 Display:EA-eDIP128B-XXX Display:EA_DOGL128-6 Display:EA_DOGM128-6 Display:EA_DOGS104X-A Display:EA_DOGXL160-7 Display:EA_DOGXL160-7_Backlight +Display:EA_T123X-I2C Display:EA_eDIP160-XXX Display:EA_eDIP240-XXX Display:EA_eDIP320X-XXX @@ -8105,9 +8813,9 @@ Display:EA_eDIPTFT43-XXX Display:EA_eDIPTFT57-XXX Display:EA_eDIPTFT70-ATC Display:EA_eDIPTFT70-XXX -Display:EA_T123X-I2C Display:ER-OLED0.42-1W_Folded Display:ERM19264 +Display:ER_OLEDM0.91_1x-I2C Display:HDSM-441B_HDSM-443B Display:HDSM-541B_HDSM-543B Display:HDSP-4830 @@ -8121,9 +8829,10 @@ Display:HY1602E Display:LCD-016N002L Display:LM16255 Display:NHD-0420H1Z -Display:NHD-C0220BiZ-FSRGB Display:NHD-C0220BiZ +Display:NHD-C0220BiZ-FSRGB Display:NHD-C12832A1Z-FSRGB +Display:Noritake_CU20025-Ux1J Display:OLED-128O064D Display:RC1602A Display:WC1602A @@ -8183,23 +8892,25 @@ Display_7Segment:Sx39-1xxxxx Ferrite_THT:LairdTech_28C0236-0JW-10 Fiducial:Fiducial_0.5mm_Mask1.5mm Fiducial:Fiducial_0.5mm_Mask1mm +Fiducial:Fiducial_0.5mm_Mask2mm Fiducial:Fiducial_0.75mm_Mask1.5mm Fiducial:Fiducial_0.75mm_Mask2.25mm Fiducial:Fiducial_1.5mm_Mask3mm Fiducial:Fiducial_1.5mm_Mask4.5mm Fiducial:Fiducial_1mm_Mask2mm Fiducial:Fiducial_1mm_Mask3mm +Fiducial:Fiducial_Cross_1.5mm_Mask2mm Filter:Filter_1109-5_1.1x0.9mm Filter:Filter_1411-5_1.4x1.1mm Filter:Filter_Bourns_SRF0905_6.0x9.2mm Filter:Filter_FILTERCON_1FPxx Filter:Filter_KEMET_PZB300_24.0x12.5mm_P10.0mm +Filter:Filter_Mini-Circuits_FV1206 Filter:Filter_Mini-Circuits_FV1206-1 Filter:Filter_Mini-Circuits_FV1206-4 Filter:Filter_Mini-Circuits_FV1206-5 Filter:Filter_Mini-Circuits_FV1206-6 Filter:Filter_Mini-Circuits_FV1206-7 -Filter:Filter_Mini-Circuits_FV1206 Filter:Filter_Murata_BNX025 Filter:Filter_Murata_BNX025_ThermalVias Filter:Filter_Murata_SFECF-6 @@ -8210,51 +8921,6 @@ Filter:Filter_SAW_Epcos_DCC6C_3x3mm Filter:Filter_Schaffner_FN405 Filter:Filter_Schaffner_FN406 Fuse:FuseHolder_Blade_ATO_Littelfuse_FLR_178.6165 -Fuse:Fuseholder_Blade_ATO_Littelfuse_Pudenz_2_Pin -Fuse:Fuseholder_Blade_Mini_Keystone_3568 -Fuse:Fuseholder_Clip-5x20mm_Bel_FC-203-22_Lateral_P17.80x5.00mm_D1.17mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Eaton_1A5601-01_Inline_P20.80x6.76mm_D1.70mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Keystone_3512P_Inline_P23.62x7.27mm_D1.02x2.41x1.02x1.57mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Keystone_3512_Inline_P23.62x7.27mm_D1.02x1.57mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Keystone_3517_Inline_P23.11x6.76mm_D1.70mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Keystone_3518P_Inline_P23.11x6.76mm_D2.44x1.70mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Littelfuse_100_Inline_P20.50x4.60mm_D1.30mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Littelfuse_111_Inline_P20.00x5.00mm_D1.05mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Littelfuse_111_Lateral_P18.80x5.00mm_D1.17mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Littelfuse_445-030_Inline_P20.50x5.20mm_D1.30mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Littelfuse_519_Inline_P20.60x5.00mm_D1.00mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Littelfuse_520_Inline_P20.50x5.80mm_D1.30mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Littelfuse_521_Lateral_P17.00x5.00mm_D1.30mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Schurter_CQM_Inline_P20.60x5.00mm_D1.00mm_Horizontal -Fuse:Fuseholder_Clip-5x20mm_Schurter_OG_Lateral_P15.00x5.00mm_D1.3mm_Horizontal -Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102071_Inline_P34.70x7.60mm_D2.00mm_Horizontal -Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102_122_Inline_P34.21x7.62mm_D1.98mm_Horizontal -Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102_Inline_P34.21x7.62mm_D2.54mm_Horizontal -Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_122_Inline_P34.21x7.62mm_D2.54mm_Horizontal -Fuse:Fuseholder_Cylinder-5x20mm_Bulgin_FX0456_Vertical_Closed -Fuse:Fuseholder_Cylinder-5x20mm_Bulgin_FX0457_Horizontal_Closed -Fuse:Fuseholder_Cylinder-5x20mm_EATON_H15-V-1_Vertical_Closed -Fuse:Fuseholder_Cylinder-5x20mm_EATON_HBV_Vertical_Closed -Fuse:Fuseholder_Cylinder-5x20mm_EATON_HBW_Vertical_Closed -Fuse:Fuseholder_Cylinder-5x20mm_Schurter_0031_8201_Horizontal_Open -Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FAB_0031-355x_Horizontal_Closed -Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FPG4_Vertical_Closed -Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FUP_0031.2510_Horizontal_Closed -Fuse:Fuseholder_Cylinder-5x20mm_Schurter_OGN-SMD_Horizontal_Open -Fuse:Fuseholder_Cylinder-5x20mm_Stelvio-Kontek_PTF78_Horizontal_Open -Fuse:Fuseholder_Cylinder-5x20mm_Wuerth_696103101002-SMD_Horizontal_Open -Fuse:Fuseholder_Cylinder-6.3x32mm_Schurter_0031-8002_Horizontal_Open -Fuse:Fuseholder_Cylinder-6.3x32mm_Schurter_FUP_0031.2520_Horizontal_Closed -Fuse:Fuseholder_Keystone_3555-2 -Fuse:Fuseholder_Littelfuse_100_series_5x20mm -Fuse:Fuseholder_Littelfuse_100_series_5x25mm -Fuse:Fuseholder_Littelfuse_100_series_5x30mm -Fuse:Fuseholder_Littelfuse_445_030_series_5x20mm -Fuse:Fuseholder_Littelfuse_445_030_series_5x25mm -Fuse:Fuseholder_Littelfuse_445_030_series_5x30mm -Fuse:Fuseholder_Littelfuse_Nano2_154x -Fuse:Fuseholder_Littelfuse_Nano2_157x -Fuse:Fuseholder_TR5_Littelfuse_No560_No460 Fuse:Fuse_0402_1005Metric Fuse:Fuse_0402_1005Metric_Pad0.77x0.64mm_HandSolder Fuse:Fuse_0603_1608Metric @@ -8329,6 +8995,54 @@ Fuse:Fuse_Littelfuse_395Series Fuse:Fuse_Schurter_UMT250 Fuse:Fuse_Schurter_UMZ250 Fuse:Fuse_SunFuse-6HP +Fuse:Fuseholder_Blade_ATO_Littelfuse_Pudenz_2_Pin +Fuse:Fuseholder_Blade_Mini_Keystone_3568 +Fuse:Fuseholder_Clip-5x20mm_Bel_FC-203-22_Lateral_P17.80x5.00mm_D1.17mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Eaton_1A5601-01_Inline_P20.80x6.76mm_D1.70mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Keystone_3512P_Inline_P23.62x7.27mm_D1.02x2.41x1.02x1.57mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Keystone_3512_Inline_P23.62x7.27mm_D1.02x1.57mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Keystone_3517_Inline_P23.11x6.76mm_D1.70mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Keystone_3518P_Inline_P23.11x6.76mm_D2.44x1.70mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_100_Inline_P20.50x4.60mm_D1.30mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_111_Inline_P20.00x5.00mm_D1.05mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_111_Lateral_P18.80x5.00mm_D1.17mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_445-030_Inline_P20.50x5.20mm_D1.30mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_519_Inline_P20.60x5.00mm_D1.00mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_520_Inline_P20.50x5.80mm_D1.30mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_521_Lateral_P17.00x5.00mm_D1.30mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Schurter_CQM_Inline_P20.60x5.00mm_D1.00mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Schurter_OG_Lateral_P15.00x5.00mm_D1.3mm_Horizontal +Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102071_Inline_P34.70x7.60mm_D2.00mm_Horizontal +Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102_122_Inline_P34.21x7.62mm_D1.98mm_Horizontal +Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102_Inline_P34.21x7.62mm_D2.54mm_Horizontal +Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_122_Inline_P34.21x7.62mm_D2.54mm_Horizontal +Fuse:Fuseholder_Cylinder-5x20mm_Bulgin_FX0456_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Bulgin_FX0457_Horizontal_Closed +Fuse:Fuseholder_Cylinder-5x20mm_EATON_H15-V-1_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_EATON_HBV_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_EATON_HBW_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_0031_8201_Horizontal_Open +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FAB_0031-355x_Horizontal_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FPG4_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FUP_0031.2510_Horizontal_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_OGN-SMD_Horizontal_Open +Fuse:Fuseholder_Cylinder-5x20mm_Stelvio-Kontek_PTF78_Horizontal_Open +Fuse:Fuseholder_Cylinder-5x20mm_Wuerth_696103101002-SMD_Horizontal_Open +Fuse:Fuseholder_Cylinder-6.3x32mm_Schurter_0031-8002_Horizontal_Open +Fuse:Fuseholder_Cylinder-6.3x32mm_Schurter_FUP_0031.2520_Horizontal_Closed +Fuse:Fuseholder_Keystone_3555-2 +Fuse:Fuseholder_Littelfuse_100_series_5x20mm +Fuse:Fuseholder_Littelfuse_100_series_5x25mm +Fuse:Fuseholder_Littelfuse_100_series_5x30mm +Fuse:Fuseholder_Littelfuse_445_030_series_5x20mm +Fuse:Fuseholder_Littelfuse_445_030_series_5x25mm +Fuse:Fuseholder_Littelfuse_445_030_series_5x30mm +Fuse:Fuseholder_Littelfuse_Nano2_154x +Fuse:Fuseholder_Littelfuse_Nano2_157x +Fuse:Fuseholder_TR5_Littelfuse_No560_No460 +Fuse:GDT_Bourns_2038 +Fuse:GDT_Yageo_3RxxxxL-6 +Fuse:GDT_Yageo_3RxxxxM-6 Heatsink:Heatsink_125x35x50mm_3xFixationM3 Heatsink:Heatsink_35x26mm_1xFixation3mm_Fischer-SK486-35 Heatsink:Heatsink_38x38mm_SpringFixation @@ -8336,6 +9050,17 @@ Heatsink:Heatsink_62x40mm_2xFixation3mm Heatsink:Heatsink_AAVID_573300D00010G_TO-263 Heatsink:Heatsink_AAVID_576802B03900G Heatsink:Heatsink_AAVID_590302B03600G +Heatsink:Heatsink_AAVID_Extruded_531002B00000G_34.9x12.7mm_H25.4mm +Heatsink:Heatsink_AAVID_Extruded_531002B02100G_34.9x12.7mm_H25.4mm +Heatsink:Heatsink_AAVID_Extruded_531002B02500G_34.9x12.7mm_H25.4mm +Heatsink:Heatsink_AAVID_Extruded_531102B00000G_34.9x12.7mm_H38.1mm +Heatsink:Heatsink_AAVID_Extruded_531102B02100G_34.9x12.7mm_H38.1mm +Heatsink:Heatsink_AAVID_Extruded_531102B02500G_34.9x12.7mm_H38.1mm +Heatsink:Heatsink_AAVID_Extruded_531102V02500G_34.9x12.7mm_H38.1mm +Heatsink:Heatsink_AAVID_Extruded_531202B00000G_34.9x12.7mm_H50.8mm +Heatsink:Heatsink_AAVID_Extruded_531202B02100G_34.9x12.7mm_H50.8mm +Heatsink:Heatsink_AAVID_Extruded_531202B02500G_34.9x12.7mm_H50.8mm +Heatsink:Heatsink_AAVID_Extruded_531302B02500G_34.9x12.7mm_H63.5mm Heatsink:Heatsink_AAVID_TV5G_TO220_Horizontal Heatsink:Heatsink_Fischer_FK224xx2201_25x8.3mm Heatsink:Heatsink_Fischer_FK24413D2PAK_26x13mm @@ -8359,13 +9084,11 @@ Inductor_SMD:L_0603_1608Metric Inductor_SMD:L_0603_1608Metric_Pad1.05x0.95mm_HandSolder Inductor_SMD:L_0805_2012Metric Inductor_SMD:L_0805_2012Metric_Pad1.05x1.20mm_HandSolder -Inductor_SMD:L_0805_2012Metric_Pad1.15x1.40mm_HandSolder Inductor_SMD:L_10.4x10.4_H4.8 Inductor_SMD:L_1008_2520Metric Inductor_SMD:L_1008_2520Metric_Pad1.43x2.20mm_HandSolder Inductor_SMD:L_1206_3216Metric Inductor_SMD:L_1206_3216Metric_Pad1.22x1.90mm_HandSolder -Inductor_SMD:L_1206_3216Metric_Pad1.42x1.75mm_HandSolder Inductor_SMD:L_1210_3225Metric Inductor_SMD:L_1210_3225Metric_Pad1.42x2.65mm_HandSolder Inductor_SMD:L_12x12mm_H4.5mm @@ -8382,11 +9105,6 @@ Inductor_SMD:L_2512_6332Metric_Pad1.52x3.35mm_HandSolder Inductor_SMD:L_6.3x6.3_H3 Inductor_SMD:L_7.3x7.3_H3.5 Inductor_SMD:L_7.3x7.3_H4.5 -Inductor_SMD:L_Abracon_ASPI-0425 -Inductor_SMD:L_Abracon_ASPI-0630LR -Inductor_SMD:L_Abracon_ASPI-3012S -Inductor_SMD:L_Abracon_ASPI-4030S -Inductor_SMD:L_Abracon_ASPIAIG-F4020 Inductor_SMD:L_APV_ANR252010 Inductor_SMD:L_APV_ANR252012 Inductor_SMD:L_APV_ANR3010 @@ -8430,6 +9148,11 @@ Inductor_SMD:L_APV_APH1265 Inductor_SMD:L_APV_APH1770 Inductor_SMD:L_APV_APH2213 Inductor_SMD:L_AVX_LMLP07A7 +Inductor_SMD:L_Abracon_ASPI-0425 +Inductor_SMD:L_Abracon_ASPI-0630LR +Inductor_SMD:L_Abracon_ASPI-3012S +Inductor_SMD:L_Abracon_ASPI-4030S +Inductor_SMD:L_Abracon_ASPIAIG-F4020 Inductor_SMD:L_Bourns-SRN1060 Inductor_SMD:L_Bourns-SRN4018 Inductor_SMD:L_Bourns-SRN6028 @@ -8497,6 +9220,40 @@ Inductor_SMD:L_Changjiang_FNR6045S Inductor_SMD:L_Changjiang_FNR8040S Inductor_SMD:L_Changjiang_FNR8050S Inductor_SMD:L_Changjiang_FNR8065S +Inductor_SMD:L_Changjiang_FTC100765D +Inductor_SMD:L_Changjiang_FTC121065S +Inductor_SMD:L_Changjiang_FTC141207S +Inductor_SMD:L_Changjiang_FTC141208S +Inductor_SMD:L_Changjiang_FTC141265S +Inductor_SMD:L_Changjiang_FTC160808S +Inductor_SMD:L_Changjiang_FTC160865S +Inductor_SMD:L_Changjiang_FTC201208S +Inductor_SMD:L_Changjiang_FTC201210S +Inductor_SMD:L_Changjiang_FTC201212S +Inductor_SMD:L_Changjiang_FTC201265S +Inductor_SMD:L_Changjiang_FTC201607S +Inductor_SMD:L_Changjiang_FTC201608S +Inductor_SMD:L_Changjiang_FTC201610D +Inductor_SMD:L_Changjiang_FTC201610S +Inductor_SMD:L_Changjiang_FTC201612S +Inductor_SMD:L_Changjiang_FTC201655S +Inductor_SMD:L_Changjiang_FTC201665S +Inductor_SMD:L_Changjiang_FTC252008S +Inductor_SMD:L_Changjiang_FTC252010S +Inductor_SMD:L_Changjiang_FTC252012S +Inductor_SMD:L_Changjiang_FTC252075S +Inductor_SMD:L_Changjiang_FTC303010D +Inductor_SMD:L_Changjiang_FTC303012D +Inductor_SMD:L_Changjiang_FTC303015D +Inductor_SMD:L_Changjiang_FTC303018D +Inductor_SMD:L_Changjiang_FTC303020D +Inductor_SMD:L_Changjiang_FTC322510S +Inductor_SMD:L_Changjiang_FTC322512S +Inductor_SMD:L_Changjiang_FTC322520S +Inductor_SMD:L_Changjiang_FTC404010S +Inductor_SMD:L_Changjiang_FTC404012S +Inductor_SMD:L_Changjiang_FTC404020S +Inductor_SMD:L_Changjiang_FTC404030S Inductor_SMD:L_Changjiang_FXL0412 Inductor_SMD:L_Changjiang_FXL0420 Inductor_SMD:L_Changjiang_FXL0518 @@ -8522,8 +9279,8 @@ Inductor_SMD:L_Chilisin_BMRA00040420 Inductor_SMD:L_Chilisin_BMRA00050520 Inductor_SMD:L_Chilisin_BMRA00050530 Inductor_SMD:L_Chilisin_BMRB00050512 -Inductor_SMD:L_Chilisin_BMRB00050518-B Inductor_SMD:L_Chilisin_BMRB00050518 +Inductor_SMD:L_Chilisin_BMRB00050518-B Inductor_SMD:L_Chilisin_BMRB00060612 Inductor_SMD:L_Chilisin_BMRB00060618 Inductor_SMD:L_Chilisin_BMRB00060624 @@ -8539,6 +9296,22 @@ Inductor_SMD:L_Chilisin_BMRx00050512-B Inductor_SMD:L_Chilisin_BMRx00050515 Inductor_SMD:L_Chilisin_BMRx00060615 Inductor_SMD:L_Chilisin_BMRx00060630 +Inductor_SMD:L_Chilisin_BWVS00322515 +Inductor_SMD:L_Chilisin_BWVS00404012 +Inductor_SMD:L_Chilisin_BWVS00404018 +Inductor_SMD:L_Chilisin_BWVS00404026 +Inductor_SMD:L_Chilisin_BWVS00505020 +Inductor_SMD:L_Chilisin_BWVS00505030 +Inductor_SMD:L_Chilisin_BWVS00505040 +Inductor_SMD:L_Chilisin_BWVS00606020 +Inductor_SMD:L_Chilisin_BWVS00606028 +Inductor_SMD:L_Chilisin_BWVS00606045 +Inductor_SMD:L_Chilisin_BWVS00808040 +Inductor_SMD:L_Coilcraft_0403HQ_1008Metric +Inductor_SMD:L_Coilcraft_0604HQ_1610Metric +Inductor_SMD:L_Coilcraft_0805HQ_2012Metric +Inductor_SMD:L_Coilcraft_1008HQ_2520Metric +Inductor_SMD:L_Coilcraft_1008HQ_2520Metric_LowProfile Inductor_SMD:L_Coilcraft_1515SQ-47N Inductor_SMD:L_Coilcraft_1515SQ-68N Inductor_SMD:L_Coilcraft_1515SQ-82N @@ -8641,6 +9414,14 @@ Inductor_SMD:L_Coilcraft_XFL2010 Inductor_SMD:L_Coilcraft_XxL4020 Inductor_SMD:L_Coilcraft_XxL4030 Inductor_SMD:L_Coilcraft_XxL4040 +Inductor_SMD:L_CommonModeChoke_Bourns_SRF1260 +Inductor_SMD:L_CommonModeChoke_Coilank_ACM1210 +Inductor_SMD:L_CommonModeChoke_Coilank_ACM1608 +Inductor_SMD:L_CommonModeChoke_Coilank_ACM2012 +Inductor_SMD:L_CommonModeChoke_Coilank_ACM2520 +Inductor_SMD:L_CommonModeChoke_Coilank_ACM3216 +Inductor_SMD:L_CommonModeChoke_Coilank_ACM3225 +Inductor_SMD:L_CommonModeChoke_Coilank_ACM4532 Inductor_SMD:L_CommonModeChoke_Coilcraft_0603USB Inductor_SMD:L_CommonModeChoke_Coilcraft_0805USB Inductor_SMD:L_CommonModeChoke_Coilcraft_1812CAN @@ -8649,6 +9430,9 @@ Inductor_SMD:L_CommonModeChoke_TDK_ACM2520-2P Inductor_SMD:L_CommonModeChoke_TDK_ACM2520-3P Inductor_SMD:L_CommonModeChoke_TDK_ACM7060 Inductor_SMD:L_CommonModeChoke_Wuerth_WE-SL5 +Inductor_SMD:L_CommonModeChoke_Wuerth_WE-SL5-HC +Inductor_SMD:L_CommonModeChoke_XR_XRGM0905C +Inductor_SMD:L_CommonMode_Bourns_DR331 Inductor_SMD:L_CommonMode_Delevan_4222 Inductor_SMD:L_CommonMode_Wuerth_WE-SL2 Inductor_SMD:L_CommonMode_Wurth_WE-CNSW-1206 @@ -8666,7 +9450,8 @@ Inductor_SMD:L_Ferrocore_DLG-1004 Inductor_SMD:L_Ferrocore_DLG-1005 Inductor_SMD:L_KOHERelec_MDA5030 Inductor_SMD:L_KOHERelec_MDA7030 -Inductor_SMD:L_Murata_DEM35xxC +Inductor_SMD:L_Murata_DEM3512C +Inductor_SMD:L_Murata_DEM3518C Inductor_SMD:L_Murata_DFE201610P Inductor_SMD:L_Murata_LQH2MCNxxxx02_2.0x1.6mm Inductor_SMD:L_Murata_LQH55DN_5.7x5.0mm @@ -8689,8 +9474,8 @@ Inductor_SMD:L_Neosid_Ms50T Inductor_SMD:L_Neosid_Ms85 Inductor_SMD:L_Neosid_Ms85T Inductor_SMD:L_Neosid_Ms95 -Inductor_SMD:L_Neosid_Ms95a Inductor_SMD:L_Neosid_Ms95T +Inductor_SMD:L_Neosid_Ms95a Inductor_SMD:L_Neosid_SM-NE127 Inductor_SMD:L_Neosid_SM-NE127_HandSoldering Inductor_SMD:L_Neosid_SM-NE150 @@ -8705,14 +9490,37 @@ Inductor_SMD:L_Neosid_SMs42 Inductor_SMD:L_Neosid_SMs50 Inductor_SMD:L_Neosid_SMs85 Inductor_SMD:L_Neosid_SMs95_SMs95p +Inductor_SMD:L_Panasonic_PCC-M0530M +Inductor_SMD:L_Panasonic_PCC-M0540M +Inductor_SMD:L_Panasonic_PCC-M0630M +Inductor_SMD:L_Panasonic_PCC-M0645M +Inductor_SMD:L_Panasonic_PCC-M0750M +Inductor_SMD:L_Panasonic_PCC-M0754M +Inductor_SMD:L_Panasonic_PCC-M0850M +Inductor_SMD:L_Panasonic_PCC-M0854M +Inductor_SMD:L_Panasonic_PCC-M1040ML +Inductor_SMD:L_Panasonic_PCC-M1050M +Inductor_SMD:L_Panasonic_PCC-M1050ML +Inductor_SMD:L_Panasonic_PCC-M1054M +Inductor_SMD:L_Panasonic_PCC-M1060ML Inductor_SMD:L_Pulse_P059x Inductor_SMD:L_Pulse_PA4320 Inductor_SMD:L_Pulse_PA4332 +Inductor_SMD:L_Pulse_PA4334 Inductor_SMD:L_Pulse_PA4340 Inductor_SMD:L_Pulse_PA4341 Inductor_SMD:L_Pulse_PA4344 Inductor_SMD:L_Pulse_PA4349 Inductor_SMD:L_Pulse_PA5402 +Inductor_SMD:L_SOREDE_SNR.1050_10x10x5mm +Inductor_SMD:L_SXN_SMDRI124 +Inductor_SMD:L_SXN_SMDRI125 +Inductor_SMD:L_SXN_SMDRI127 +Inductor_SMD:L_SXN_SMDRI62 +Inductor_SMD:L_SXN_SMDRI64 +Inductor_SMD:L_SXN_SMDRI73 +Inductor_SMD:L_SXN_SMDRI74 +Inductor_SMD:L_SXN_SMMS1770 Inductor_SMD:L_Sagami_CER1242B Inductor_SMD:L_Sagami_CER1257B Inductor_SMD:L_Sagami_CER1277B @@ -8720,7 +9528,6 @@ Inductor_SMD:L_Sagami_CWR1242C Inductor_SMD:L_Sagami_CWR1257C Inductor_SMD:L_Sagami_CWR1277C Inductor_SMD:L_SigTra_SC3316F -Inductor_SMD:L_SOREDE_SNR.1050_10x10x5mm Inductor_SMD:L_Sumida_CDMC6D28_7.25x6.5mm Inductor_SMD:L_Sumida_CR75 Inductor_SMD:L_Sunlord_MWSA0402S @@ -8812,13 +9619,25 @@ Inductor_SMD:L_Sunlord_SWPA8040S Inductor_SMD:L_Sunlord_SWRB1204S Inductor_SMD:L_Sunlord_SWRB1205S Inductor_SMD:L_Sunlord_SWRB1207S -Inductor_SMD:L_SXN_SMDRI124 -Inductor_SMD:L_SXN_SMDRI125 -Inductor_SMD:L_SXN_SMDRI127 -Inductor_SMD:L_SXN_SMDRI62 -Inductor_SMD:L_SXN_SMDRI64 -Inductor_SMD:L_SXN_SMDRI73 -Inductor_SMD:L_SXN_SMDRI74 +Inductor_SMD:L_TDK_MLZ1608 +Inductor_SMD:L_TDK_MLZ2012_h0.85mm +Inductor_SMD:L_TDK_MLZ2012_h1.25mm +Inductor_SMD:L_TDK_NLV25_2.5x2.0mm +Inductor_SMD:L_TDK_NLV32_3.2x2.5mm +Inductor_SMD:L_TDK_SLF10145 +Inductor_SMD:L_TDK_SLF10165 +Inductor_SMD:L_TDK_SLF12555 +Inductor_SMD:L_TDK_SLF12565 +Inductor_SMD:L_TDK_SLF12575 +Inductor_SMD:L_TDK_SLF6025 +Inductor_SMD:L_TDK_SLF6028 +Inductor_SMD:L_TDK_SLF6045 +Inductor_SMD:L_TDK_SLF7032 +Inductor_SMD:L_TDK_SLF7045 +Inductor_SMD:L_TDK_SLF7055 +Inductor_SMD:L_TDK_VLF10040 +Inductor_SMD:L_TDK_VLP8040 +Inductor_SMD:L_TDK_VLS6045EX_VLS6045AF Inductor_SMD:L_TaiTech_TMPC1265_13.5x12.5mm Inductor_SMD:L_Taiyo-Yuden_BK_Array_1206_3216Metric Inductor_SMD:L_Taiyo-Yuden_MD-1616 @@ -8842,25 +9661,17 @@ Inductor_SMD:L_Taiyo-Yuden_NR-60xx Inductor_SMD:L_Taiyo-Yuden_NR-60xx_HandSoldering Inductor_SMD:L_Taiyo-Yuden_NR-80xx Inductor_SMD:L_Taiyo-Yuden_NR-80xx_HandSoldering -Inductor_SMD:L_TDK_MLZ1608 -Inductor_SMD:L_TDK_MLZ2012_h0.85mm -Inductor_SMD:L_TDK_MLZ2012_h1.25mm -Inductor_SMD:L_TDK_NLV25_2.5x2.0mm -Inductor_SMD:L_TDK_NLV32_3.2x2.5mm -Inductor_SMD:L_TDK_SLF10145 -Inductor_SMD:L_TDK_SLF10165 -Inductor_SMD:L_TDK_SLF12555 -Inductor_SMD:L_TDK_SLF12565 -Inductor_SMD:L_TDK_SLF12575 -Inductor_SMD:L_TDK_SLF6025 -Inductor_SMD:L_TDK_SLF6028 -Inductor_SMD:L_TDK_SLF6045 -Inductor_SMD:L_TDK_SLF7032 -Inductor_SMD:L_TDK_SLF7045 -Inductor_SMD:L_TDK_SLF7055 -Inductor_SMD:L_TDK_VLF10040 -Inductor_SMD:L_TDK_VLP8040 -Inductor_SMD:L_TDK_VLS6045EX_VLS6045AF +Inductor_SMD:L_TechFuse_SL0420 +Inductor_SMD:L_TechFuse_SL0520 +Inductor_SMD:L_TechFuse_SL0530 +Inductor_SMD:L_TechFuse_SL0620 +Inductor_SMD:L_TechFuse_SL0624 +Inductor_SMD:L_TechFuse_SL0630 +Inductor_SMD:L_TechFuse_SL0650 +Inductor_SMD:L_TechFuse_SL1040 +Inductor_SMD:L_TechFuse_SL1050 +Inductor_SMD:L_TechFuse_SL1250 +Inductor_SMD:L_TechFuse_SL1265 Inductor_SMD:L_TracoPower_TCK-047_5.2x5.8mm Inductor_SMD:L_TracoPower_TCK-141 Inductor_SMD:L_Vishay_IFSC-1515AH_4x4x1.8mm @@ -8868,6 +9679,7 @@ Inductor_SMD:L_Vishay_IHLP-1212 Inductor_SMD:L_Vishay_IHLP-1616 Inductor_SMD:L_Vishay_IHLP-2020 Inductor_SMD:L_Vishay_IHLP-2525 +Inductor_SMD:L_Vishay_IHLP-3232 Inductor_SMD:L_Vishay_IHLP-4040 Inductor_SMD:L_Vishay_IHLP-5050 Inductor_SMD:L_Vishay_IHLP-6767 @@ -8903,6 +9715,9 @@ Inductor_SMD:L_Wuerth_HCM-1350 Inductor_SMD:L_Wuerth_HCM-1390 Inductor_SMD:L_Wuerth_HCM-7050 Inductor_SMD:L_Wuerth_HCM-7070 +Inductor_SMD:L_Wuerth_LQFS-3818 +Inductor_SMD:L_Wuerth_LQFS-4818 +Inductor_SMD:L_Wuerth_LQFS-4828 Inductor_SMD:L_Wuerth_MAPI-1610 Inductor_SMD:L_Wuerth_MAPI-2010 Inductor_SMD:L_Wuerth_MAPI-2506 @@ -8915,6 +9730,23 @@ Inductor_SMD:L_Wuerth_MAPI-3015 Inductor_SMD:L_Wuerth_MAPI-3020 Inductor_SMD:L_Wuerth_MAPI-4020 Inductor_SMD:L_Wuerth_MAPI-4030 +Inductor_SMD:L_Wuerth_MAPI-5020 +Inductor_SMD:L_Wuerth_MAPI-5030 +Inductor_SMD:L_Wuerth_PMCI-160808 +Inductor_SMD:L_Wuerth_PMCI-201210 +Inductor_SMD:L_Wuerth_PMCI-201610 +Inductor_SMD:L_Wuerth_PMCI-252010 +Inductor_SMD:L_Wuerth_PMCI-252012 +Inductor_SMD:L_Wuerth_PMCI-322510 +Inductor_SMD:L_Wuerth_PMCI-322512 +Inductor_SMD:L_Wuerth_PMCI-322515 +Inductor_SMD:L_Wuerth_PMFI-201610 +Inductor_SMD:L_Wuerth_PMFI-201610_PMCI-compatible +Inductor_SMD:L_Wuerth_PMFI-252012 +Inductor_SMD:L_Wuerth_PMFI-252012_PMCI-compatible +Inductor_SMD:L_Wuerth_PMFI-322512 +Inductor_SMD:L_Wuerth_PMFI-322512_PMCI-compatible +Inductor_SMD:L_Wuerth_PMFI-353220 Inductor_SMD:L_Wuerth_WE-DD-Typ-L-Typ-XL-Typ-XXL Inductor_SMD:L_Wuerth_WE-DD-Typ-M-Typ-S Inductor_SMD:L_Wuerth_WE-GF-1210 @@ -8930,7 +9762,8 @@ Inductor_SMD:L_Wuerth_WE-PD4-Typ-X Inductor_SMD:L_Wuerth_WE-PDF Inductor_SMD:L_Wuerth_WE-PDF_Handsoldering Inductor_SMD:L_Wuerth_WE-TPC-3816 -Inductor_SMD:L_Wuerth_WE-XHMI-8080 +Inductor_SMD:L_Wuerth_XHMI-6060 +Inductor_SMD:L_Wuerth_XHMI-8080 Inductor_SMD:L_Wurth_WE-CAIR-5910 Inductor_SMD_Wurth:L_Wurth_WE-LQSH-2010 Inductor_SMD_Wurth:L_Wurth_WE-LQSH-2512 @@ -9047,8 +9880,14 @@ Inductor_THT:L_CommonMode_TDK_B82747E6353A040 Inductor_THT:L_CommonMode_TDK_B82767S4123N030 Inductor_THT:L_CommonMode_TDK_B82767S4193N030 Inductor_THT:L_CommonMode_TDK_B82767S4263N030 +Inductor_THT:L_CommonMode_Toroid_Vertical_L13.0mm_W7.5mm_Px6.00mm_Py5.00mm_PRODTech_PDMCAT1065 Inductor_THT:L_CommonMode_Toroid_Vertical_L19.3mm_W10.8mm_Px6.35mm_Py15.24mm_Bourns_8100 Inductor_THT:L_CommonMode_Toroid_Vertical_L21.0mm_W10.0mm_Px5.08mm_Py12.70mm_Murata_5100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L22.0mm_W12.0mm_Px9.00mm_Py8.00mm_PRODTech_PDMCAT18107-102ML +Inductor_THT:L_CommonMode_Toroid_Vertical_L22.0mm_W12.0mm_Px9.00mm_Py8.00mm_PRODTech_PDMCAT18107-103ML +Inductor_THT:L_CommonMode_Toroid_Vertical_L22.0mm_W12.0mm_Px9.00mm_Py8.00mm_PRODTech_PDMCAT18107-202ML +Inductor_THT:L_CommonMode_Toroid_Vertical_L22.0mm_W12.0mm_Px9.00mm_Py8.00mm_PRODTech_PDMCAT18107-203ML +Inductor_THT:L_CommonMode_Toroid_Vertical_L22.0mm_W12.0mm_Px9.00mm_Py8.00mm_PRODTech_PDMCAT18107-502ML Inductor_THT:L_CommonMode_Toroid_Vertical_L24.0mm_W16.3mm_Px10.16mm_Py20.32mm_Murata_5200 Inductor_THT:L_CommonMode_Toroid_Vertical_L30.5mm_W15.2mm_Px10.16mm_Py20.32mm_Bourns_8100 Inductor_THT:L_CommonMode_Toroid_Vertical_L34.3mm_W20.3mm_Px15.24mm_Py22.86mm_Bourns_8100 @@ -9073,15 +9912,15 @@ Inductor_THT:L_Mount_Lodestone_VTM280 Inductor_THT:L_Mount_Lodestone_VTM950-6 Inductor_THT:L_Radial_D10.0mm_P5.00mm_Fastron_07M Inductor_THT:L_Radial_D10.0mm_P5.00mm_Fastron_07P -Inductor_THT:L_Radial_D10.0mm_P5.00mm_Neosid_SD12k_style3 Inductor_THT:L_Radial_D10.0mm_P5.00mm_Neosid_SD12_style3 +Inductor_THT:L_Radial_D10.0mm_P5.00mm_Neosid_SD12k_style3 Inductor_THT:L_Radial_D10.5mm_P4.00x5.00mm_Murata_1200RS Inductor_THT:L_Radial_D10.5mm_P5.00mm_Abacron_AISR-01 -Inductor_THT:L_Radial_D12.0mm_P10.00mm_Neosid_SD12k_style1 Inductor_THT:L_Radial_D12.0mm_P10.00mm_Neosid_SD12_style1 +Inductor_THT:L_Radial_D12.0mm_P10.00mm_Neosid_SD12k_style1 Inductor_THT:L_Radial_D12.0mm_P5.00mm_Fastron_11P -Inductor_THT:L_Radial_D12.0mm_P5.00mm_Neosid_SD12k_style2 Inductor_THT:L_Radial_D12.0mm_P5.00mm_Neosid_SD12_style2 +Inductor_THT:L_Radial_D12.0mm_P5.00mm_Neosid_SD12k_style2 Inductor_THT:L_Radial_D12.0mm_P6.00mm_Murata_1900R Inductor_THT:L_Radial_D12.5mm_P7.00mm_Fastron_09HCP Inductor_THT:L_Radial_D12.5mm_P9.00mm_Fastron_09HCP @@ -9215,6 +10054,7 @@ Inductor_THT_Wurth:L_Wurth_WE-HCFT-3540_LeadDiameter0.8mm Inductor_THT_Wurth:L_Wurth_WE-HCFT-3540_LeadDiameter1.3mm Inductor_THT_Wurth:L_Wurth_WE-HCFT-3540_LeadDiameter1.5mm Inductor_THT_Wurth:L_Wurth_WE-HCFT-3540_LeadDiameter2.0mm +Jumper:Jumper_Harwin_S1621_P10.9mm Jumper:SolderJumper-2_P1.3mm_Bridged2Bar_Pad1.0x1.5mm Jumper:SolderJumper-2_P1.3mm_Bridged2Bar_RoundedPad1.0x1.5mm Jumper:SolderJumper-2_P1.3mm_Bridged_Pad1.0x1.5mm @@ -9222,10 +10062,18 @@ Jumper:SolderJumper-2_P1.3mm_Bridged_RoundedPad1.0x1.5mm Jumper:SolderJumper-2_P1.3mm_Open_Pad1.0x1.5mm Jumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mm Jumper:SolderJumper-2_P1.3mm_Open_TrianglePad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged123_Pad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged123_Pad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P1.3mm_Bridged123_RoundedPad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged123_RoundedPad1.0x1.5mm_NumberLabels Jumper:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm Jumper:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_NumberLabels Jumper:SolderJumper-3_P1.3mm_Bridged12_RoundedPad1.0x1.5mm Jumper:SolderJumper-3_P1.3mm_Bridged12_RoundedPad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P1.3mm_Bridged2Bar123_Pad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged2Bar123_Pad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P1.3mm_Bridged2Bar123_RoundedPad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged2Bar123_RoundedPad1.0x1.5mm_NumberLabels Jumper:SolderJumper-3_P1.3mm_Bridged2Bar12_Pad1.0x1.5mm Jumper:SolderJumper-3_P1.3mm_Bridged2Bar12_Pad1.0x1.5mm_NumberLabels Jumper:SolderJumper-3_P1.3mm_Bridged2Bar12_RoundedPad1.0x1.5mm @@ -9238,6 +10086,8 @@ Jumper:SolderJumper-3_P2.0mm_Open_TrianglePad1.0x1.5mm Jumper:SolderJumper-3_P2.0mm_Open_TrianglePad1.0x1.5mm_NumberLabels LED_SMD:LED-APA102-2020 LED_SMD:LED-L1T2_LUMILEDS +LED_SMD:LED_01005_0402Metric +LED_SMD:LED_01005_0402Metric_Pad0.57x0.30mm_HandSolder LED_SMD:LED_0201_0603Metric LED_SMD:LED_0201_0603Metric_Pad0.64x0.40mm_HandSolder LED_SMD:LED_0402_1005Metric @@ -9261,6 +10111,7 @@ LED_SMD:LED_2512_6332Metric_Pad1.52x3.35mm_HandSolder LED_SMD:LED_ASMB-KTF0-0A306 LED_SMD:LED_Avago_PLCC4_3.2x2.8mm_CW LED_SMD:LED_Avago_PLCC6_3x2.8mm +LED_SMD:LED_CSP_Samsung_LH181B_2.36x2.36mm LED_SMD:LED_Cree-PLCC4_2x2mm_CW LED_SMD:LED_Cree-PLCC4_3.2x2.8mm_CCW LED_SMD:LED_Cree-PLCC4_5x5mm_CW @@ -9268,17 +10119,21 @@ LED_SMD:LED_Cree-PLCC6_4.7x1.5mm LED_SMD:LED_Cree-XB LED_SMD:LED_Cree-XH LED_SMD:LED_Cree-XHP35 -LED_SMD:LED_Cree-XHP50_12V -LED_SMD:LED_Cree-XHP50_6V +LED_SMD:LED_Cree-XHP50_12V_HighDensity +LED_SMD:LED_Cree-XHP50_12V_HighIntensity +LED_SMD:LED_Cree-XHP50_3V_HighDensity +LED_SMD:LED_Cree-XHP50_3V_HighIntensity +LED_SMD:LED_Cree-XHP50_6V_HighDensity +LED_SMD:LED_Cree-XHP50_6V_HighIntensity LED_SMD:LED_Cree-XHP70_12V LED_SMD:LED_Cree-XHP70_6V -LED_SMD:LED_Cree-XP-G LED_SMD:LED_Cree-XP +LED_SMD:LED_Cree-XP-G LED_SMD:LED_Cree-XQ LED_SMD:LED_Cree-XQ_HandSoldering -LED_SMD:LED_CSP_Samsung_LH181B_2.36x2.36mm LED_SMD:LED_Dialight_591 LED_SMD:LED_Everlight-SMD3528_3.5x2.8mm_67-21ST +LED_SMD:LED_Foshan-NTD3528_3.5x2.8mm LED_SMD:LED_Inolux_IN-P55TATRGB_PLCC6_5.0x5.5mm_P1.8mm LED_SMD:LED_Inolux_IN-PI554FCH_PLCC4_5.0x5.0mm_P3.2mm LED_SMD:LED_Kingbright_AAA3528ESGCT @@ -9297,8 +10152,6 @@ LED_SMD:LED_LiteOn_LTST-S326 LED_SMD:LED_Lumex_SML-LX0303SIUPGUSB LED_SMD:LED_Lumex_SML-LX0404SIUPGUSB LED_SMD:LED_Luminus_MP-3030-1100_3.0x3.0mm -LED_SMD:LED_miniPLCC_2315 -LED_SMD:LED_miniPLCC_2315_Handsoldering LED_SMD:LED_OPSCO_SK6812_PLCC4_5.0x5.0mm_P3.1mm LED_SMD:LED_Osram_Lx_P47F_D2mm_ReverseMount LED_SMD:LED_PLCC-2_3.4x3.0mm_AK @@ -9318,6 +10171,7 @@ LED_SMD:LED_RGB_Wuerth-PLCC4_3.2x2.8mm_150141M173100 LED_SMD:LED_RGB_Wuerth_150080M153000 LED_SMD:LED_ROHM_SMLVN6 LED_SMD:LED_SK6805_PLCC4_2.4x2.7mm_P1.3mm +LED_SMD:LED_SK6812MINI-E_3.2x2.8mm_P1.5mm_ReverseMount LED_SMD:LED_SK6812MINI_PLCC4_3.5x3.5mm_P1.75mm LED_SMD:LED_SK6812_EC15_1.5x1.5mm LED_SMD:LED_SK6812_PLCC4_5.0x5.0mm_P3.2mm @@ -9327,6 +10181,8 @@ LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm LED_SMD:LED_WS2812_PLCC6_5.0x5.0mm_P1.6mm LED_SMD:LED_Wurth_150044M155260 LED_SMD:LED_Yuji_5730 +LED_SMD:LED_miniPLCC_2315 +LED_SMD:LED_miniPLCC_2315_Handsoldering LED_THT:LED_BL-FL7680RGB LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O1.27mm_Z1.6mm LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O1.27mm_Z4.9mm @@ -9338,13 +10194,13 @@ LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O6.35mm_Z1.6mm LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O6.35mm_Z4.9mm LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O6.35mm_Z8.2mm LED_THT:LED_D1.8mm_W3.3mm_H2.4mm -LED_THT:LED_D10.0mm-3 LED_THT:LED_D10.0mm +LED_THT:LED_D10.0mm-3 LED_THT:LED_D2.0mm_W4.0mm_H2.8mm_FlatTop LED_THT:LED_D2.0mm_W4.8mm_H2.5mm_FlatTop LED_THT:LED_D20.0mm -LED_THT:LED_D3.0mm-3 LED_THT:LED_D3.0mm +LED_THT:LED_D3.0mm-3 LED_THT:LED_D3.0mm_Clear LED_THT:LED_D3.0mm_FlatTop LED_THT:LED_D3.0mm_Horizontal_O1.27mm_Z10.0mm @@ -9362,12 +10218,12 @@ LED_THT:LED_D3.0mm_Horizontal_O6.35mm_Z6.0mm LED_THT:LED_D3.0mm_IRBlack LED_THT:LED_D3.0mm_IRGrey LED_THT:LED_D4.0mm +LED_THT:LED_D5.0mm LED_THT:LED_D5.0mm-3 LED_THT:LED_D5.0mm-3_Horizontal_O3.81mm_Z3.0mm LED_THT:LED_D5.0mm-4_RGB LED_THT:LED_D5.0mm-4_RGB_Staggered_Pins LED_THT:LED_D5.0mm-4_RGB_Wide_Pins -LED_THT:LED_D5.0mm LED_THT:LED_D5.0mm_Clear LED_THT:LED_D5.0mm_FlatTop LED_THT:LED_D5.0mm_Horizontal_O1.27mm_Z15.0mm @@ -9384,14 +10240,14 @@ LED_THT:LED_D5.0mm_Horizontal_O6.35mm_Z3.0mm LED_THT:LED_D5.0mm_Horizontal_O6.35mm_Z9.0mm LED_THT:LED_D5.0mm_IRBlack LED_THT:LED_D5.0mm_IRGrey -LED_THT:LED_D8.0mm-3 LED_THT:LED_D8.0mm +LED_THT:LED_D8.0mm-3 LED_THT:LED_Oval_W5.2mm_H3.8mm LED_THT:LED_Rectangular_W3.0mm_H2.0mm LED_THT:LED_Rectangular_W3.9mm_H1.8mm LED_THT:LED_Rectangular_W3.9mm_H1.9mm -LED_THT:LED_Rectangular_W5.0mm_H2.0mm-3Pins LED_THT:LED_Rectangular_W5.0mm_H2.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm-3Pins LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O1.27mm_Z1.0mm LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O1.27mm_Z3.0mm LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O1.27mm_Z5.0mm @@ -9448,10 +10304,10 @@ Module:RaspberryPi_Pico_SMD_HandSolder Module:RaspberryPi_Pico_W_SMD Module:RaspberryPi_Pico_W_SMD_HandSolder Module:Raspberry_Pi_Zero_Socketed_THT_FaceDown_MountingHoles -Module:Sipeed-M1 -Module:Sipeed-M1W Module:ST_Morpho_Connector_144_STLink Module:ST_Morpho_Connector_144_STLink_MountingHoles +Module:Sipeed-M1 +Module:Sipeed-M1W Module:Texas_EUK_R-PDSS-T7_THT Module:Texas_EUS_R-PDSS-T5_THT Module:Texas_EUW_R-PDSS-T7_THT @@ -9635,16 +10491,7 @@ Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H6mm_9771060360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H7mm_9771070360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H8mm_9771080360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H9mm_9771090360 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H10mm_9774100482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H1mm_9774010482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H2mm_9774020482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H3mm_9774030482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H4mm_9774040482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H5mm_9774050482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H6mm_9774060482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H7mm_9774070482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H8mm_9774080482 -Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H9mm_9774090482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H0.5mm_9774005633 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H1.5mm_9774015633 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H1mm_9774010633 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H2.5mm_9774025633 @@ -9669,6 +10516,28 @@ Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H5mm_ThreadDepth2mm_NoNPTH_97730506 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H6mm_ThreadDepth2mm_97730606332 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H6mm_ThreadDepth2mm_97730606334 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H6mm_ThreadDepth2mm_NoNPTH_97730606330 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H0.5mm_9774005151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H1.5mm_9774015151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H10mm_9774100151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H1mm_9774010151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H2.5mm_9774025151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H2.7mm_9774027151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H2mm_9774020151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H3.5mm_9774035151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H3mm_9774030151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H4.5mm_9774045151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H4mm_9774040151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H5.5mm_9774055151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H5mm_9774050151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H6.5mm_9774065151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H6mm_9774060151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H7.5mm_9774075151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H7mm_9774070151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H8.5mm_9774085151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H8mm_9774080151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H9.5mm_9774095151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2.5_H9mm_9774090151 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H0.5mm_9774005243 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H1.5mm_9774015243 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H1mm_9774010243 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H2.5mm_9774025243 @@ -9681,6 +10550,7 @@ Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H5mm_9774050243 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H6mm_9774060243 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H7mm_9774070243 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H8mm_9774080243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H0.5mm_9774005360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H1.5mm_9774015360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H10mm_9774100360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H11mm_9774110360 @@ -9688,6 +10558,7 @@ Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H12mm_9774120360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H13mm_9774130360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H14mm_9774140360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H15mm_9774150360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H16mm_9774160360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H1mm_9774010360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H2.5mm_9774025360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H2mm_9774020360 @@ -9698,6 +10569,16 @@ Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H6mm_9774060360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H7mm_9774070360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H8mm_9774080360 Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H9mm_9774090360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H10mm_9774100482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H1mm_9774010482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H2mm_9774020482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H3mm_9774030482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H4mm_9774040482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H5mm_9774050482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H6mm_9774060482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H7mm_9774070482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H8mm_9774080482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M4_H9mm_9774090482 Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H10.6mm_ReverseMount_9775106960 Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H11.6mm_ReverseMount_9775116960 Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H2.6mm_ReverseMount_9775026960 @@ -9767,6 +10648,7 @@ Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H12mm_9774120960 Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H13mm_9774130960 Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H14mm_9774140960 Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H15mm_9774150960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H16mm_9774160960 Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H1mm_9774010960 Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H2.5mm_9774025960 Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H2mm_9774020960 @@ -9823,6 +10705,7 @@ OptoDevice:Everlight_ITR9608-F OptoDevice:Finder_34.81 OptoDevice:Hamamatsu_C12880 OptoDevice:Hamamatsu_S13360-30CS +OptoDevice:Hamamatsu_S14160-6050HS OptoDevice:Kingbright_KPS-3227 OptoDevice:Kingbright_KPS-5130 OptoDevice:Kingbright_KRC011_Horizontal @@ -9857,9 +10740,9 @@ OptoDevice:Lightpipe_Mentor_1276.2004 OptoDevice:Lite-On_LTR-303ALS-01 OptoDevice:Luna_NSL-32 OptoDevice:Maxim_OLGA-14_3.3x5.6mm_P0.8mm +OptoDevice:ONSemi_QSE15x OptoDevice:OnSemi_CASE100AQ OptoDevice:OnSemi_CASE100CY -OptoDevice:ONSemi_QSE15x OptoDevice:Osram_BP104-SMD OptoDevice:Osram_BPW34S-SMD OptoDevice:Osram_BPW82 @@ -9876,8 +10759,6 @@ OptoDevice:Osram_SMD-SmartDIL OptoDevice:Panasonic_APV-AQY_SSOP-4_4.45x2.65mm_P1.27mm OptoDevice:PerkinElmer_VTL5C OptoDevice:PerkinElmer_VTL5Cx2 -OptoDevice:Renesas_DFN-6_1.5x1.6mm_P0.5mm -OptoDevice:Rohm_RPR-0720 OptoDevice:R_LDR_10x8.5mm_P7.6mm_Vertical OptoDevice:R_LDR_11x9.4mm_P8.2mm_Vertical OptoDevice:R_LDR_12x10.8mm_P9.0mm_Vertical @@ -9889,12 +10770,14 @@ OptoDevice:R_LDR_7x6mm_P5.1mm_Vertical OptoDevice:R_LDR_D13.8mm_P9.0mm_Vertical OptoDevice:R_LDR_D20mm_P17.5mm_Vertical OptoDevice:R_LDR_D6.4mm_P3.4mm_Vertical +OptoDevice:Renesas_DFN-6_1.5x1.6mm_P0.5mm +OptoDevice:Rohm_RPR-0720 +OptoDevice:ST_VL53L0X OptoDevice:Sharp_GP2S700HCP OptoDevice:Sharp_GP2Y0A41SK0F OptoDevice:Sharp_IS471F OptoDevice:Sharp_IS485 OptoDevice:Siemens_SFH900 -OptoDevice:ST_VL53L0X OptoDevice:Toshiba_TORX170_TORX173_TORX193_TORX194 OptoDevice:Toshiba_TOTX170_TOTX173_TOTX193_TOTX194 OptoDevice:Vishay_CAST-3Pin @@ -9909,8 +10792,6 @@ Oscillator:Oscillator_DIP-8 Oscillator:Oscillator_DIP-8_LargePads Oscillator:Oscillator_OCXO_Morion_MV267 Oscillator:Oscillator_OCXO_Morion_MV317 -Oscillator:Oscillator_SeikoEpson_SG-8002DB -Oscillator:Oscillator_SeikoEpson_SG-8002DC Oscillator:Oscillator_SMD_Abracon_ABLNO Oscillator:Oscillator_SMD_Abracon_ASCO-4Pin_1.6x1.2mm Oscillator:Oscillator_SMD_Abracon_ASDMB-4Pin_2.5x2.0mm @@ -9921,7 +10802,8 @@ Oscillator:Oscillator_SMD_Abracon_ASV-4Pin_7.0x5.1mm_HandSoldering Oscillator:Oscillator_SMD_Diodes_FN-4Pin_7.0x5.0mm Oscillator:Oscillator_SMD_ECS_2520MV-xxx-xx-4Pin_2.5x2.0mm Oscillator:Oscillator_SMD_EuroQuartz_XO32-4Pin_3.2x2.5mm -Oscillator:Oscillator_SMD_EuroQuartz_XO32-4Pin_3.2x2.5mm_HandSoldering +Oscillator:Oscillator_SMD_EuroQuartz_XO32-4Pin_3.2x2.5mm_RotB +Oscillator:Oscillator_SMD_EuroQuartz_XO32-4Pin_3.2x2.5mm_RotB_HandSoldering Oscillator:Oscillator_SMD_EuroQuartz_XO53-4Pin_5.0x3.2mm Oscillator:Oscillator_SMD_EuroQuartz_XO53-4Pin_5.0x3.2mm_HandSoldering Oscillator:Oscillator_SMD_EuroQuartz_XO91-4Pin_7.0x5.0mm @@ -9945,6 +10827,8 @@ Oscillator:Oscillator_SMD_IQD_IQXO70-4Pin_7.5x5.0mm_HandSoldering Oscillator:Oscillator_SMD_Kyocera_2520-6Pin_2.5x2.0mm Oscillator:Oscillator_SMD_Kyocera_KC2520Z-4Pin_2.5x2.0mm Oscillator:Oscillator_SMD_OCXO_ConnorWinfield_OH300 +Oscillator:Oscillator_SMD_SI570_SI571_HandSoldering +Oscillator:Oscillator_SMD_SI570_SI571_Standard Oscillator:Oscillator_SMD_SeikoEpson_SG210-4Pin_2.5x2.0mm Oscillator:Oscillator_SMD_SeikoEpson_SG210-4Pin_2.5x2.0mm_HandSoldering Oscillator:Oscillator_SMD_SeikoEpson_SG3030CM @@ -9959,19 +10843,19 @@ Oscillator:Oscillator_SMD_SeikoEpson_SG8002JC-4Pin_10.5x5.0mm_HandSoldering Oscillator:Oscillator_SMD_SeikoEpson_SG8002LB-4Pin_5.0x3.2mm Oscillator:Oscillator_SMD_SeikoEpson_SG8002LB-4Pin_5.0x3.2mm_HandSoldering Oscillator:Oscillator_SMD_SeikoEpson_TG2520SMN-xxx-xxxxxx-4Pin_2.5x2.0mm -Oscillator:Oscillator_SMD_SI570_SI571_HandSoldering -Oscillator:Oscillator_SMD_SI570_SI571_Standard -Oscillator:Oscillator_SMD_Silicon_Labs_LGA-6_2.5x3.2mm_P1.25mm -Oscillator:Oscillator_SMD_SiTime_PQFD-6L_3.2x2.5mm -Oscillator:Oscillator_SMD_SiTime_SiT9121-6Pin_3.2x2.5mm Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_2.0x1.6mm Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_2.5x2.0mm Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_3.2x2.5mm Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_5.0x3.2mm Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_7.0x5.0mm +Oscillator:Oscillator_SMD_SiTime_PQFD-6L_3.2x2.5mm +Oscillator:Oscillator_SMD_SiTime_SiT9121-6Pin_3.2x2.5mm +Oscillator:Oscillator_SMD_Silicon_Labs_LGA-6_2.5x3.2mm_P1.25mm Oscillator:Oscillator_SMD_TCXO_G158 Oscillator:Oscillator_SMD_TXC_7C-4Pin_5.0x3.2mm Oscillator:Oscillator_SMD_TXC_7C-4Pin_5.0x3.2mm_HandSoldering +Oscillator:Oscillator_SeikoEpson_SG-8002DB +Oscillator:Oscillator_SeikoEpson_SG-8002DC Package_BGA:Alliance_TFBGA-36_6x8mm_Layout6x8_P0.75mm Package_BGA:Alliance_TFBGA-54_8x8mm_Layout9x9_P0.8mm Package_BGA:Analog_BGA-165_11.9x16mm_Layout11x15_P1.0mm @@ -9980,20 +10864,26 @@ Package_BGA:Analog_BGA-28_4x6.25mm_Layout4x7_P0.8mm Package_BGA:Analog_BGA-49_6.25x6.25mm_Layout7x7_P0.8mm Package_BGA:Analog_BGA-77_9x15mm_Layout7x11_P1.27mm Package_BGA:BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD +Package_BGA:BGA-100_12x18mm_Layout10x17_P1mm +Package_BGA:BGA-100_14x18mm_Layout10x17_P1mm Package_BGA:BGA-100_6.0x6.0mm_Layout11x11_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD Package_BGA:BGA-1023_33.0x33.0mm_Layout32x32_P1.0mm Package_BGA:BGA-1156_35.0x35.0mm_Layout34x34_P1.0mm Package_BGA:BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD Package_BGA:BGA-1295_37.5x37.5mm_Layout36x36_P1.0mm Package_BGA:BGA-132_12x18mm_Layout11x17_P1.0mm +Package_BGA:BGA-132_12x18mm_Layout11x17_P1mm Package_BGA:BGA-144_13.0x13.0mm_Layout12x12_P1.0mm Package_BGA:BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD Package_BGA:BGA-152_14x18mm_Layout13x17_P0.5mm +Package_BGA:BGA-152_14x18mm_Layout13x17_P1mm Package_BGA:BGA-153_8.0x8.0mm_Layout15x15_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD Package_BGA:BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD Package_BGA:BGA-16_1.92x1.92mm_Layout4x4_P0.5mm Package_BGA:BGA-196_15x15mm_Layout14x14_P1.0mm Package_BGA:BGA-200_10x14.5mm_Layout12x22_P0.8x0.65mm +Package_BGA:BGA-24_6x8mm_Layout5x5_P1.0mm +Package_BGA:BGA-24_8x8mm_Layout5x5_P1.0mm Package_BGA:BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD Package_BGA:BGA-256_14.0x14.0mm_Layout16x16_P0.8mm_Ball0.45mm_Pad0.32mm_NSMD Package_BGA:BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD @@ -10009,6 +10899,7 @@ Package_BGA:BGA-48_8.0x9.0mm_Layout6x8_P0.8mm Package_BGA:BGA-529_19x19mm_Layout23x23_P0.8mm Package_BGA:BGA-624_21x21mm_Layout25x25_P0.8mm Package_BGA:BGA-625_21.0x21.0mm_Layout25x25_P0.8mm +Package_BGA:BGA-63_9x11mm_Layout10x12_P0.8mm Package_BGA:BGA-64_9.0x9.0mm_Layout10x10_P0.8mm Package_BGA:BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD Package_BGA:BGA-676_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD @@ -10019,14 +10910,22 @@ Package_BGA:BGA-96_9.0x13.0mm_Layout2x3x16_P0.8mm Package_BGA:BGA-9_1.6x1.6mm_Layout3x3_P0.5mm Package_BGA:EPC_BGA-4_0.9x0.9mm_Layout2x2_P0.45mm Package_BGA:FB-BGA-484_23.0x23.0mm_Layout22x22_P1.0mm +Package_BGA:FBGA-78_7.5x10.5mm_Layout9x13_P0.8mm +Package_BGA:FBGA-78_7.5x10.6mm_Layout9x13_P0.8mm Package_BGA:FBGA-78_7.5x11mm_Layout2x3x13_P0.8mm +Package_BGA:FBGA-78_8x10.5mm_Layout9x13_P0.8mm +Package_BGA:FBGA-78_9x10.5mm_Layout9x13_P0.8mm +Package_BGA:FBGA-78_9x10.6mm_Layout9x13_P0.8mm +Package_BGA:FBGA-96_7.5x13.5mm_Layout9x16_P0.8mm +Package_BGA:FBGA-96_7.5x13mm_Layout9x16_P0.8mm +Package_BGA:FBGA-96_8x13mm_Layout9x16_P0.8mm +Package_BGA:FBGA-96_8x14mm_Layout9x16_P0.8mm +Package_BGA:FBGA-96_9x13mm_Layout9x16_P0.8mm +Package_BGA:FBGA-96_9x14mm_Layout9x16_P0.8mm +Package_BGA:FCPBGA-780_23x23mm_Layout28x28_P0.8mm Package_BGA:Fujitsu_WLP-15_2.28x3.092mm_Layout3x5_P0.4mm Package_BGA:Infineon_LFBGA-292_17x17mm_Layout20x20_P0.8mm Package_BGA:Infineon_TFBGA-48_6x10mm_Layout6x8_P0.75mm -Package_BGA:Lattice_caBGA-381_17x17mm_Layout20x20_P0.8mm -Package_BGA:Lattice_caBGA-381_17x17mm_Layout20x20_P0.8mm_SMD -Package_BGA:Lattice_caBGA-756_27x27mm_Layout32x32_P0.8mm -Package_BGA:Lattice_iCE40_csBGA-132_8x8mm_Layout14x14_P0.5mm Package_BGA:LFBGA-100_10x10mm_Layout10x10_P0.8mm Package_BGA:LFBGA-144_10x10mm_Layout12x12_P0.8mm Package_BGA:LFBGA-153_11.5x13mm_Layout14x14_P0.5mm @@ -10036,12 +10935,21 @@ Package_BGA:LFBGA-169_14x18mm_Layout14x28_P0.5mm Package_BGA:LFBGA-289_14x14mm_Layout17x17_P0.8mm Package_BGA:LFBGA-400_16x16mm_Layout20x20_P0.8mm Package_BGA:LFBGA-484_18x18mm_Layout22x22_P0.8mm +Package_BGA:Lattice_caBGA-381_17x17mm_Layout20x20_P0.8mm +Package_BGA:Lattice_caBGA-381_17x17mm_Layout20x20_P0.8mm_SMD +Package_BGA:Lattice_caBGA-756_27x27mm_Layout32x32_P0.8mm +Package_BGA:Lattice_iCE40_csBGA-132_8x8mm_Layout14x14_P0.5mm Package_BGA:Linear_BGA-133_15.0x15.0mm_Layout12x12_P1.27mm Package_BGA:MAPBGA-272_9x9mm_Layout17x17_P0.5mm Package_BGA:MAPBGA-289_14x14mm_Layout17x17_P0.8mm Package_BGA:Maxim_WLP-12 Package_BGA:Maxim_WLP-12_2.008x1.608mm_Layout4x3_P0.4mm Package_BGA:Maxim_WLP-9_1.595x1.415_Layout3x3_P0.4mm_Ball0.27mm_Pad0.25mm_NSMD +Package_BGA:Microchip_FCG1152_BGA-1152_35x35mm_Layout34x34_P1.0mm +Package_BGA:Microchip_FCSG325_BGA-325_11x11mm_Layout21x21_P0.5mm +Package_BGA:Microchip_FCSG536_BGA-536_16x16mm_Layout30x30_P0.5mm +Package_BGA:Microchip_FCVG484_BGA-484_19x19mm_Layout22x22_P0.8mm +Package_BGA:Microchip_FCVG784_BGA-784_23x23mm_Layout28x28_P0.8mm Package_BGA:Microchip_TFBGA-196_11x11mm_Layout14x14_P0.75mm_SMD Package_BGA:Micron_FBGA-78_7.5x10.6mm_Layout9x13_P0.8mm Package_BGA:Micron_FBGA-78_8x10.5mm_Layout9x13_P0.8mm @@ -10049,6 +10957,9 @@ Package_BGA:Micron_FBGA-78_9x10.5mm_Layout9x13_P0.8mm Package_BGA:Micron_FBGA-96_7.5x13.5mm_Layout9x16_P0.8mm Package_BGA:Micron_FBGA-96_8x14mm_Layout9x16_P0.8mm Package_BGA:Micron_FBGA-96_9x14mm_Layout9x16_P0.8mm +Package_BGA:NXP_SOT1982-1_VFBGA-98_7x7mm_Layout13x13_P0.5mm +Package_BGA:NXP_SOT2162-1_VFBGA-59_4x4mm_Layout9x9_P0.4mm +Package_BGA:NXP_TFBGA-50_5x5mm_Layout9x9_P0.5mm Package_BGA:NXP_VFBGA-42_2.6x3mm_Layout6x7_P0.4mm Package_BGA:ST_LFBGA-354_16x16mm_Layout19x19_P0.8mm Package_BGA:ST_LFBGA-448_18x18mm_Layout22x22_P0.8mm @@ -10057,39 +10968,15 @@ Package_BGA:ST_TFBGA-225_13x13mm_Layout15x15_P0.8mm Package_BGA:ST_TFBGA-257_10x10mm_Layout19x19_P0.5mmP0.65mm Package_BGA:ST_TFBGA-320_11x11mm_Layout21x21_P0.5mm Package_BGA:ST_TFBGA-361_12x12mm_Layout23x23_P0.5mmP0.65mm +Package_BGA:ST_TFBGA-436_18x18mm_Layout22x22_P0.8mm Package_BGA:ST_UFBGA-121_6x6mm_Layout11x11_P0.5mm Package_BGA:ST_UFBGA-129_7x7mm_Layout13x13_P0.5mm Package_BGA:ST_UFBGA-59_5x5mm_Layout8x8_P0.5mm Package_BGA:ST_UFBGA-73_5x5mm_Layout9x9_P0.5mm Package_BGA:ST_UFBGA-81_5x5mm_Layout9x9_P0.5mm +Package_BGA:ST_VFBGA-361_10x10mm_Layout19x19_P0.5mm +Package_BGA:ST_VFBGA-424_14x14mm_Layout27x27_P0.5mmP0.5x0.5mm_Stagger Package_BGA:ST_uTFBGA-36_3.6x3.6mm_Layout6x6_P0.5mm -Package_BGA:Texas_BGA-289_15x15mm_Layout17x17_P0.8mm -Package_BGA:Texas_DSBGA-10_1.36x1.86mm_Layout3x4_P0.5mm -Package_BGA:Texas_DSBGA-12_1.36x1.86mm_Layout3x4_P0.5mm -Package_BGA:Texas_DSBGA-16_2.39x2.39mm_Layout4x4_P0.5mm -Package_BGA:Texas_DSBGA-28_1.9x3mm_Layout4x7_P0.4mm -Package_BGA:Texas_DSBGA-49_3.33x3.488mm_Layout7x7_P0.4mm -Package_BGA:Texas_DSBGA-5_0.822x1.116mm_Layout2x1x2_P0.4mm -Package_BGA:Texas_DSBGA-5_0.8875x1.3875mm_Layout2x3_P0.5mm -Package_BGA:Texas_DSBGA-5_1.5855x1.6365mm_Layout3x2_P0.5mm -Package_BGA:Texas_DSBGA-64_3.415x3.535mm_Layout8x8_P0.4mm -Package_BGA:Texas_DSBGA-6_0.704x1.054mm_Layout2x3_P0.35mm -Package_BGA:Texas_DSBGA-6_0.757x1.01mm_Layout2x3_P0.35mm -Package_BGA:Texas_DSBGA-6_0.855x1.255mm_Layout2x3_P0.4mm_LevelB -Package_BGA:Texas_DSBGA-6_0.855x1.255mm_Layout2x3_P0.4mm_LevelC -Package_BGA:Texas_DSBGA-6_0.95x1.488mm_Layout2x3_P0.4mm -Package_BGA:Texas_DSBGA-6_0.9x1.4mm_Layout2x3_P0.5mm -Package_BGA:Texas_DSBGA-8_0.705x1.468mm_Layout2x4_P0.4mm -Package_BGA:Texas_DSBGA-8_0.9x1.9mm_Layout2x4_P0.5mm -Package_BGA:Texas_DSBGA-8_1.43x1.41mm_Layout3x3_P0.5mm -Package_BGA:Texas_DSBGA-8_1.5195x1.5195mm_Layout3x3_P0.5mm -Package_BGA:Texas_DSBGA-9_1.4715x1.4715mm_Layout3x3_P0.5mm -Package_BGA:Texas_DSBGA-9_1.62x1.58mm_Layout3x3_P0.5mm -Package_BGA:Texas_MicroStar_Junior_BGA-113_7x7mm_Layout12x12_P0.5mm -Package_BGA:Texas_MicroStar_Junior_BGA-12_2.0x2.5mm_Layout4x3_P0.5mm -Package_BGA:Texas_MicroStar_Junior_BGA-80_5.0x5.0mm_Layout9x9_P0.5mm -Package_BGA:Texas_PicoStar_BGA-4_0.758x0.758mm_Layout2x2_P0.4mm -Package_BGA:Texas_YFP0020_DSBGA-20_1.588x1.988mm_Layout4x5_P0.4mm Package_BGA:TFBGA-100_5.5x5.5mm_Layout10x10_P0.5mm Package_BGA:TFBGA-100_8x8mm_Layout10x10_P0.8mm Package_BGA:TFBGA-100_9.0x9.0mm_Layout10x10_P0.8mm @@ -10108,6 +10995,35 @@ Package_BGA:TFBGA-576_16x16mm_Layout24x24_P0.65mm Package_BGA:TFBGA-644_19x19mm_Layout28x28_P0.65mm Package_BGA:TFBGA-64_5x5mm_Layout8x8_P0.5mm Package_BGA:TFBGA-81_5x5mm_Layout9x9_P0.5mm +Package_BGA:Texas_BGA-289_15x15mm_Layout17x17_P0.8mm +Package_BGA:Texas_DSBGA-10_1.36x1.86mm_Layout3x4_P0.5mm +Package_BGA:Texas_DSBGA-12_1.36x1.86mm_Layout3x4_P0.5mm +Package_BGA:Texas_DSBGA-12_2.11x1.61mm_Layout4x3_P0.5mm +Package_BGA:Texas_DSBGA-16_2.39x2.39mm_Layout4x4_P0.5mm +Package_BGA:Texas_DSBGA-28_1.9x3mm_Layout4x7_P0.4mm +Package_BGA:Texas_DSBGA-49_3.33x3.488mm_Layout7x7_P0.4mm +Package_BGA:Texas_DSBGA-5_0.822x1.116mm_Layout2x1x2_P0.4mm +Package_BGA:Texas_DSBGA-5_0.8875x1.3875mm_Layout2x3_P0.5mm +Package_BGA:Texas_DSBGA-5_1.5855x1.6365mm_Layout3x2_P0.5mm +Package_BGA:Texas_DSBGA-64_3.415x3.535mm_Layout8x8_P0.4mm +Package_BGA:Texas_DSBGA-6_0.704x1.054mm_Layout2x3_P0.35mm +Package_BGA:Texas_DSBGA-6_0.757x1.01mm_Layout2x3_P0.35mm +Package_BGA:Texas_DSBGA-6_0.76x1.16mm_Layout2x3_P0.4mm +Package_BGA:Texas_DSBGA-6_0.855x1.255mm_Layout2x3_P0.4mm_LevelB +Package_BGA:Texas_DSBGA-6_0.855x1.255mm_Layout2x3_P0.4mm_LevelC +Package_BGA:Texas_DSBGA-6_0.95x1.488mm_Layout2x3_P0.4mm +Package_BGA:Texas_DSBGA-6_0.9x1.4mm_Layout2x3_P0.5mm +Package_BGA:Texas_DSBGA-8_0.705x1.468mm_Layout2x4_P0.4mm +Package_BGA:Texas_DSBGA-8_0.9x1.9mm_Layout2x4_P0.5mm +Package_BGA:Texas_DSBGA-8_1.43x1.41mm_Layout3x3_P0.5mm +Package_BGA:Texas_DSBGA-8_1.5195x1.5195mm_Layout3x3_P0.5mm +Package_BGA:Texas_DSBGA-9_1.4715x1.4715mm_Layout3x3_P0.5mm +Package_BGA:Texas_DSBGA-9_1.62x1.58mm_Layout3x3_P0.5mm +Package_BGA:Texas_MicroStar_Junior_BGA-113_7x7mm_Layout12x12_P0.5mm +Package_BGA:Texas_MicroStar_Junior_BGA-12_2.0x2.5mm_Layout4x3_P0.5mm +Package_BGA:Texas_MicroStar_Junior_BGA-80_5.0x5.0mm_Layout9x9_P0.5mm +Package_BGA:Texas_PicoStar_BGA-4_0.758x0.758mm_Layout2x2_P0.4mm +Package_BGA:Texas_YFP0020_DSBGA-20_1.588x1.988mm_Layout4x5_P0.4mm Package_BGA:UCBGA-36_2.5x2.5mm_Layout6x6_P0.4mm Package_BGA:UCBGA-49_3x3mm_Layout7x7_P0.4mm Package_BGA:UCBGA-81_4x4mm_Layout9x9_P0.4mm @@ -10172,6 +11088,8 @@ Package_BGA:Xilinx_RFG676 Package_BGA:Xilinx_RS484 Package_BGA:Xilinx_SBG484 Package_BGA:Xilinx_SBG485 +Package_BGA:csBGA-64_5x5mm_Layout8x8_P0.5mm +Package_BGA:ucBGA-64_4x4mm_Layout8x8_P0.4mm Package_CSP:Analog_LFCSP-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm Package_CSP:Analog_LFCSP-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm_ThermalVias Package_CSP:Analog_LFCSP-16-1EP_4x4mm_P0.65mm_EP2.35x2.35mm @@ -10247,13 +11165,13 @@ Package_CSP:LFCSP-WD-8-1EP_3x3mm_P0.65mm_EP1.6x2.44mm Package_CSP:LFCSP-WD-8-1EP_3x3mm_P0.65mm_EP1.6x2.44mm_ThermalVias Package_CSP:Macronix_WLCSP-12_2.02x2.09mm_Layout4x4_P0.5mm Package_CSP:Maxim_WLCSP-35_2.998x2.168mm_Layout7x5_P0.4mm +Package_CSP:NXP_SOT1444-5_WLCSP-49_3.44x3.44mm_Layout7x7_P0.4mm +Package_CSP:NXP_SOT1450-2_WLCSP-100_5.07x5.07mm_Layout10x10_P0.5mm Package_CSP:Nexperia_WLCSP-15_2.37x1.17mm_Layout6x3_P0.4mmP0.8mm Package_CSP:OnSemi_ODCSP36_BGA-36_6.13x6.13mm_Layout6x6_P1.0mm Package_CSP:OnSemi_ODCSP36_BGA-36_6.13x6.13mm_Layout6x6_P1.0mm_ManualAssembly Package_CSP:OnSemi_ODCSP8_BGA-8_3.16x3.16mm_Layout3x3_P1.26mm Package_CSP:OnSemi_ODCSP8_BGA-8_3.16x3.16mm_Layout3x3_P1.26mm_ManualAssembly -Package_CSP:pSemi_CSP-16_1.64x2.04mm_P0.4mm -Package_CSP:pSemi_CSP-16_1.64x2.04mm_P0.4mm_Pad0.18mm Package_CSP:ST_WLCSP-100_4.437x4.456mm_Layout10x10_P0.4mm Package_CSP:ST_WLCSP-100_4.4x4.38mm_Layout10x10_P0.4mm_Offcenter Package_CSP:ST_WLCSP-100_Die422 @@ -10274,6 +11192,7 @@ Package_CSP:ST_WLCSP-156_4.96x4.64mm_Layout13x12_P0.35mm Package_CSP:ST_WLCSP-168_Die434 Package_CSP:ST_WLCSP-180_Die451 Package_CSP:ST_WLCSP-18_1.86x2.14mm_Layout7x5_P0.4mm_Stagger +Package_CSP:ST_WLCSP-19_1.643x2.492mm_Layout4x11_P0.35mm_Stagger Package_CSP:ST_WLCSP-208_5.38x5.47mm_Layout26x16_P0.35mm_Stagger Package_CSP:ST_WLCSP-208_5.8x5.6mm_Layout26x16_P0.35mm_Stagger Package_CSP:ST_WLCSP-20_1.94x2.4mm_Layout4x5_P0.4mm @@ -10283,15 +11202,17 @@ Package_CSP:ST_WLCSP-25_Die425 Package_CSP:ST_WLCSP-25_Die444 Package_CSP:ST_WLCSP-25_Die457 Package_CSP:ST_WLCSP-27_2.34x2.55mm_Layout9x6_P0.4mm_Stagger -Package_CSP:ST_WLCSP-27_2.55x2.34mm_P0.40mm_Stagger Package_CSP:ST_WLCSP-36_2.58x3.07mm_Layout6x6_P0.4mm +Package_CSP:ST_WLCSP-36_2.652x2.592mm_Layout7x12_P0.4mm_Stagger_Offcenter +Package_CSP:ST_WLCSP-36_2.83x2.99mm_Layout7x13_P0.4mm_Stagger_Offcenter Package_CSP:ST_WLCSP-36_Die417 Package_CSP:ST_WLCSP-36_Die440 Package_CSP:ST_WLCSP-36_Die445 Package_CSP:ST_WLCSP-36_Die458 +Package_CSP:ST_WLCSP-39_2.76x2.78mm_Layout11x7_P0.4mm_Stagger Package_CSP:ST_WLCSP-41_2.98x2.76mm_Layout13x7_P0.4mm_Stagger -Package_CSP:ST_WLCSP-42_2.82x2.93mm_P0.40mm_Stagger Package_CSP:ST_WLCSP-42_2.93x2.82mm_Layout12x7_P0.4mm_Stagger +Package_CSP:ST_WLCSP-49_3.14x3.14mm_Layout7x7_P0.4mm_Offcenter Package_CSP:ST_WLCSP-49_3.15x3.13mm_Layout7x7_P0.4mm Package_CSP:ST_WLCSP-49_3.3x3.38mm_Layout7x7_P0.4mm_Offcenter Package_CSP:ST_WLCSP-49_Die423 @@ -10339,21 +11260,28 @@ Package_CSP:WLCSP-36_2.82x2.67mm_Layout6x6_P0.4mm Package_CSP:WLCSP-4_0.64x0.64mm_Layout2x2_P0.35mm Package_CSP:WLCSP-4_0.89x0.89mm_Layout2x2_P0.5mm Package_CSP:WLCSP-56_3.170x3.444mm_Layout7x8_P0.4mm +Package_CSP:WLCSP-6_1.46x1.1mm_Layout3x2_P0.4mm Package_CSP:WLCSP-6_1.4x1.0mm_P0.4mm Package_CSP:WLCSP-81_4.41x3.76mm_P0.4mm Package_CSP:WLCSP-8_1.551x2.284mm_Layout2x4_P0.5mm Package_CSP:WLCSP-8_1.58x1.63x0.35mm_Layout3x5_P0.35x0.4mm_Ball0.25mm_Pad0.25mm_NSMD Package_CSP:WLCSP-9_1.21x1.22mm_Layout3x3_P0.4mm +Package_CSP:Xilinx_CSG48_7.0x7.0mm_Layout7x7_P0.8mm +Package_CSP:pSemi_CSP-16_1.64x2.04mm_P0.4mm +Package_CSP:pSemi_CSP-16_1.64x2.04mm_P0.4mm_Pad0.18mm Package_DFN_QFN:AMS_QFN-4-1EP_2x2mm_P0.95mm_EP0.7x1.6mm -Package_DFN_QFN:Analog_QFN-28-36-2EP_5x6mm_P0.5mm Package_DFN_QFN:AO_AOZ666xDI_DFN-8-1EP_3x3mm_P0.65mm_EP1.25x2.7mm Package_DFN_QFN:AO_DFN-8-1EP_5.55x5.2mm_P1.27mm_EP4.12x4.6mm +Package_DFN_QFN:Analog_QFN-28-36-2EP_5x6mm_P0.5mm Package_DFN_QFN:ArtInChip_QFN-100-1EP_12x12mm_P0.4mm_EP7.4x7.4mm Package_DFN_QFN:ArtInChip_QFN-100-1EP_12x12mm_P0.4mm_EP7.4x7.4mm_ThermalVias Package_DFN_QFN:ArtInChip_QFN-68-1EP_7x7mm_P0.35mm_EP5.49x5.49mm Package_DFN_QFN:ArtInChip_QFN-68-1EP_7x7mm_P0.35mm_EP5.49x5.49mm_ThermalVias Package_DFN_QFN:ArtInChip_QFN-88-1EP_10x10mm_P0.4mm_EP6.74x6.74mm Package_DFN_QFN:ArtInChip_QFN-88-1EP_10x10mm_P0.4mm_EP6.74x6.74mm_ThermalVias +Package_DFN_QFN:CDFN-4_2.5x3.2mm_P2.1mm +Package_DFN_QFN:CDFN-4_2x2.5mm_P1.65mm +Package_DFN_QFN:CDFN-4_3.2x5mm_P2.54mm Package_DFN_QFN:Cypress_QFN-56-1EP_8x8mm_P0.5mm_EP6.22x6.22mm_ThermalVias Package_DFN_QFN:DFN-10-1EP_2.6x2.6mm_P0.5mm_EP1.3x2.2mm Package_DFN_QFN:DFN-10-1EP_2.6x2.6mm_P0.5mm_EP1.3x2.2mm_ThermalVias @@ -10361,6 +11289,8 @@ Package_DFN_QFN:DFN-10-1EP_2x3mm_P0.5mm_EP0.64x2.4mm Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.55x2.48mm Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.58x2.35mm Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.58x2.35mm_ThermalVias +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.646x3.1mm +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.646x3.1mm_ThermalVias Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.65x2.38mm Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.75x2.7mm @@ -10426,9 +11356,12 @@ Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.7x2.4mm -Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.7x2.4mm_ThermalVias +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.2x2.15mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.2x2.15mm_ThermalVias Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.55x2.4mm Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.5x2.25mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.6x2.56mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.6x2.56mm_ThermalVias Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.7x2.05mm Package_DFN_QFN:DFN-8-1EP_4x4mm_P0.8mm_EP2.39x2.21mm Package_DFN_QFN:DFN-8-1EP_4x4mm_P0.8mm_EP2.3x3.24mm @@ -10438,10 +11371,13 @@ Package_DFN_QFN:DFN-8-1EP_6x5mm_P1.27mm_EP4x4mm Package_DFN_QFN:DFN-8_2x2mm_P0.5mm Package_DFN_QFN:DFN-S-8-1EP_6x5mm_P1.27mm Package_DFN_QFN:DHVQFN-14-1EP_2.5x3mm_P0.5mm_EP1x1.5mm +Package_DFN_QFN:DHVQFN-14-1EP_2.5x3mm_P0.5mm_EP1x1.5mm_ThermalVias Package_DFN_QFN:DHVQFN-16-1EP_2.5x3.5mm_P0.5mm_EP1x2mm Package_DFN_QFN:DHVQFN-20-1EP_2.5x4.5mm_P0.5mm_EP1x3mm +Package_DFN_QFN:DHWQFN-14-1EP_2.5x3mm_P0.5mm_EP1x1.5mm +Package_DFN_QFN:DHWQFN-14-1EP_2.5x3mm_P0.5mm_EP1x1.5mm_ThermalVias Package_DFN_QFN:Diodes_DFN1006-3 -Package_DFN_QFN:Diodes_UDFN-10_1.0x2.5mm_P0.5mm +Package_DFN_QFN:Diodes_UDFN-10_1x2.5mm_P0.5mm Package_DFN_QFN:Diodes_UDFN2020-6_Type-F Package_DFN_QFN:Diodes_UDFN3810-9_TYPE_B Package_DFN_QFN:Diodes_ZL32_TQFN-32-1EP_3x6mm_P0.4mm_EP1.25x3.5mm @@ -10454,25 +11390,39 @@ Package_DFN_QFN:HVQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm Package_DFN_QFN:HVQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias Package_DFN_QFN:HVQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm Package_DFN_QFN:HVQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:HVQFN-36-1EP_6x6mm_P0.5mm_EP3.9x3.9mm +Package_DFN_QFN:HVQFN-36-1EP_6x6mm_P0.5mm_EP3.9x3.9mm_ThermalVias Package_DFN_QFN:HVQFN-40-1EP_6x6mm_P0.5mm_EP4.1x4.1mm Package_DFN_QFN:HVQFN-40-1EP_6x6mm_P0.5mm_EP4.1x4.1mm_ThermalVias Package_DFN_QFN:HXQFN-16-1EP_3x3mm_P0.5mm_EP1.85x1.85mm Package_DFN_QFN:HXQFN-16-1EP_3x3mm_P0.5mm_EP1.85x1.85mm_ThermalVias +Package_DFN_QFN:IDT_QFN-12-1EP_2x2mm_P0.5mm_EP1.1x1.1mm +Package_DFN_QFN:IDT_QFN-12-1EP_2x2mm_P0.5mm_EP1.1x1.1mm_ThermalVias Package_DFN_QFN:Infineon_MLPQ-16-14-1EP_4x4mm_P0.5mm Package_DFN_QFN:Infineon_MLPQ-40-32-1EP_7x7mm_P0.5mm Package_DFN_QFN:Infineon_MLPQ-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm Package_DFN_QFN:Infineon_MLPQ-48-1EP_7x7mm_P0.5mm_EP5.55x5.55mm Package_DFN_QFN:Infineon_PQFN-22-15-4EP_6x5mm_P0.65mm Package_DFN_QFN:Infineon_PQFN-44-31-5EP_7x7mm_P0.5mm -Package_DFN_QFN:Linear_DE14MA -Package_DFN_QFN:Linear_UGK52_QFN-46-52 Package_DFN_QFN:LQFN-10-1EP_2x2mm_P0.5mm_EP0.7x0.7mm Package_DFN_QFN:LQFN-12-1EP_2x2mm_P0.5mm_EP0.7x0.7mm Package_DFN_QFN:LQFN-16-1EP_3x3mm_P0.5mm_EP1.7x1.7mm +Package_DFN_QFN:Linear_DE14MA +Package_DFN_QFN:Linear_UGK52_QFN-46-52 +Package_DFN_QFN:MLF-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:MLF-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:MLF-6-1EP_1.6x1.6mm_P0.5mm_EP0.5x1.26mm +Package_DFN_QFN:MLF-8-1EP_3x3mm_P0.65mm_EP1.55x2.3mm +Package_DFN_QFN:MLF-8-1EP_3x3mm_P0.65mm_EP1.55x2.3mm_ThermalVias +Package_DFN_QFN:MLPQ-16-1EP_4x4mm_P0.65mm_EP2.8x2.8mm +Package_DFN_QFN:MPS_QFN-12_2x2mm_P0.4mm +Package_DFN_QFN:MPS_QFN-16_3x3mm_P0.5mm Package_DFN_QFN:Maxim_FC2QFN-14_2.5x2.5mm_P0.5mm +Package_DFN_QFN:Maxim_TDFN-10-1EP_3x3mm_P0.5mm_EP1.58x2.35mm Package_DFN_QFN:Maxim_TDFN-12-1EP_3x3mm_P0.5mm_EP1.7x2.5mm Package_DFN_QFN:Maxim_TDFN-12-1EP_3x3mm_P0.5mm_EP1.7x2.5mm_ThermalVias Package_DFN_QFN:Maxim_TDFN-6-1EP_3x3mm_P0.95mm_EP1.5x2.3mm +Package_DFN_QFN:Maxim_WSON-8-1EP_3x2mm_P0.5mm_EP1.75x1.63mm Package_DFN_QFN:Micrel_MLF-8-1EP_2x2mm_P0.5mm_EP0.6x1.2mm Package_DFN_QFN:Micrel_MLF-8-1EP_2x2mm_P0.5mm_EP0.6x1.2mm_ThermalVias Package_DFN_QFN:Micrel_MLF-8-1EP_2x2mm_P0.5mm_EP0.8x1.3mm_ThermalVias @@ -10484,17 +11434,11 @@ Package_DFN_QFN:Microchip_DRQFN-64-1EP_7x7mm_P0.65mm_EP4.1x4.1mm_ThermalVias Package_DFN_QFN:Microsemi_QFN-40-32-2EP_6x8mm_P0.5mm Package_DFN_QFN:Mini-Circuits_DL805 Package_DFN_QFN:Mini-Circuits_FG873-4_3x3mm -Package_DFN_QFN:MLF-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm -Package_DFN_QFN:MLF-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias -Package_DFN_QFN:MLF-6-1EP_1.6x1.6mm_P0.5mm_EP0.5x1.26mm -Package_DFN_QFN:MLF-8-1EP_3x3mm_P0.65mm_EP1.55x2.3mm -Package_DFN_QFN:MLF-8-1EP_3x3mm_P0.65mm_EP1.55x2.3mm_ThermalVias -Package_DFN_QFN:MLPQ-16-1EP_4x4mm_P0.65mm_EP2.8x2.8mm -Package_DFN_QFN:MPS_QFN-12_2x2mm_P0.4mm -Package_DFN_QFN:Nordic_AQFN-73-1EP_7x7mm_P0.5mm -Package_DFN_QFN:Nordic_AQFN-94-1EP_7x7mm_P0.4mm Package_DFN_QFN:NXP_LQFN-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm_16xMask0.45x0.45 Package_DFN_QFN:NXP_LQFN-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm_16xMask0.45x0.45_ThermalVias +Package_DFN_QFN:NXP_VQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm +Package_DFN_QFN:Nordic_AQFN-73-1EP_7x7mm_P0.5mm +Package_DFN_QFN:Nordic_AQFN-94-1EP_7x7mm_P0.4mm Package_DFN_QFN:OnSemi_DFN-14-1EP_4x4mm_P0.5mm_EP2.7x3.4mm Package_DFN_QFN:OnSemi_DFN-8_2x2mm_P0.5mm Package_DFN_QFN:OnSemi_SIP-38-6EP-9x7mm_P0.65mm_EP1.2x1.2mm @@ -10503,10 +11447,12 @@ Package_DFN_QFN:OnSemi_UDFN-8_1.2x1.8mm_P0.4mm Package_DFN_QFN:OnSemi_VCT-28_3.5x3.5mm_P0.4mm Package_DFN_QFN:OnSemi_XDFN-10_1.35x2.2mm_P0.4mm Package_DFN_QFN:OnSemi_XDFN4-1EP_1.0x1.0mm_EP0.52x0.52mm +Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic Package_DFN_QFN:Panasonic_HQFN-16-1EP_4x4mm_P0.65mm_EP2.9x2.9mm Package_DFN_QFN:Panasonic_HSON-8_8x8mm_P2.00mm -Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.51mm_EP1.45x1.45mm +Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.45x1.45mm +Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.45x1.45mm_ThermalVias Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.65x1.65mm Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.65x1.65mm_ThermalVias Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.6x1.6mm @@ -10592,9 +11538,6 @@ Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.3x2.3mm Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.3x2.3mm_ThermalVias Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.4x2.4mm Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.4x2.4mm_ThermalVias -Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.6x2.6mm -Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.6x2.6mm_ThermalVias -Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.7x2.7mm Package_DFN_QFN:QFN-28-1EP_4x5mm_P0.5mm_EP2.65x3.65mm Package_DFN_QFN:QFN-28-1EP_4x5mm_P0.5mm_EP2.65x3.65mm_ThermalVias Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm @@ -10646,8 +11589,6 @@ Package_DFN_QFN:QFN-36-1EP_6x6mm_P0.5mm_EP4.1x4.1mm Package_DFN_QFN:QFN-36-1EP_6x6mm_P0.5mm_EP4.1x4.1mm_ThermalVias Package_DFN_QFN:QFN-38-1EP_4x6mm_P0.4mm_EP2.65x4.65mm Package_DFN_QFN:QFN-38-1EP_4x6mm_P0.4mm_EP2.65x4.65mm_ThermalVias -Package_DFN_QFN:QFN-38-1EP_5x7mm_P0.5mm_EP3.15x5.15mm -Package_DFN_QFN:QFN-38-1EP_5x7mm_P0.5mm_EP3.15x5.15mm_ThermalVias Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm_ThermalVias Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.8x3.8mm @@ -10670,6 +11611,8 @@ Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.2x4.2mm Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.2x4.2mm_ThermalVias Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.3x4.3mm Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.3x4.3mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.4x4.4mm +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.4x4.4mm_ThermalVias Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.66x4.66mm Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.66x4.66mm_ThermalVias Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.6x4.6mm @@ -10753,17 +11696,22 @@ Package_DFN_QFN:QFN-76-1EP_9x9mm_P0.4mm_EP3.8x3.8mm Package_DFN_QFN:QFN-76-1EP_9x9mm_P0.4mm_EP3.8x3.8mm_ThermalVias Package_DFN_QFN:QFN-76-1EP_9x9mm_P0.4mm_EP5.81x6.31mm Package_DFN_QFN:QFN-76-1EP_9x9mm_P0.4mm_EP5.81x6.31mm_ThermalVias +Package_DFN_QFN:QFN-8-1EP_6x5mm_P1.27mm_EP3.4x4.2mm Package_DFN_QFN:QFN-80-1EP_10x10mm_P0.4mm_EP3.4x3.4mm Package_DFN_QFN:QFN-80-1EP_10x10mm_P0.4mm_EP3.4x3.4mm_ThermalVias Package_DFN_QFN:Qorvo_DFN-8-1EP_2x2mm_P0.5mm Package_DFN_QFN:Qorvo_TQFN66-48-1EP_6x6mm_P0.4mm_EP4.2x4.2mm Package_DFN_QFN:Qorvo_TQFN66-48-1EP_6x6mm_P0.4mm_EP4.2x4.2mm_ThermalVias Package_DFN_QFN:ROHM_DFN0604-3 -Package_DFN_QFN:SiliconLabs_QFN-20-1EP_3x3mm_P0.5mm_EP1.8x1.8mm -Package_DFN_QFN:SiliconLabs_QFN-20-1EP_3x3mm_P0.5mm_EP1.8x1.8mm_ThermalVias +Package_DFN_QFN:Renesas_UQFN-16_2x2mm_P0.4mm +Package_DFN_QFN:Renesas_UQFN-20_2x3mm_P0.4mm_LayoutBorder4x6y +Package_DFN_QFN:ST_DFN-10-1EP_3x3mm_P0.5mm_EP0.62x0.98mm +Package_DFN_QFN:ST_DFN-10-1EP_3x3mm_P0.5mm_EP0.62x0.98mm_ThermalVias Package_DFN_QFN:ST_UFDFPN-12-1EP_3x3mm_P0.5mm_EP1.4x2.55mm Package_DFN_QFN:ST_UFQFPN-20_3x3mm_P0.5mm Package_DFN_QFN:ST_UQFN-6L_1.5x1.7mm_P0.5mm +Package_DFN_QFN:SiliconLabs_QFN-20-1EP_3x3mm_P0.5mm_EP1.8x1.8mm +Package_DFN_QFN:SiliconLabs_QFN-20-1EP_3x3mm_P0.5mm_EP1.8x1.8mm_ThermalVias Package_DFN_QFN:TDFN-10-1EP_2x3mm_P0.5mm_EP0.9x2mm Package_DFN_QFN:TDFN-10-1EP_2x3mm_P0.5mm_EP0.9x2mm_ThermalVias Package_DFN_QFN:TDFN-12-1EP_3x3mm_P0.4mm_EP1.7x2.45mm @@ -10771,6 +11719,7 @@ Package_DFN_QFN:TDFN-12-1EP_3x3mm_P0.4mm_EP1.7x2.45mm_ThermalVias Package_DFN_QFN:TDFN-12_2x3mm_P0.5mm Package_DFN_QFN:TDFN-14-1EP_3x3mm_P0.4mm_EP1.78x2.35mm Package_DFN_QFN:TDFN-14-1EP_3x3mm_P0.4mm_EP1.78x2.35mm_ThermalVias +Package_DFN_QFN:TDFN-6-1EP_1.2x1.2mm_P0.4mm_EP0.3x0.94mm Package_DFN_QFN:TDFN-6-1EP_2.5x2.5mm_P0.65mm_EP1.3x2mm Package_DFN_QFN:TDFN-6-1EP_2.5x2.5mm_P0.65mm_EP1.3x2mm_ThermalVias Package_DFN_QFN:TDFN-8-1EP_2x2mm_P0.5mm_EP0.8x1.2mm @@ -10779,14 +11728,60 @@ Package_DFN_QFN:TDFN-8-1EP_3x2mm_P0.5mm_EP1.4x1.4mm Package_DFN_QFN:TDFN-8-1EP_3x2mm_P0.5mm_EP1.80x1.65mm Package_DFN_QFN:TDFN-8-1EP_3x2mm_P0.5mm_EP1.80x1.65mm_ThermalVias Package_DFN_QFN:TDFN-8_1.4x1.6mm_P0.4mm +Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.23x1.23mm +Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.23x1.23mm_ThermalVias +Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm +Package_DFN_QFN:TQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm +Package_DFN_QFN:TQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP2.29x2.29mm +Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP2.29x2.29mm_ThermalVias +Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP3.1x3.1mm +Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.9x2.9mm +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.9x2.9mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.1x3.1mm +Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.25x3.25mm +Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.25x3.25mm_ThermalVias +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_PullBack +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_PullBack_ThermalVias +Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm +Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm_ThermalVias +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.4x3.4mm +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.4x3.4mm_ThermalVias +Package_DFN_QFN:TQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm +Package_DFN_QFN:TQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm_ThermalVias +Package_DFN_QFN:TQFN-44-1EP_7x7mm_P0.5mm_EP4.7x4.7mm +Package_DFN_QFN:TQFN-44-1EP_7x7mm_P0.5mm_EP4.7x4.7mm_ThermalVias +Package_DFN_QFN:TQFN-48-1EP_7x7mm_P0.5mm_EP5.1x5.1mm +Package_DFN_QFN:TQFN-48-1EP_7x7mm_P0.5mm_EP5.1x5.1mm_ThermalVias Package_DFN_QFN:Texas_B3QFN-14-1EP_5x5.5mm_P0.65mm Package_DFN_QFN:Texas_B3QFN-14-1EP_5x5.5mm_P0.65mm_ThermalVia +Package_DFN_QFN:Texas_DLH0010A_WSON-10-1EP_2.2x2mm_P0.4mm_EP0.9x1.5mm +Package_DFN_QFN:Texas_DLH0010A_WSON-10-1EP_2.2x2mm_P0.4mm_EP0.9x1.5mm_ThermalVias Package_DFN_QFN:Texas_DRB0008A +Package_DFN_QFN:Texas_DSQ0010A_WSON-10-1EP_2x2mm_P0.4mm_EP0.9x1.5mm +Package_DFN_QFN:Texas_DSQ0010A_WSON-10-1EP_2x2mm_P0.4mm_EP0.9x1.5mm_ThermalVias Package_DFN_QFN:Texas_MOF0009A Package_DFN_QFN:Texas_PicoStar_DFN-3_0.69x0.60mm Package_DFN_QFN:Texas_QFN-41_10x16mm Package_DFN_QFN:Texas_R-PUQFN-N10 Package_DFN_QFN:Texas_R-PUQFN-N12 +Package_DFN_QFN:Texas_RDX0007A_QFN-FCMOD-7-3.3x4mm-P0.5mm_4EP +Package_DFN_QFN:Texas_REE0036A_WQFN-36-1EP_4x5mm_P0.4mm_EP2.7x3.7mm +Package_DFN_QFN:Texas_REE0036A_WQFN-36-1EP_4x5mm_P0.4mm_EP2.7x3.7mm_ThermalVias Package_DFN_QFN:Texas_REF0038A_WQFN-38-2EP_6x4mm_P0.4 Package_DFN_QFN:Texas_RGC0064B_VQFN-64-1EP_9x9mm_P0.5mm_EP4.25x4.25mm Package_DFN_QFN:Texas_RGC0064B_VQFN-64-1EP_9x9mm_P0.5mm_EP4.25x4.25mm_ThermalVias @@ -10822,6 +11817,8 @@ Package_DFN_QFN:Texas_RHB0032E_VQFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm Package_DFN_QFN:Texas_RHB0032E_VQFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm_ThermalVias Package_DFN_QFN:Texas_RHB0032M_VQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm Package_DFN_QFN:Texas_RHB0032M_VQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:Texas_RHF0024A_VQFN-24-1EP_4x5mm_P0.5mm_EP2.65x3.65mm +Package_DFN_QFN:Texas_RHF0024A_VQFN-24-1EP_4x5mm_P0.5mm_EP2.65x3.65mm_ThermalVias Package_DFN_QFN:Texas_RHH0036C_VQFN-36-1EP_6x6mm_P0.5mm_EP4.4x4.4mm Package_DFN_QFN:Texas_RHH0036C_VQFN-36-1EP_6x6mm_P0.5mm_EP4.4x4.4mm_ThermalVias Package_DFN_QFN:Texas_RJE0020A_VQFN-20-1EP_3x3mm_P0.45mm_EP0.675x0.76mm @@ -10829,10 +11826,18 @@ Package_DFN_QFN:Texas_RJE0020A_VQFN-20-1EP_3x3mm_P0.45mm_EP0.675x0.76mm_ThermalV Package_DFN_QFN:Texas_RMG0012A_WQFN-12_1.8x1.8mm_P0.4mm Package_DFN_QFN:Texas_RMQ0024A_WQFN-24-1EP_3x3mm_P0.4mm_EP1.9x1.9mm Package_DFN_QFN:Texas_RMQ0024A_WQFN-24-1EP_3x3mm_P0.4mm_EP1.9x1.9mm_ThermalVias +Package_DFN_QFN:Texas_RNH0030A_WQFN-30-1EP_2.5x4.5mm_P0.4mm_EP1.2x3.2mm +Package_DFN_QFN:Texas_RNH0030A_WQFN-30-1EP_2.5x4.5mm_P0.4mm_EP1.2x3.2mm_ThermalVias Package_DFN_QFN:Texas_RNN0018A Package_DFN_QFN:Texas_RNP0030B_WQFN-30-1EP_4x6mm_P0.5mm_EP1.8x4.5mm Package_DFN_QFN:Texas_RNP0030B_WQFN-30-1EP_4x6mm_P0.5mm_EP1.8x4.5mm_ThermalVias +Package_DFN_QFN:Texas_RNQ0040A_WQFN-40-1EP_6x4mm_P0.4mm_EP4.7x2.7mm +Package_DFN_QFN:Texas_RNQ0040A_WQFN-40-1EP_6x4mm_P0.4mm_EP4.7x2.7mm_ThermalVias +Package_DFN_QFN:Texas_RNX0012C_VQFN-14-11-1EP_2x3mm_P0.5mm_EP0.25x1.825mm Package_DFN_QFN:Texas_RPU0010A_VQFN-HR-10_2x2mm_P0.5mm +Package_DFN_QFN:Texas_RQM0029A_VQFN-29_4x4mm_P0.4mm +Package_DFN_QFN:Texas_RRW0024A_WQFN-24-1EP_3x3mm_P0.4mm_EP1.9x1.9mm +Package_DFN_QFN:Texas_RRW0024A_WQFN-24-1EP_3x3mm_P0.4mm_EP1.9x1.9mm_ThermalVias Package_DFN_QFN:Texas_RSA_VQFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm Package_DFN_QFN:Texas_RSA_VQFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm_ThermalVias Package_DFN_QFN:Texas_RSN_WQFN-32-1EP_4x4mm_P0.4mm_EP2.8x2.8mm @@ -10846,11 +11851,17 @@ Package_DFN_QFN:Texas_RTW_WQFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.7mm Package_DFN_QFN:Texas_RTW_WQFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias Package_DFN_QFN:Texas_RTY_WQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm Package_DFN_QFN:Texas_RTY_WQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:Texas_RUK0020B_WQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm +Package_DFN_QFN:Texas_RUK0020B_WQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm_ThermalVias Package_DFN_QFN:Texas_RUM0016A_WQFN-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm Package_DFN_QFN:Texas_RUM0016A_WQFN-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:Texas_RUM0016E_WQFN-16-1EP_4x4mm_P0.65mm_EP2.5x2.5mm +Package_DFN_QFN:Texas_RUM0016E_WQFN-16-1EP_4x4mm_P0.65mm_EP2.5x2.5mm_ThermalVias Package_DFN_QFN:Texas_RUN0010A_WQFN-10_2x2mm_P0.5mm Package_DFN_QFN:Texas_RVA_VQFN-16-1EP_3.5x3.5mm_P0.5mm_EP2.14x2.14mm Package_DFN_QFN:Texas_RVA_VQFN-16-1EP_3.5x3.5mm_P0.5mm_EP2.14x2.14mm_ThermalVias +Package_DFN_QFN:Texas_RVC0020A_WQFN-20-1EP_3x4mm_P0.5mm_EP1.6x2.6mm +Package_DFN_QFN:Texas_RVC0020A_WQFN-20-1EP_3x4mm_P0.5mm_EP1.6x2.6mm_ThermalVias Package_DFN_QFN:Texas_RVE0028A_VQFN-28-1EP_3.5x4.5mm_P0.4mm_EP2.1x3.1mm Package_DFN_QFN:Texas_RVE0028A_VQFN-28-1EP_3.5x4.5mm_P0.4mm_EP2.1x3.1mm_ThermalVias Package_DFN_QFN:Texas_RWH0032A @@ -10865,55 +11876,19 @@ Package_DFN_QFN:Texas_S-PWQFN-N100_EP5.5x5.5mm_ThermalVias Package_DFN_QFN:Texas_S-PWQFN-N20 Package_DFN_QFN:Texas_S-PX2QFN-14 Package_DFN_QFN:Texas_UQFN-10_1.5x2mm_P0.5mm +Package_DFN_QFN:Texas_UQFN-8_1.5x1.5mm_P0.5mm Package_DFN_QFN:Texas_VQFN-HR-12_2x2.5mm_P0.5mm Package_DFN_QFN:Texas_VQFN-HR-12_2x2.5mm_P0.5mm_ThermalVias Package_DFN_QFN:Texas_VQFN-HR-20_3x2.5mm_P0.5mm_RQQ0011A Package_DFN_QFN:Texas_VQFN-RHL-20 Package_DFN_QFN:Texas_VQFN-RHL-20_ThermalVias Package_DFN_QFN:Texas_VQFN-RNR0011A-11 +Package_DFN_QFN:Texas_WQFN-40-1EP_3x6mm_P0.4mm_EP1.7x4.5mm +Package_DFN_QFN:Texas_WQFN-40-1EP_3x6mm_P0.4mm_EP1.7x4.5mm_ThermalVias Package_DFN_QFN:Texas_WQFN-MR-100_3x3-DapStencil Package_DFN_QFN:Texas_WQFN-MR-100_ThermalVias_3x3-DapStencil Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm Package_DFN_QFN:Texas_X2QFN-RUE-12_1.4x2mm_P0.4mm -Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.23x1.23mm -Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.23x1.23mm_ThermalVias -Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm -Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP2.29x2.29mm -Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP2.29x2.29mm_ThermalVias -Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP3.1x3.1mm -Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP3.1x3.1mm_ThermalVias -Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.1x2.1mm -Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.1x2.1mm_ThermalVias -Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm -Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias -Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.9x2.9mm -Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.9x2.9mm_ThermalVias -Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.1x3.1mm -Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.1x3.1mm_ThermalVias -Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.25x3.25mm -Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.25x3.25mm_ThermalVias -Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm -Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm_ThermalVias -Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm -Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias -Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_PullBack -Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_PullBack_ThermalVias -Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm -Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm_ThermalVias -Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm -Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm_ThermalVias -Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm -Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm_ThermalVias -Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm -Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias -Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.4x3.4mm -Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.4x3.4mm_ThermalVias -Package_DFN_QFN:TQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm -Package_DFN_QFN:TQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm_ThermalVias -Package_DFN_QFN:TQFN-44-1EP_7x7mm_P0.5mm_EP4.7x4.7mm -Package_DFN_QFN:TQFN-44-1EP_7x7mm_P0.5mm_EP4.7x4.7mm_ThermalVias -Package_DFN_QFN:TQFN-48-1EP_7x7mm_P0.5mm_EP5.1x5.1mm -Package_DFN_QFN:TQFN-48-1EP_7x7mm_P0.5mm_EP5.1x5.1mm_ThermalVias Package_DFN_QFN:UDC-QFN-20-4EP_3x4mm_P0.5mm_EP0.41x0.25mm Package_DFN_QFN:UDFN-10_1.35x2.6mm_P0.5mm Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm @@ -10923,6 +11898,7 @@ Package_DFN_QFN:UFQFPN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm_ThermalVias Package_DFN_QFN:UQFN-10_1.3x1.8mm_P0.4mm Package_DFN_QFN:UQFN-10_1.4x1.8mm_P0.4mm Package_DFN_QFN:UQFN-10_1.6x2.1mm_P0.5mm +Package_DFN_QFN:UQFN-12-1EP_2x2mm_P0.4mm_EP1.1x1.1mm Package_DFN_QFN:UQFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm Package_DFN_QFN:UQFN-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm Package_DFN_QFN:UQFN-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm_ThermalVias @@ -10937,6 +11913,7 @@ Package_DFN_QFN:UQFN-20-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_ThermalVias Package_DFN_QFN:UQFN-20_3x3mm_P0.4mm Package_DFN_QFN:UQFN-28-1EP_4x4mm_P0.4mm_EP2.35x2.35mm Package_DFN_QFN:UQFN-28-1EP_4x4mm_P0.4mm_EP2.35x2.35mm_ThermalVias +Package_DFN_QFN:UQFN-32_5x5mm_P0.5mm Package_DFN_QFN:UQFN-40-1EP_5x5mm_P0.4mm_EP3.8x3.8mm Package_DFN_QFN:UQFN-40-1EP_5x5mm_P0.4mm_EP3.8x3.8mm_ThermalVias Package_DFN_QFN:UQFN-48-1EP_6x6mm_P0.4mm_EP4.45x4.45mm @@ -10944,8 +11921,6 @@ Package_DFN_QFN:UQFN-48-1EP_6x6mm_P0.4mm_EP4.45x4.45mm_ThermalVias Package_DFN_QFN:UQFN-48-1EP_6x6mm_P0.4mm_EP4.62x4.62mm Package_DFN_QFN:UQFN-48-1EP_6x6mm_P0.4mm_EP4.62x4.62mm_ThermalVias Package_DFN_QFN:VDFN-8-1EP_2x2mm_P0.5mm_EP0.9x1.7mm -Package_DFN_QFN:Vishay_PowerPAK_MLP44-24L -Package_DFN_QFN:Vishay_PowerPAK_MLP44-24L_ThermalVias Package_DFN_QFN:VQFN-100-1EP_12x12mm_P0.4mm_EP8x8mm Package_DFN_QFN:VQFN-100-1EP_12x12mm_P0.4mm_EP8x8mm_ThermalVias Package_DFN_QFN:VQFN-12-1EP_4x4mm_P0.8mm_EP2.1x2.1mm @@ -10974,6 +11949,8 @@ Package_DFN_QFN:VQFN-28-1EP_4x5mm_P0.5mm_EP2.55x3.55mm Package_DFN_QFN:VQFN-28-1EP_4x5mm_P0.5mm_EP2.55x3.55mm_ThermalVias Package_DFN_QFN:VQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm Package_DFN_QFN:VQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm_ThermalVias +Package_DFN_QFN:VQFN-28-1EP_5x5mm_P0.5mm_EP3.7x3.7mm +Package_DFN_QFN:VQFN-28-1EP_5x5mm_P0.5mm_EP3.7x3.7mm_ThermalVias Package_DFN_QFN:VQFN-32-1EP_4x4mm_P0.4mm_EP2.8x2.8mm Package_DFN_QFN:VQFN-32-1EP_4x4mm_P0.4mm_EP2.8x2.8mm_ThermalVias Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.15x3.15mm @@ -10982,22 +11959,45 @@ Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm_ThermalVias +Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.3x3.3mm +Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.3x3.3mm_ThermalVias Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm_ThermalVias Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm_ThermalVias +Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.7x3.7mm +Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.7x3.7mm_ThermalVias +Package_DFN_QFN:VQFN-40-1EP_6x6mm_P0.5mm_EP3.5x3.5mm +Package_DFN_QFN:VQFN-40-1EP_6x6mm_P0.5mm_EP3.5x3.5mm_ThermalVias Package_DFN_QFN:VQFN-46-1EP_5x6mm_P0.4mm_EP2.8x3.8mm Package_DFN_QFN:VQFN-46-1EP_5x6mm_P0.4mm_EP2.8x3.8mm_ThermalVias Package_DFN_QFN:VQFN-48-1EP_6x6mm_P0.4mm_EP4.1x4.1mm Package_DFN_QFN:VQFN-48-1EP_6x6mm_P0.4mm_EP4.1x4.1mm_ThermalVias +Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP2.6x2.6mm_ThermalVias Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP4.1x4.1mm Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP4.1x4.1mm_ThermalVias +Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP4.2x4.2mm +Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP4.2x4.2mm_ThermalVias Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm_ThermalVias +Package_DFN_QFN:VQFN-52-1EP_6x6mm_P0.4mm_EP4.7x4.7mm +Package_DFN_QFN:VQFN-52-1EP_6x6mm_P0.4mm_EP4.7x4.7mm_ThermalVias +Package_DFN_QFN:VQFN-56-1EP_8x8mm_P0.5mm_EP5.1x4.96mm +Package_DFN_QFN:VQFN-56-1EP_8x8mm_P0.5mm_EP5.1x4.96mm_ThermalVias +Package_DFN_QFN:VQFN-56-1EP_8x8mm_P0.5mm_EP5.5x5.06mm +Package_DFN_QFN:VQFN-56-1EP_8x8mm_P0.5mm_EP5.5x5.06mm_ThermalVias Package_DFN_QFN:VQFN-64-1EP_9x9mm_P0.5mm_EP5.4x5.4mm Package_DFN_QFN:VQFN-64-1EP_9x9mm_P0.5mm_EP5.4x5.4mm_ThermalVias Package_DFN_QFN:VQFN-64-1EP_9x9mm_P0.5mm_EP7.15x7.15mm Package_DFN_QFN:VQFN-64-1EP_9x9mm_P0.5mm_EP7.15x7.15mm_ThermalVias +Package_DFN_QFN:VQFN-68-1EP_8x8mm_P0.4mm_EP4.3x4.3mm +Package_DFN_QFN:Vishay_PowerPAK_MLP44-24L +Package_DFN_QFN:Vishay_PowerPAK_MLP44-24L_ThermalVias +Package_DFN_QFN:Vishay_PowerPAK_MLP55-27L +Package_DFN_QFN:Vishay_PowerPAK_MLP55-27L_R +Package_DFN_QFN:Vishay_PowerPAK_MLP55-27L_R_ThermalVias +Package_DFN_QFN:Vishay_PowerPAK_MLP55-27L_ThermalVias Package_DFN_QFN:W-PDFN-8-1EP_6x5mm_P1.27mm_EP3x3mm Package_DFN_QFN:WCH_QFN-16-1EP_3x3mm_P0.5mm_EP1.8x1.8mm Package_DFN_QFN:WDFN-10-1EP_3x3mm_P0.5mm_EP1.8x2.5mm @@ -11027,11 +12027,23 @@ Package_DFN_QFN:WQFN-16-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias Package_DFN_QFN:WQFN-20-1EP_2.5x4.5mm_P0.5mm_EP1x2.9mm Package_DFN_QFN:WQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm Package_DFN_QFN:WQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm_ThermalVias +Package_DFN_QFN:WQFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:WQFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm_ThermalVias Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:WQFN-28-1EP_3.5x5.5mm_P0.5mm_EP2.05x4.05mm +Package_DFN_QFN:WQFN-28-1EP_3.5x5.5mm_P0.5mm_EP2.05x4.05mm_ThermalVias +Package_DFN_QFN:WQFN-28-1EP_4x4mm_P0.4mm_EP2.7x2.7mm +Package_DFN_QFN:WQFN-28-1EP_4x4mm_P0.4mm_EP2.7x2.7mm_ThermalVias Package_DFN_QFN:WQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_DFN_QFN:WQFN-38-1EP_5x7mm_P0.5mm_EP2.7x4.7mm +Package_DFN_QFN:WQFN-38-1EP_5x7mm_P0.5mm_EP2.7x4.7mm_ThermalVias +Package_DFN_QFN:WQFN-38-1EP_5x7mm_P0.5mm_EP3.15x5.15mm +Package_DFN_QFN:WQFN-38-1EP_5x7mm_P0.5mm_EP3.15x5.15mm_ThermalVias +Package_DFN_QFN:WQFN-38-1EP_5x7mm_P0.5mm_EP3.65x5.65mm +Package_DFN_QFN:WQFN-38-1EP_5x7mm_P0.5mm_EP3.65x5.65mm_ThermalVias Package_DFN_QFN:WQFN-42-1EP_3.5x9mm_P0.5mm_EP2.05x7.55mm Package_DFN_QFN:WQFN-42-1EP_3.5x9mm_P0.5mm_EP2.05x7.55mm_ThermalVias Package_DIP:CERDIP-14_W7.62mm_SideBrazed @@ -11145,6 +12157,12 @@ Package_DIP:DIP-24_W7.62mm_SMDSocket_SmallPads Package_DIP:DIP-24_W7.62mm_Socket Package_DIP:DIP-24_W7.62mm_Socket_LongPads Package_DIP:DIP-24_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-26_W15.24mm +Package_DIP:DIP-26_W15.24mm_LongPads +Package_DIP:DIP-26_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-26_W15.24mm_Socket +Package_DIP:DIP-26_W15.24mm_Socket_LongPads +Package_DIP:DIP-26_W16.51mm_SMDSocket_LongPads Package_DIP:DIP-28_W15.24mm Package_DIP:DIP-28_W15.24mm_LongPads Package_DIP:DIP-28_W15.24mm_SMDSocket_SmallPads @@ -11246,13 +12264,13 @@ Package_DIP:DIP-8_W8.89mm_SMDSocket_LongPads Package_DIP:Fairchild_LSOP-8 Package_DIP:IXYS_Flatpak-8_6.3x9.7mm_P2.54mm Package_DIP:IXYS_SMD-8_6.3x9.7mm_P2.54mm -Package_DIP:PowerIntegrations_eDIP-12B Package_DIP:PowerIntegrations_PDIP-8B Package_DIP:PowerIntegrations_PDIP-8C Package_DIP:PowerIntegrations_SDIP-10C Package_DIP:PowerIntegrations_SMD-8 Package_DIP:PowerIntegrations_SMD-8B Package_DIP:PowerIntegrations_SMD-8C +Package_DIP:PowerIntegrations_eDIP-12B Package_DIP:SMDIP-10_W11.48mm Package_DIP:SMDIP-10_W7.62mm Package_DIP:SMDIP-10_W9.53mm @@ -11339,7 +12357,9 @@ Package_DirectFET:DirectFET_SJ Package_DirectFET:DirectFET_SQ Package_DirectFET:DirectFET_ST Package_LCC:Analog_LCC-8_5x5mm_P1.27mm +Package_LCC:MO047AD_PLCC-52_19.1x19.1mm_P1.27mm Package_LCC:PLCC-20 +Package_LCC:PLCC-20_9.0x9.0mm_P1.27mm Package_LCC:PLCC-20_SMD-Socket Package_LCC:PLCC-20_THT-Socket Package_LCC:PLCC-28 @@ -11366,6 +12386,7 @@ Package_LGA:AMS_LGA-10-1EP_2.7x4mm_P0.6mm Package_LGA:AMS_LGA-20_4.7x4.5mm_P0.65mm Package_LGA:AMS_OLGA-8_2x3.1mm_P0.8mm Package_LGA:Bosch_LGA-14_3x2.5mm_P0.5mm +Package_LGA:Bosch_LGA-16_4.5x3mm_P0.5mm_LayoutBorder7x1y_ClockwisePinNumbering Package_LGA:Bosch_LGA-8_2.5x2.5mm_P0.65mm_ClockwisePinNumbering Package_LGA:Bosch_LGA-8_2x2.5mm_P0.65mm_ClockwisePinNumbering Package_LGA:Bosch_LGA-8_3x3mm_P0.8mm_ClockwisePinNumbering @@ -11385,18 +12406,23 @@ Package_LGA:LGA-8_8x6.2mm_P1.27mm Package_LGA:LGA-8_8x6mm_P1.27mm Package_LGA:Linear_LGA-133_15.0x15.0mm_Layout12x12_P1.27mm Package_LGA:MPS_LGA-18-10EP_12x12mm_P3.3mm -Package_LGA:Nordic_nRF9160-SIxx_LGA-102-59EP_16.0x10.5mm_P0.5mm Package_LGA:NXP_LGA-8_3x5mm_P1.25mm_H1.1mm Package_LGA:NXP_LGA-8_3x5mm_P1.25mm_H1.2mm +Package_LGA:NXP_USON-8_1x1.35mm_P0.35mm +Package_LGA:Nordic_nRF9151-LAxx_LGA-80-33EP_12.1x11.1mm_P0.5mm +Package_LGA:Nordic_nRF9160-SIxx_LGA-102-59EP_16.0x10.5mm_P0.5mm Package_LGA:Rohm_MLGA010V020A_LGA-10_2x2mm_P0.45mm_LayoutBorder2x3y Package_LGA:ST_CCLGA-7L_2.8x2.8mm_P1.15mm_H1.95mm Package_LGA:ST_HLGA-10_2.5x2.5mm_P0.6mm_LayoutBorder3x2y Package_LGA:ST_HLGA-10_2x2mm_P0.5mm_LayoutBorder3x2y +Package_LGA:Texas_SIL0008C_MicroSiP-8-1EP_2.8x3mm_P0.65mm_EP1.1x1.9mm +Package_LGA:Texas_SIL0008C_MicroSiP-8-1EP_2.8x3mm_P0.65mm_EP1.1x1.9mm_ThermalVias Package_LGA:Texas_SIL0008D_MicroSiP-8-1EP_2.8x3mm_P0.65mm_EP1.1x1.9mm Package_LGA:Texas_SIL0008D_MicroSiP-8-1EP_2.8x3mm_P0.65mm_EP1.1x1.9mm_ThermalVias Package_LGA:Texas_SIL0010A_MicroSiP-10-1EP_3.8x3mm_P0.6mm_EP0.7x2.9mm Package_LGA:Texas_SIL0010A_MicroSiP-10-1EP_3.8x3mm_P0.6mm_EP0.7x2.9mm_ThermalVias Package_LGA:VLGA-4_2x2.5mm_P1.65mm +Package_LGA:ublox_LGA-53_4.5x4.5mm_Layout9x9_P0.5mm Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP4x4mm Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP4x4mm_ThermalVias Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP5x5mm @@ -11407,8 +12433,9 @@ Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP7.2x6.35mm Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP7.2x6.35mm_ThermalVias Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP8.93x8.7mm Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP8.93x8.7mm_ThermalVias -Package_QFP:HTQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm -Package_QFP:HTQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm_Mask4.4x4.4mm_ThermalVias +Package_QFP:Hitachi_FP80B_PQFP-80_14x20mm_P0.8mm +Package_QFP:LQFP-100-1EP_14x14mm_P0.5mm_EP6.9x6.9mm +Package_QFP:LQFP-100-1EP_14x14mm_P0.5mm_EP6.9x6.9mm_ThermalVias Package_QFP:LQFP-100_14x14mm_P0.5mm Package_QFP:LQFP-128_14x14mm_P0.4mm Package_QFP:LQFP-128_14x20mm_P0.5mm @@ -11416,6 +12443,8 @@ Package_QFP:LQFP-144-1EP_20x20mm_P0.5mm_EP6.5x6.5mm Package_QFP:LQFP-144-1EP_20x20mm_P0.5mm_EP6.5x6.5mm_ThermalVias Package_QFP:LQFP-144_20x20mm_P0.5mm Package_QFP:LQFP-160_24x24mm_P0.5mm +Package_QFP:LQFP-176-1EP_24x24mm_P0.5mm_EP6.6x6.6mm +Package_QFP:LQFP-176-1EP_24x24mm_P0.5mm_EP6.6x6.6mm_ThermalVias Package_QFP:LQFP-176_20x20mm_P0.4mm Package_QFP:LQFP-176_24x24mm_P0.5mm Package_QFP:LQFP-208_28x28mm_P0.5mm @@ -11441,10 +12470,11 @@ Package_QFP:LQFP-64_7x7mm_P0.4mm Package_QFP:LQFP-80_10x10mm_P0.4mm Package_QFP:LQFP-80_12x12mm_P0.5mm Package_QFP:LQFP-80_14x14mm_P0.65mm +Package_QFP:MO112AC1_PQFP-52_10x10mm_P0.65mm Package_QFP:Microchip_PQFP-44_10x10mm_P0.8mm -Package_QFP:MQFP-44_10x10mm_P0.8mm Package_QFP:PQFP-100_14x20mm_P0.65mm Package_QFP:PQFP-112_20x20mm_P0.65mm +Package_QFP:PQFP-128_28x28mm_P0.8mm Package_QFP:PQFP-132_24x24mm_P0.635mm Package_QFP:PQFP-132_24x24mm_P0.635mm_i386 Package_QFP:PQFP-144_28x28mm_P0.65mm @@ -11453,12 +12483,10 @@ Package_QFP:PQFP-168_28x28mm_P0.65mm Package_QFP:PQFP-208_28x28mm_P0.5mm Package_QFP:PQFP-240_32.1x32.1mm_P0.5mm Package_QFP:PQFP-256_28x28mm_P0.4mm -Package_QFP:PQFP-32_5x5mm_P0.5mm Package_QFP:PQFP-44_10x10mm_P0.8mm Package_QFP:PQFP-64_14x14mm_P0.8mm +Package_QFP:PQFP-80_14x14mm_P0.65mm Package_QFP:PQFP-80_14x20mm_P0.8mm -Package_QFP:Texas_PHP0048E_HTQFP-48-1EP_7x7mm_P0.5mm_EP6.5x6.5mm_Mask3.62x3.62mm -Package_QFP:Texas_PHP0048E_HTQFP-48-1EP_7x7mm_P0.5mm_EP6.5x6.5mm_Mask3.62x3.62mm_ThermalVias Package_QFP:TQFP-100-1EP_14x14mm_P0.5mm_EP5x5mm Package_QFP:TQFP-100-1EP_14x14mm_P0.5mm_EP5x5mm_ThermalVias Package_QFP:TQFP-100_12x12mm_P0.4mm @@ -11467,11 +12495,15 @@ Package_QFP:TQFP-120_14x14mm_P0.4mm Package_QFP:TQFP-128_14x14mm_P0.4mm Package_QFP:TQFP-144_16x16mm_P0.4mm Package_QFP:TQFP-144_20x20mm_P0.5mm +Package_QFP:TQFP-176_20x20mm_P0.4mm Package_QFP:TQFP-176_24x24mm_P0.5mm +Package_QFP:TQFP-32_5x5mm_P0.5mm Package_QFP:TQFP-32_7x7mm_P0.8mm Package_QFP:TQFP-44-1EP_10x10mm_P0.8mm_EP4.5x4.5mm +Package_QFP:TQFP-44-1EP_10x10mm_P0.8mm_EP4.5x4.5mm_ThermalVias Package_QFP:TQFP-44_10x10mm_P0.8mm Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm +Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm_ThermalVias Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP4.11x4.11mm Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP5x5mm Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP5x5mm_ThermalVias @@ -11480,21 +12512,21 @@ Package_QFP:TQFP-52-1EP_10x10mm_P0.65mm_EP6.5x6.5mm Package_QFP:TQFP-52-1EP_10x10mm_P0.65mm_EP6.5x6.5mm_ThermalVias Package_QFP:TQFP-64-1EP_10x10mm_P0.5mm_EP5.305x5.305mm Package_QFP:TQFP-64-1EP_10x10mm_P0.5mm_EP5.305x5.305mm_ThermalVias -Package_QFP:TQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm Package_QFP:TQFP-64_10x10mm_P0.5mm Package_QFP:TQFP-64_14x14mm_P0.8mm Package_QFP:TQFP-64_7x7mm_P0.4mm Package_QFP:TQFP-80-1EP_14x14mm_P0.65mm_EP9.5x9.5mm +Package_QFP:TQFP-80-1EP_14x14mm_P0.65mm_EP9.5x9.5mm_ThermalVias Package_QFP:TQFP-80_12x12mm_P0.5mm Package_QFP:TQFP-80_14x14mm_P0.65mm -Package_QFP:VQFP-100_14x14mm_P0.5mm -Package_QFP:VQFP-128_14x14mm_P0.4mm -Package_QFP:VQFP-176_20x20mm_P0.4mm -Package_QFP:VQFP-80_14x14mm_P0.65mm +Package_QFP:Texas_PHP0048E_HTQFP-48-1EP_7x7mm_P0.5mm_EP6.5x6.5mm_Mask3.62x3.62mm +Package_QFP:Texas_PHP0048E_HTQFP-48-1EP_7x7mm_P0.5mm_EP6.5x6.5mm_Mask3.62x3.62mm_ThermalVias +Package_QFP:Texas_TQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm_Mask4.44x4.44mm +Package_QFP:Texas_TQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm_Mask4.44x4.44mm_ThermalVias +Package_QFP:Texas_TQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm_Mask5x5mm +Package_QFP:Texas_TQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm_Mask5x5mm_ThermalVias Package_SIP:PowerIntegrations_eSIP-7C Package_SIP:PowerIntegrations_eSIP-7F -Package_SIP:Sanyo_STK4xx-15_59.2x8.0mm_P2.54mm -Package_SIP:Sanyo_STK4xx-15_78.0x8.0mm_P2.54mm Package_SIP:SIP-8_19x3mm_P2.54mm Package_SIP:SIP-9_21.54x3mm_P2.54mm Package_SIP:SIP-9_22.3x3mm_P2.54mm @@ -11507,6 +12539,8 @@ Package_SIP:SIP9_Housing_BigPads Package_SIP:SLA704XM Package_SIP:STK672-040-E Package_SIP:STK672-080-E +Package_SIP:Sanyo_STK4xx-15_59.2x8.0mm_P2.54mm +Package_SIP:Sanyo_STK4xx-15_78.0x8.0mm_P2.54mm Package_SO:Analog_MSOP-12-16-1EP_3x4.039mm_P0.5mm_EP1.651x2.845mm Package_SO:Analog_MSOP-12-16-1EP_3x4.039mm_P0.5mm_EP1.651x2.845mm_ThermalVias Package_SO:Analog_MSOP-12-16_3x4.039mm_P0.5mm @@ -11527,11 +12561,11 @@ Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm_ThermalVias Package_SO:HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.4x3.2mm Package_SO:HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.4x3.2mm_ThermalVias -Package_SO:HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3x3.1mm -Package_SO:HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3x3.1mm_ThermalVias Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm_ThermalVias +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.66x2.46mm +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.66x2.46mm_ThermalVias Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3x3mm_ThermalVias Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3x3mm Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP2.74x3.86mm @@ -11540,11 +12574,12 @@ Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.4x3.7mm Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.75x3.43mm Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.75x3.43mm_ThermalVias -Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.75x3.43mm_ThermalVias_HandSolder Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.96x2.96mm Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.96x2.96mm_ThermalVias Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_ThermalVias Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.2x5mm +Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.44x3.42mm +Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.44x3.42mm_ThermalVias Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.4x2.98mm Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.4x2.98mm_ThermalVias Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.4x4.68mm @@ -11563,14 +12598,16 @@ Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP1.5x3.3mm Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP1.5x3.3mm_ThermalVias Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP2.74x4.75mm Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP2.74x4.75mm_ThermalVias +Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP3.05x6.65mm +Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP3.05x6.65mm_ThermalVias Package_SO:HTSSOP-38-1EP_6.1x12.5mm_P0.65mm_EP5.2x12.5mm_Mask3.39x6.35mm Package_SO:HTSSOP-38-1EP_6.1x12.5mm_P0.65mm_EP5.2x12.5mm_Mask3.39x6.35mm_ThermalVias Package_SO:HTSSOP-44-1EP_6.1x14mm_P0.635mm_EP5.2x14mm_Mask4.31x8.26mm Package_SO:HTSSOP-44-1EP_6.1x14mm_P0.635mm_EP5.2x14mm_Mask4.31x8.26mm_ThermalVias Package_SO:HTSSOP-44_6.1x14mm_P0.635mm_TopEP4.14x7.01mm Package_SO:HTSSOP-56-1EP_6.1x14mm_P0.5mm_EP3.61x6.35mm -Package_SO:HVSSOP-10-1EP_3x3mm_P0.5mm_EP1.57x1.88mm -Package_SO:HVSSOP-10-1EP_3x3mm_P0.5mm_EP1.57x1.88mm_ThermalVias +Package_SO:HVSSOP-10-1EP_3x3mm_P0.5mm_EP1.83x1.89mm +Package_SO:HVSSOP-10-1EP_3x3mm_P0.5mm_EP1.83x1.89mm_ThermalVias Package_SO:HVSSOP-8-1EP_3x3mm_P0.65mm_EP1.57x1.89mm Package_SO:HVSSOP-8-1EP_3x3mm_P0.65mm_EP1.57x1.89mm_ThermalVias Package_SO:Infineon_PG-DSO-12-11 @@ -11591,10 +12628,10 @@ Package_SO:Infineon_PG-DSO-8-43 Package_SO:Infineon_PG-DSO-8-59_7.5x6.3mm Package_SO:Infineon_PG-TSDSO-14-22 Package_SO:Infineon_SOIC-20W_7.6x12.8mm_P1.27mm +Package_SO:JEITA_SOIC-16_3.9x9.9mm_P1.27mm +Package_SO:JEITA_SOIC-8_3.9x4.9mm_P1.27mm Package_SO:Linear_HTSSOP-31-38-1EP_4.4x9.7mm_P0.5mm_EP2.74x4.75mm Package_SO:Linear_HTSSOP-31-38-1EP_4.4x9.7mm_P0.5mm_EP2.74x4.75mm_ThermalVias -Package_SO:MFSOP6-4_4.4x3.6mm_P1.27mm -Package_SO:MFSOP6-5_4.4x3.6mm_P1.27mm Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP1.68x1.88mm Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP1.68x1.88mm_ThermalVias Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP1.73x1.98mm @@ -11621,18 +12658,19 @@ Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP2.5x3mm_Mask1.73x2.36mm_ThermalVias Package_SO:MSOP-8_3x3mm_P0.65mm Package_SO:NXP_HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.2x3.4mm Package_SO:NXP_HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.2x3.4mm_ThermalVias -Package_SO:OnSemi_Micro8 Package_SO:ONSemi_SO-8FL_488AA -Package_SO:PowerIntegrations_eSOP-12B +Package_SO:OnSemi_751EP_SOIC-4_3.9x4.725mm_P2.54mm +Package_SO:OnSemi_Micro8 +Package_SO:PSOP-44_16.9x27.17mm_P1.27mm Package_SO:PowerIntegrations_SO-8 Package_SO:PowerIntegrations_SO-8B Package_SO:PowerIntegrations_SO-8C +Package_SO:PowerIntegrations_eSOP-12B Package_SO:PowerPAK_SO-8L_Single Package_SO:PowerPAK_SO-8_Dual Package_SO:PowerPAK_SO-8_Single Package_SO:PowerSSO-16-1EP_3.9x4.9mm_P0.5mm_EP2.5x3.61mm Package_SO:PowerSSO-16-1EP_3.9x4.9mm_P0.5mm_EP2.5x3.61mm_ThermalVias -Package_SO:PSOP-44_16.9x27.17mm_P1.27mm Package_SO:QSOP-16_3.9x4.9mm_P0.635mm Package_SO:QSOP-20_3.9x8.7mm_P0.635mm Package_SO:QSOP-24_3.9x8.7mm_P0.635mm @@ -11653,7 +12691,6 @@ Package_SO:SO-4_4.4x3.9mm_P2.54mm Package_SO:SO-4_4.4x4.3mm_P2.54mm Package_SO:SO-4_7.6x3.6mm_P2.54mm Package_SO:SO-5-6_4.55x3.7mm_P1.27mm -Package_SO:SO-5_4.4x3.6mm_P1.27mm Package_SO:SO-6L_10x3.84mm_P1.27mm Package_SO:SO-6_4.4x3.6mm_P1.27mm Package_SO:SO-8_3.9x4.9mm_P1.27mm @@ -11676,6 +12713,7 @@ Package_SO:SOIC-28W_7.5x18.7mm_P1.27mm Package_SO:SOIC-32_7.518x20.777mm_P1.27mm Package_SO:SOIC-4_4.55x2.6mm_P1.27mm Package_SO:SOIC-4_4.55x3.7mm_P2.54mm +Package_SO:SOIC-5-6_4.4x3.6mm_P1.27mm Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.29x3mm Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.29x3mm_ThermalVias Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.3mm @@ -11686,9 +12724,14 @@ Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.514x3.2mm Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.514x3.2mm_ThermalVias Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.62x3.51mm Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.62x3.51mm_ThermalVias +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.71x3.7mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.71x3.7mm_ThermalVias +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.34x2.34mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.34x2.34mm_ThermalVias Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.71x3.4mm Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.71x3.4mm_ThermalVias Package_SO:SOIC-8-N7_3.9x4.9mm_P1.27mm +Package_SO:SOIC-8_3.81x9.347mm_P2.54mm Package_SO:SOIC-8_3.9x4.9mm_P1.27mm Package_SO:SOIC-8_5.3x5.3mm_P1.27mm Package_SO:SOIC-8_5.3x6.2mm_P1.27mm @@ -11700,7 +12743,6 @@ Package_SO:SOJ-32_10.16x20.955mm_P1.27mm Package_SO:SOJ-32_7.62x20.955mm_P1.27mm Package_SO:SOJ-36_10.16x23.495mm_P1.27mm Package_SO:SOJ-44_10.16x28.575mm_P1.27mm -Package_SO:SOP-16_3.9x9.9mm_P1.27mm Package_SO:SOP-16_4.4x10.4mm_P1.27mm Package_SO:SOP-16_4.55x10.3mm_P1.27mm Package_SO:SOP-18_7.495x11.515mm_P1.27mm @@ -11717,7 +12759,6 @@ Package_SO:SOP-4_7.5x4.1mm_P2.54mm Package_SO:SOP-8-1EP_4.57x4.57mm_P1.27mm_EP4.57x4.45mm Package_SO:SOP-8-1EP_4.57x4.57mm_P1.27mm_EP4.57x4.45mm_ThermalVias Package_SO:SOP-8_3.76x4.96mm_P1.27mm -Package_SO:SOP-8_3.9x4.9mm_P1.27mm Package_SO:SOP-8_6.605x9.655mm_P2.54mm Package_SO:SOP-8_6.62x9.15mm_P2.54mm Package_SO:SSO-4_6.7x5.1mm_P2.54mm_Clearance8mm @@ -11745,6 +12786,7 @@ Package_SO:SSOP-24_3.9x8.7mm_P0.635mm Package_SO:SSOP-24_5.3x8.2mm_P0.65mm Package_SO:SSOP-28_3.9x9.9mm_P0.635mm Package_SO:SSOP-28_5.3x10.2mm_P0.65mm +Package_SO:SSOP-40_8.8x17.5mm_P0.8mm Package_SO:SSOP-44_5.3x12.8mm_P0.5mm Package_SO:SSOP-48_5.3x12.8mm_P0.5mm Package_SO:SSOP-48_7.5x15.9mm_P0.635mm @@ -11753,7 +12795,7 @@ Package_SO:SSOP-56_7.5x18.5mm_P0.635mm Package_SO:SSOP-8_2.95x2.8mm_P0.65mm Package_SO:SSOP-8_3.95x5.21x3.27mm_P1.27mm Package_SO:SSOP-8_3.9x5.05mm_P1.27mm -Package_SO:SSOP-8_5.25x5.24mm_P1.27mm +Package_SO:SSOP-8_5.3x3mm_P0.65mm Package_SO:STC_SOP-16_3.9x9.9mm_P1.27mm Package_SO:ST_MultiPowerSO-30 Package_SO:ST_PowerSSO-24_SlugDown @@ -11762,25 +12804,6 @@ Package_SO:ST_PowerSSO-24_SlugUp Package_SO:ST_PowerSSO-36_SlugDown Package_SO:ST_PowerSSO-36_SlugDown_ThermalVias Package_SO:ST_PowerSSO-36_SlugUp -Package_SO:Texas_DAD0032A_HTSSOP-32_6.1x11mm_P0.65mm_TopEP3.71x3.81mm -Package_SO:Texas_DGN0008B_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x3mm_Mask1.88x1.98mm -Package_SO:Texas_DGN0008B_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x3mm_Mask1.88x1.98mm_ThermalVias -Package_SO:Texas_DGN0008D_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.57x1.89mm -Package_SO:Texas_DGN0008D_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.57x1.89mm_ThermalVias -Package_SO:Texas_DGN0008G_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.846x2.15mm -Package_SO:Texas_DGN0008G_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.846x2.15mm_ThermalVias -Package_SO:Texas_DKD0036A_HSSOP-36_11x15.9mm_P0.65mm_TopEP5.85x12.65mm -Package_SO:Texas_DYY0016A_TSOT-23-16_4.2x2.0mm_P0.5mm -Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm -Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm_ThermalVias -Package_SO:Texas_HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.4x3.1mm_ThermalVias -Package_SO:Texas_PW0020A_TSSOP-20_4.4x6.5mm_P0.65mm -Package_SO:Texas_PWP0020A -Package_SO:Texas_PWP0028V_TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.7mm_Mask2.94x5.62mm -Package_SO:Texas_PWP0028V_TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.7mm_Mask2.94x5.62mm_ThermalVias -Package_SO:Texas_R-PDSO-G8_EP2.95x4.9mm_Mask2.4x3.1mm -Package_SO:Texas_R-PDSO-G8_EP2.95x4.9mm_Mask2.4x3.1mm_ThermalVias -Package_SO:Texas_S-PDSO-G8_3x3mm_P0.65mm Package_SO:TI_SO-PowerPAD-8 Package_SO:TI_SO-PowerPAD-8_ThermalVias Package_SO:TSOP-5_1.65x3.05mm_P0.95mm @@ -11825,7 +12848,6 @@ Package_SO:TSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP2.15x3.35mm Package_SO:TSSOP-20_4.4x5mm_P0.4mm Package_SO:TSSOP-20_4.4x5mm_P0.5mm Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm -Package_SO:TSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.2x5mm Package_SO:TSSOP-24_4.4x5mm_P0.4mm Package_SO:TSSOP-24_4.4x6.5mm_P0.5mm Package_SO:TSSOP-24_4.4x7.8mm_P0.65mm @@ -11885,13 +12907,36 @@ Package_SO:TSSOP-68_8x14mm_P0.4mm Package_SO:TSSOP-80_6.1x17mm_P0.4mm Package_SO:TSSOP-8_3x3mm_P0.65mm Package_SO:TSSOP-8_4.4x3mm_P0.65mm -Package_SO:Vishay_PowerPAK_1212-8_Dual -Package_SO:Vishay_PowerPAK_1212-8_Single +Package_SO:Texas_DAD0032A_HTSSOP-32_6.1x11mm_P0.65mm_TopEP3.71x3.81mm +Package_SO:Texas_DGN0008B_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x3mm_Mask1.88x1.98mm +Package_SO:Texas_DGN0008B_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x3mm_Mask1.88x1.98mm_ThermalVias +Package_SO:Texas_DGN0008D_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.57x1.89mm +Package_SO:Texas_DGN0008D_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.57x1.89mm_ThermalVias +Package_SO:Texas_DGN0008G_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.846x2.15mm +Package_SO:Texas_DGN0008G_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.846x2.15mm_ThermalVias +Package_SO:Texas_DGS0020A_TSSOP-20_3x5.1mm_P0.5mm +Package_SO:Texas_DKD0036A_HSSOP-36_11x15.9mm_P0.65mm_TopEP5.85x12.65mm +Package_SO:Texas_DYY0016A_TSOT-23-16_2x4.2mm_P0.5mm +Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm +Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm_ThermalVias +Package_SO:Texas_HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.4x3.1mm_ThermalVias +Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm +Package_SO:Texas_HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3.155x3.255mm_ThermalVias +Package_SO:Texas_PW0020A_TSSOP-20_4.4x6.5mm_P0.65mm +Package_SO:Texas_PWP0020A +Package_SO:Texas_PWP0028V_TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.7mm_Mask2.94x5.62mm +Package_SO:Texas_PWP0028V_TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.7mm_Mask2.94x5.62mm_ThermalVias +Package_SO:Texas_R-PDSO-G8_EP2.95x4.9mm_Mask2.4x3.1mm +Package_SO:Texas_R-PDSO-G8_EP2.95x4.9mm_Mask2.4x3.1mm_ThermalVias +Package_SO:Texas_S-PDSO-G8_3x3mm_P0.65mm +Package_SO:Toshiba_SOIC-4-6_4.4x3.6mm_P1.27mm +Package_SO:Toshiba_SOIC-5-6_4.4x3.6mm_P1.27mm Package_SO:VSO-40_7.6x15.4mm_P0.762mm Package_SO:VSO-56_11.1x21.5mm_P0.75mm -Package_SO:VSSOP-10_3x3mm_P0.5mm Package_SO:VSSOP-8_2.3x2mm_P0.5mm Package_SO:VSSOP-8_3x3mm_P0.65mm +Package_SO:Vishay_PowerPAK_1212-8_Dual +Package_SO:Vishay_PowerPAK_1212-8_Single Package_SO:Zetex_SM8 Package_SON:Diodes_PowerDI3333-8 Package_SON:Diodes_PowerDI3333-8_UXC_3.3x3.3mm_P0.65mm @@ -11908,11 +12953,16 @@ Package_SON:Infineon_PG-TISON-8-2 Package_SON:Infineon_PG-TISON-8-3 Package_SON:Infineon_PG-TISON-8-4 Package_SON:Infineon_PG-TISON-8-5 +Package_SON:MPS_USON-6_1.2x1.6mm_P0.5mm +Package_SON:MPS_VSON-6_1x1.5mm_P0.5mm Package_SON:MicroCrystal_C7_SON-8_1.5x3.2mm_P0.9mm +Package_SON:Microchip_USON-10-1EP_3x3mm_P0.5mm_EP1.8x2.5mm +Package_SON:Microchip_USON-10-1EP_3x3mm_P0.5mm_EP1.8x2.5mm_ThermalVias +Package_SON:NXP_LSON-16-1EP_3.5x4.5mm_P0.5mm_EP2x3.8mm +Package_SON:NXP_XSON-16 Package_SON:Nexperia_HUSON-12_USON-12-1EP_1.35x2.5mm_P0.4mm_EP0.4x2mm Package_SON:Nexperia_HUSON-16_USON-16-1EP_1.35x3.3mm_P0.4mm_EP0.4x2.8mm Package_SON:Nexperia_HUSON-8_USON-8-1EP_1.35x1.7mm_P0.4mm_EP0.4x1.2mm -Package_SON:NXP_XSON-16 Package_SON:ROHM_VML0806 Package_SON:RTC_SMD_MicroCrystal_C3_2.5x3.7mm Package_SON:SON-8-1EP_3x2mm_P0.5mm_EP1.4x1.6mm @@ -11927,6 +12977,8 @@ Package_SON:Texas_DRC0010J_ThermalVias Package_SON:Texas_DRX_WSON-10_2.5x2.5mm_P0.5mm Package_SON:Texas_DSC0010J Package_SON:Texas_DSC0010J_ThermalVias +Package_SON:Texas_DSG0008A_WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm +Package_SON:Texas_DSG0008A_WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm_ThermalVias Package_SON:Texas_PWSON-N6 Package_SON:Texas_R-PUSON-N14 Package_SON:Texas_R-PUSON-N8_USON-8-1EP_1.6x2.1mm_P0.5mm_EP0.4x1.7mm @@ -11942,6 +12994,8 @@ Package_SON:Texas_S-PWSON-N8_EP1.2x2mm Package_SON:Texas_S-PWSON-N8_EP1.2x2mm_ThermalVias Package_SON:Texas_USON-6_1x1.45mm_P0.5mm_SMD Package_SON:Texas_VSON-HR-8_1.5x2mm_P0.5mm +Package_SON:Texas_X2SON-4-1EP_1.1x1.4mm_P0.5mm_EP0.8x0.6mm +Package_SON:Texas_X2SON-4-1EP_1.1x1.4mm_P0.5mm_EP0.8x0.6mm_ThermalVias Package_SON:Texas_X2SON-4_1x1mm_P0.65mm Package_SON:Texas_X2SON-5_0.8x0.8mm_P0.48mm Package_SON:Texas_X2SON-5_0.8x0.8mm_P0.48mm_RoutingVia @@ -11959,8 +13013,6 @@ Package_SON:VSON-8-1EP_3x3mm_P0.65mm_EP1.6x2.4mm Package_SON:VSON-8_1.5x2mm_P0.5mm Package_SON:VSON-8_3.3x3.3mm_P0.65mm_NexFET Package_SON:VSONP-8-1EP_5x6_P1.27mm -Package_SON:Winbond_USON-8-1EP_3x2mm_P0.5mm_EP0.2x1.6mm -Package_SON:Winbond_USON-8-2EP_3x4mm_P0.8mm_EP0.2x0.8mm Package_SON:WSON-10-1EP_2.5x2.5mm_P0.5mm_EP1.2x2mm Package_SON:WSON-10-1EP_2.5x2.5mm_P0.5mm_EP1.2x2mm_ThermalVias Package_SON:WSON-10-1EP_2x3mm_P0.5mm_EP0.84x2.4mm @@ -11998,10 +13050,12 @@ Package_SON:WSON-8-1EP_4x4mm_P0.8mm_EP2.6x3mm_ThermalVias Package_SON:WSON-8-1EP_6x5mm_P1.27mm_EP3.4x4.3mm Package_SON:WSON-8-1EP_6x5mm_P1.27mm_EP3.4x4mm Package_SON:WSON-8-1EP_8x6mm_P1.27mm_EP3.4x4.3mm +Package_SON:Winbond_USON-8-1EP_3x2mm_P0.5mm_EP0.2x1.6mm +Package_SON:Winbond_USON-8-2EP_3x4mm_P0.8mm_EP0.2x0.8mm Package_SON:X2SON-8_1.4x1mm_P0.35mm Package_SO_J-Lead:TSOC-6_3.76x3.94mm_P1.27mm -Package_TO_SOT_SMD:Analog_KS-4 Package_TO_SOT_SMD:ATPAK-2 +Package_TO_SOT_SMD:Analog_KS-4 Package_TO_SOT_SMD:Diodes_SOT-553 Package_TO_SOT_SMD:HVSOF5 Package_TO_SOT_SMD:HVSOF6 @@ -12020,15 +13074,14 @@ Package_TO_SOT_SMD:LFPAK56 Package_TO_SOT_SMD:LFPAK88 Package_TO_SOT_SMD:Nexperia_CFP15_SOT-1289 Package_TO_SOT_SMD:OnSemi_ECH8 +Package_TO_SOT_SMD:PQFN_8x8 Package_TO_SOT_SMD:PowerMacro_M234_NoHole Package_TO_SOT_SMD:PowerMacro_M234_WithHole -Package_TO_SOT_SMD:PQFN_8x8 -Package_TO_SOT_SMD:Rohm_HRP7 Package_TO_SOT_SMD:ROHM_SOT-457_ClockwisePinNumbering +Package_TO_SOT_SMD:Rohm_HRP7 Package_TO_SOT_SMD:SC-59 Package_TO_SOT_SMD:SC-59_Handsoldering Package_TO_SOT_SMD:SC-70-8 -Package_TO_SOT_SMD:SC-70-8_Handsoldering Package_TO_SOT_SMD:SC-74-6_1.55x2.9mm_P0.95mm Package_TO_SOT_SMD:SC-74A-5_1.55x2.9mm_P0.95mm Package_TO_SOT_SMD:SC-82AA @@ -12042,12 +13095,13 @@ Package_TO_SOT_SMD:SOT-143 Package_TO_SOT_SMD:SOT-143R Package_TO_SOT_SMD:SOT-143R_Handsoldering Package_TO_SOT_SMD:SOT-143_Handsoldering +Package_TO_SOT_SMD:SOT-223 Package_TO_SOT_SMD:SOT-223-3_TabPin2 Package_TO_SOT_SMD:SOT-223-5 Package_TO_SOT_SMD:SOT-223-6 Package_TO_SOT_SMD:SOT-223-6_TabPin3 Package_TO_SOT_SMD:SOT-223-8 -Package_TO_SOT_SMD:SOT-223 +Package_TO_SOT_SMD:SOT-23 Package_TO_SOT_SMD:SOT-23-3 Package_TO_SOT_SMD:SOT-23-5 Package_TO_SOT_SMD:SOT-23-5_HandSoldering @@ -12055,21 +13109,17 @@ Package_TO_SOT_SMD:SOT-23-6 Package_TO_SOT_SMD:SOT-23-6_Handsoldering Package_TO_SOT_SMD:SOT-23-8 Package_TO_SOT_SMD:SOT-23-8_Handsoldering -Package_TO_SOT_SMD:SOT-23 Package_TO_SOT_SMD:SOT-23W Package_TO_SOT_SMD:SOT-23W_Handsoldering Package_TO_SOT_SMD:SOT-23_Handsoldering Package_TO_SOT_SMD:SOT-323_SC-70 -Package_TO_SOT_SMD:SOT-323_SC-70_Handsoldering Package_TO_SOT_SMD:SOT-343_SC-70-4 -Package_TO_SOT_SMD:SOT-343_SC-70-4_Handsoldering Package_TO_SOT_SMD:SOT-353_SC-70-5 -Package_TO_SOT_SMD:SOT-353_SC-70-5_Handsoldering Package_TO_SOT_SMD:SOT-363_SC-70-6 -Package_TO_SOT_SMD:SOT-363_SC-70-6_Handsoldering Package_TO_SOT_SMD:SOT-383F Package_TO_SOT_SMD:SOT-383FL Package_TO_SOT_SMD:SOT-416 +Package_TO_SOT_SMD:SOT-457T Package_TO_SOT_SMD:SOT-523 Package_TO_SOT_SMD:SOT-543 Package_TO_SOT_SMD:SOT-553 @@ -12089,15 +13139,6 @@ Package_TO_SOT_SMD:SuperSOT-3 Package_TO_SOT_SMD:SuperSOT-6 Package_TO_SOT_SMD:SuperSOT-8 Package_TO_SOT_SMD:TDSON-8-1 -Package_TO_SOT_SMD:Texas_DRT-3 -Package_TO_SOT_SMD:Texas_NDQ -Package_TO_SOT_SMD:Texas_NDW-7_TabPin4 -Package_TO_SOT_SMD:Texas_NDW-7_TabPin8 -Package_TO_SOT_SMD:Texas_NDY0011A -Package_TO_SOT_SMD:Texas_R-PDSO-G5_DCK-5 -Package_TO_SOT_SMD:Texas_R-PDSO-G6 -Package_TO_SOT_SMD:Texas_R-PDSO-N5_DRL-5 -Package_TO_SOT_SMD:Texas_R-PDSO-N6_DRL-6 Package_TO_SOT_SMD:TO-252-2 Package_TO_SOT_SMD:TO-252-2_TabPin1 Package_TO_SOT_SMD:TO-252-3_TabPin2 @@ -12129,17 +13170,27 @@ Package_TO_SOT_SMD:TO-50-4_LongPad-NoHole_Housing Package_TO_SOT_SMD:TO-50-4_LongPad-WithHole_Housing Package_TO_SOT_SMD:TO-50-4_ShortPad-NoHole_Housing Package_TO_SOT_SMD:TO-50-4_ShortPad-WithHole_Housing +Package_TO_SOT_SMD:TSOT-23 Package_TO_SOT_SMD:TSOT-23-5 Package_TO_SOT_SMD:TSOT-23-5_HandSoldering Package_TO_SOT_SMD:TSOT-23-6 Package_TO_SOT_SMD:TSOT-23-6_HandSoldering Package_TO_SOT_SMD:TSOT-23-8 Package_TO_SOT_SMD:TSOT-23-8_HandSoldering -Package_TO_SOT_SMD:TSOT-23 Package_TO_SOT_SMD:TSOT-23_HandSoldering +Package_TO_SOT_SMD:Texas_DDF0008A_SOT-8_1.6x2.9mm_P0.65mm +Package_TO_SOT_SMD:Texas_DRT-3 +Package_TO_SOT_SMD:Texas_NDQ +Package_TO_SOT_SMD:Texas_NDW-7_TabPin4 +Package_TO_SOT_SMD:Texas_NDW-7_TabPin8 +Package_TO_SOT_SMD:Texas_NDY0011A +Package_TO_SOT_SMD:Texas_R-PDSO-G5_DCK-5 +Package_TO_SOT_SMD:Texas_R-PDSO-G6 +Package_TO_SOT_SMD:Texas_R-PDSO-N5_DRL-5 +Package_TO_SOT_SMD:Texas_R-PDSO-N6_DRL-6 +Package_TO_SOT_SMD:VSOF5 Package_TO_SOT_SMD:Vishay_PowerPAK_SC70-6L_Dual Package_TO_SOT_SMD:Vishay_PowerPAK_SC70-6L_Single -Package_TO_SOT_SMD:VSOF5 Package_TO_SOT_THT:Analog_TO-46-4_ThermalShield Package_TO_SOT_THT:Fairchild_TO-220F-6L Package_TO_SOT_THT:Heraeus_TO-92-2 @@ -12217,6 +13268,7 @@ Package_TO_SOT_THT:TO-220-7_P2.54x3.8mm_StaggerOdd_Lead5.85mm_TabDown Package_TO_SOT_THT:TO-220-7_P2.54x5.08mm_StaggerOdd_Lead3.08mm_Vertical Package_TO_SOT_THT:TO-220-7_P2.54x5.1mm_StaggerOdd_Lead8.025mm_TabDown Package_TO_SOT_THT:TO-220-8_Vertical +Package_TO_SOT_THT:TO-220-9_P1.93x5.08mm_StaggerOdd_Lead3.378mm_Vertical Package_TO_SOT_THT:TO-220-9_P1.94x3.7mm_StaggerEven_Lead3.8mm_Vertical Package_TO_SOT_THT:TO-220-9_P1.94x3.7mm_StaggerOdd_Lead3.8mm_Vertical Package_TO_SOT_THT:TO-220-9_P1.94x3.8mm_StaggerEven_Lead5.85mm_TabDown @@ -12345,13 +13397,13 @@ Package_TO_SOT_THT:TO-8-2 Package_TO_SOT_THT:TO-8-2_Window Package_TO_SOT_THT:TO-8-3 Package_TO_SOT_THT:TO-8-3_Window +Package_TO_SOT_THT:TO-92 Package_TO_SOT_THT:TO-92-2 Package_TO_SOT_THT:TO-92-2_Horizontal1 Package_TO_SOT_THT:TO-92-2_Horizontal2 Package_TO_SOT_THT:TO-92-2_W4.0mm_Horizontal_FlatSideDown Package_TO_SOT_THT:TO-92-2_W4.0mm_Horizontal_FlatSideUp Package_TO_SOT_THT:TO-92-2_Wide -Package_TO_SOT_THT:TO-92 Package_TO_SOT_THT:TO-92Flat Package_TO_SOT_THT:TO-92L Package_TO_SOT_THT:TO-92L_HandSolder @@ -12359,9 +13411,7 @@ Package_TO_SOT_THT:TO-92L_Inline Package_TO_SOT_THT:TO-92L_Inline_Wide Package_TO_SOT_THT:TO-92L_Wide Package_TO_SOT_THT:TO-92Mini-2 -Package_TO_SOT_THT:TO-92S-2 Package_TO_SOT_THT:TO-92S -Package_TO_SOT_THT:TO-92S_Wide Package_TO_SOT_THT:TO-92_HandSolder Package_TO_SOT_THT:TO-92_Horizontal1 Package_TO_SOT_THT:TO-92_Horizontal2 @@ -12378,6 +13428,37 @@ Package_TO_SOT_THT:TO-99-6 Package_TO_SOT_THT:TO-99-6_Window Package_TO_SOT_THT:TO-99-8 Package_TO_SOT_THT:TO-99-8_Window +Panelization:BreakLine_11h_D0.5mm_P0.85mm +Panelization:BreakLine_1h_D0.5mm_P0.85mm +Panelization:BreakLine_2h_D0.5mm_P0.85mm +Panelization:BreakLine_2h_D0.5mm_P1.7mm +Panelization:BreakLine_3h_D0.5mm_P0.85mm +Panelization:BreakLine_3h_D0.5mm_P1.7mm +Panelization:BreakLine_5h_D0.5mm_P0.85mm +Panelization:BreakLine_7h_D0.5mm_P0.85mm +Panelization:BreakLine_9h_D0.5mm_P0.85mm +Panelization:MouseBite-Part_2.0x2.0mm_0h +Panelization:MouseBite-Part_2.0x2.0mm_inset_D0.5mm_P0.85mm_4h_Trace +Panelization:MouseBite-Part_2.0x2.0mm_inset_D0.5mm_P0.85mm_5h +Panelization:MouseBite-Part_2.0x2.0mm_outset_D0.5mm_P0.85mm_2h_Trace +Panelization:MouseBite-Part_2.0x2.0mm_outset_D0.5mm_P0.85mm_3h +Panelization:MouseBite-Part_2.0x4.5mm_0h +Panelization:MouseBite-Part_2.0x4.5mm_inset_D0.5mm_P0.85mm_5h_Trace +Panelization:MouseBite-Part_2.0x4.5mm_inset_D0.5mm_P0.85mm_7h +Panelization:MouseBite-Part_2.0x4.5mm_outset_D0.5mm_P0.85mm_5h_Trace +Panelization:MouseBite-Part_2.0x4.5mm_outset_D0.5mm_P0.85mm_7h +Panelization:MouseBite-Slot-Jumper_01005 +Panelization:MouseBite-Slot-Jumper_0201 +Panelization:MouseBite-Slot_2.0x2.0mm_inset_D0.5mm_P0.85mm_4h_Trace +Panelization:MouseBite-Slot_2.0x2.0mm_inset_D0.5mm_P0.85mm_5h +Panelization:MouseBite-Slot_2.0x2.0mm_outset_D0.5mm_P0.85mm_2h_Trace +Panelization:MouseBite-Slot_2.0x2.0mm_outset_D0.5mm_P0.85mm_3h +Panelization:MouseBite-Slot_2.0x4.5mm_inset_D0.5mm_P0.85mm_5h_Trace +Panelization:MouseBite-Slot_2.0x4.5mm_inset_D0.5mm_P0.85mm_7h +Panelization:MouseBite-Slot_2.0x4.5mm_outset_D0.5mm_P0.85mm_5h_Trace +Panelization:MouseBite-Slot_2.0x4.5mm_outset_D0.5mm_P0.85mm_7h +Panelization:MouseBite-Slot_2.54x2.54mm_on-edge_D0.5mm_P0.85mm_4h_Trace +Panelization:MouseBite-Slot_2.54x5.08mm_on-edge_D0.5mm_P0.85mm_5h_Trace Potentiometer_SMD:Potentiometer_ACP_CA14-VSMD_Vertical Potentiometer_SMD:Potentiometer_ACP_CA14-VSMD_Vertical_Hole Potentiometer_SMD:Potentiometer_ACP_CA6-VSMD_Vertical @@ -12398,7 +13479,7 @@ Potentiometer_SMD:Potentiometer_Bourns_3269X_Horizontal Potentiometer_SMD:Potentiometer_Bourns_3314G_Vertical Potentiometer_SMD:Potentiometer_Bourns_3314J_Vertical Potentiometer_SMD:Potentiometer_Bourns_3314R-1_Vertical_Hole -Potentiometer_SMD:Potentiometer_Bourns_3314R-GM5_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3314R-GM5_Vertical_Hole Potentiometer_SMD:Potentiometer_Bourns_3314S_Horizontal Potentiometer_SMD:Potentiometer_Bourns_PRS11S_Vertical Potentiometer_SMD:Potentiometer_Bourns_TC33X_Vertical @@ -12461,6 +13542,7 @@ Potentiometer_THT:Potentiometer_Bourns_3339S_Horizontal Potentiometer_THT:Potentiometer_Bourns_3339W_Horizontal Potentiometer_THT:Potentiometer_Bourns_3386C_Horizontal Potentiometer_THT:Potentiometer_Bourns_3386F_Vertical +Potentiometer_THT:Potentiometer_Bourns_3386H_Horizontal Potentiometer_THT:Potentiometer_Bourns_3386P_Vertical Potentiometer_THT:Potentiometer_Bourns_3386W_Horizontal Potentiometer_THT:Potentiometer_Bourns_3386X_Horizontal @@ -12515,6 +13597,191 @@ Potentiometer_THT:Potentiometer_Vishay_T73XX_Horizontal Potentiometer_THT:Potentiometer_Vishay_T73YP_Vertical Potentiometer_THT:Potentiometer_Vishay_T93XA_Horizontal Potentiometer_THT:Potentiometer_Vishay_T93YA_Vertical +RF:Skyworks_SKY13575_639LF +RF:Skyworks_SKY65404-31 +RF_Antenna:AVX_M620720 +RF_Antenna:Abracon_APAES868R8060C16-T +RF_Antenna:Abracon_PRO-OB-440 +RF_Antenna:Abracon_PRO-OB-471 +RF_Antenna:Antenova_SR4G013_GPS +RF_Antenna:Astrocast_AST50127-00 +RF_Antenna:Coilcraft_MA5532-AE_RFID +RF_Antenna:Johanson_2450AT18x100 +RF_Antenna:Johanson_2450AT18x100_2400-2500Mhz +RF_Antenna:Johanson_2450AT43F0100 +RF_Antenna:Johanson_2450AT43F0100_2400-2500Mhz +RF_Antenna:Molex_47948-0001_2.4Ghz +RF_Antenna:NiceRF_SW868-TH13_868Mhz +RF_Antenna:Pulse_W3000 +RF_Antenna:Pulse_W3011 +RF_Antenna:Texas_SWRA117D_2.4GHz_Left +RF_Antenna:Texas_SWRA117D_2.4GHz_Right +RF_Antenna:Texas_SWRA416_868MHz_915MHz +RF_Converter:Anaren_0805_2012Metric-6 +RF_Converter:Balun_Johanson_0896BM15A0001 +RF_Converter:Balun_Johanson_0900FM15K0039 +RF_Converter:Balun_Johanson_0900PC15J0013 +RF_Converter:Balun_Johanson_1.6x0.8mm +RF_Converter:Balun_Johanson_5400BL15B050E +RF_Converter:RF_Attenuator_Susumu_PAT1220 +RF_GPS:Linx_RXM-GPS +RF_GPS:OriginGPS_ORG1510 +RF_GPS:Quectel_L70-R +RF_GPS:Quectel_L76 +RF_GPS:Quectel_L80-R +RF_GPS:Quectel_L96 +RF_GPS:SIM28ML +RF_GPS:Sierra_XA11X0 +RF_GPS:Sierra_XM11X0 +RF_GPS:ublox_LEA +RF_GPS:ublox_MAX +RF_GPS:ublox_NEO +RF_GPS:ublox_SAM-M8Q +RF_GPS:ublox_SAM-M8Q_HandSolder +RF_GPS:ublox_ZED +RF_GPS:ublox_ZOE_M8 +RF_GSM:Quectel_BC66 +RF_GSM:Quectel_BC95 +RF_GSM:Quectel_BG95 +RF_GSM:Quectel_BG96 +RF_GSM:Quectel_M95 +RF_GSM:SIMCom_SIM800C +RF_GSM:SIMCom_SIM900 +RF_GSM:Telit_SE150A4 +RF_GSM:Telit_xL865 +RF_GSM:ublox_LENA-R8_LGA-100 +RF_GSM:ublox_SARA_LGA-96 +RF_Mini-Circuits:Mini-Circuits_BK377 +RF_Mini-Circuits:Mini-Circuits_BK377_LandPatternPL-005 +RF_Mini-Circuits:Mini-Circuits_CD541_H2.08mm +RF_Mini-Circuits:Mini-Circuits_CD542_H2.84mm +RF_Mini-Circuits:Mini-Circuits_CD542_LandPatternPL-052 +RF_Mini-Circuits:Mini-Circuits_CD542_LandPatternPL-094 +RF_Mini-Circuits:Mini-Circuits_CD636_H4.11mm +RF_Mini-Circuits:Mini-Circuits_CD636_LandPatternPL-035 +RF_Mini-Circuits:Mini-Circuits_CD637_H5.23mm +RF_Mini-Circuits:Mini-Circuits_CK605 +RF_Mini-Circuits:Mini-Circuits_CK605_LandPatternPL-012 +RF_Mini-Circuits:Mini-Circuits_DB1627 +RF_Mini-Circuits:Mini-Circuits_GP1212 +RF_Mini-Circuits:Mini-Circuits_GP1212_LandPatternPL-176 +RF_Mini-Circuits:Mini-Circuits_GP731 +RF_Mini-Circuits:Mini-Circuits_GP731_LandPatternPL-176 +RF_Mini-Circuits:Mini-Circuits_HF1139 +RF_Mini-Circuits:Mini-Circuits_HF1139_LandPatternPL-230 +RF_Mini-Circuits:Mini-Circuits_HQ1157 +RF_Mini-Circuits:Mini-Circuits_HZ1198 +RF_Mini-Circuits:Mini-Circuits_HZ1198_LandPatternPL-247 +RF_Mini-Circuits:Mini-Circuits_MMM168 +RF_Mini-Circuits:Mini-Circuits_MMM168_LandPatternPL-225 +RF_Mini-Circuits:Mini-Circuits_QQQ130_ClockwisePinNumbering +RF_Mini-Circuits:Mini-Circuits_QQQ130_LandPattern_PL-236_ClockwisePinNumbering +RF_Mini-Circuits:Mini-Circuits_TT1224_ClockwisePinNumbering +RF_Mini-Circuits:Mini-Circuits_TT1224_LandPatternPL-258_ClockwisePinNumbering +RF_Mini-Circuits:Mini-Circuits_TTT167 +RF_Mini-Circuits:Mini-Circuits_TTT167_LandPatternPL-079 +RF_Mini-Circuits:Mini-Circuits_YY161 +RF_Mini-Circuits:Mini-Circuits_YY161_LandPatternPL-049 +RF_Module:Ai-Thinker-Ra-01-LoRa +RF_Module:Astrocast_AST50147-00 +RF_Module:Atmel_ATSAMR21G18-MR210UA_NoRFPads +RF_Module:BLE112-A +RF_Module:BM78SPPS5xC2 +RF_Module:CMWX1ZZABZ +RF_Module:CYBLE-21Pin-10x10mm +RF_Module:DWM1000 +RF_Module:DecaWave_DWM1001 +RF_Module:Digi_XBee_SMT +RF_Module:E18-MS1-PCB +RF_Module:E73-2G4M04S +RF_Module:ESP-01 +RF_Module:ESP-07 +RF_Module:ESP-12E +RF_Module:ESP-WROOM-02 +RF_Module:ESP32-C3-DevKitM-1 +RF_Module:ESP32-C3-WROOM-02 +RF_Module:ESP32-C3-WROOM-02U +RF_Module:ESP32-C6-MINI-1 +RF_Module:ESP32-S2-MINI-1 +RF_Module:ESP32-S2-MINI-1U +RF_Module:ESP32-S2-WROVER +RF_Module:ESP32-S3-WROOM-1 +RF_Module:ESP32-S3-WROOM-1U +RF_Module:ESP32-S3-WROOM-2 +RF_Module:ESP32-WROOM-32 +RF_Module:ESP32-WROOM-32D +RF_Module:ESP32-WROOM-32E +RF_Module:ESP32-WROOM-32U +RF_Module:ESP32-WROOM-32UE +RF_Module:Garmin_M8-35_9.8x14.0mm_Layout6x6_P1.5mm +RF_Module:HOPERF_RFM69HW +RF_Module:HOPERF_RFM9XW_SMD +RF_Module:HOPERF_RFM9XW_THT +RF_Module:Heltec_HT-CT62 +RF_Module:IQRF_TRx2DA_KON-SIM-01 +RF_Module:IQRF_TRx2D_KON-SIM-01 +RF_Module:Jadak_Thingmagic_M6e-Nano +RF_Module:Laird_BL652 +RF_Module:Laird_BL653 +RF_Module:MCU_Seeed_ESP32C3 +RF_Module:MOD-nRF8001 +RF_Module:Microchip_BM83 +RF_Module:Microchip_RN4871 +RF_Module:Modtronix_inAir9 +RF_Module:MonoWireless_TWE-L-WX +RF_Module:NINA-B111 +RF_Module:Particle_P1 +RF_Module:RAK3172 +RF_Module:RAK4200 +RF_Module:RAK811 +RF_Module:RFDigital_RFD77101 +RF_Module:RMC20452T +RF_Module:RN2483 +RF_Module:RN42 +RF_Module:RN42N +RF_Module:Raytac_MDBT42Q +RF_Module:Raytac_MDBT50Q +RF_Module:ST-SiP-LGA-86-11x7.3mm +RF_Module:ST_SPBTLE +RF_Module:TD1205 +RF_Module:TD1208 +RF_Module:Taiyo-Yuden_EYSGJNZWY +RF_Module:WEMOS_C3_mini +RF_Module:WEMOS_D1_mini_light +RF_Module:ZETA-433-SO_SMD +RF_Module:ZETA-433-SO_THT +RF_Module:nRF24L01_Breakout +RF_Shielding:Laird_Technologies_97-2002_25.40x25.40mm +RF_Shielding:Laird_Technologies_97-2003_12.70x13.37mm +RF_Shielding:Laird_Technologies_BMI-S-101_13.66x12.70mm +RF_Shielding:Laird_Technologies_BMI-S-102_16.50x16.50mm +RF_Shielding:Laird_Technologies_BMI-S-103_26.21x26.21mm +RF_Shielding:Laird_Technologies_BMI-S-104_32.00x32.00mm +RF_Shielding:Laird_Technologies_BMI-S-105_38.10x25.40mm +RF_Shielding:Laird_Technologies_BMI-S-106_36.83x33.68mm +RF_Shielding:Laird_Technologies_BMI-S-107_44.37x44.37mm +RF_Shielding:Laird_Technologies_BMI-S-201-F_13.66x12.70mm +RF_Shielding:Laird_Technologies_BMI-S-202-F_16.50x16.50mm +RF_Shielding:Laird_Technologies_BMI-S-203-F_26.21x26.21mm +RF_Shielding:Laird_Technologies_BMI-S-204-F_32.00x32.00mm +RF_Shielding:Laird_Technologies_BMI-S-205-F_38.10x25.40mm +RF_Shielding:Laird_Technologies_BMI-S-206-F_36.83x33.68mm +RF_Shielding:Laird_Technologies_BMI-S-207-F_44.37x44.37mm +RF_Shielding:Laird_Technologies_BMI-S-208-F_39.60x39.60mm +RF_Shielding:Laird_Technologies_BMI-S-209-F_29.36x18.50mm +RF_Shielding:Laird_Technologies_BMI-S-210-F_44.00x30.50mm +RF_Shielding:Laird_Technologies_BMI-S-230-F_50.8x38.1mm +RF_Shielding:Wuerth_36103205_20x20mm +RF_Shielding:Wuerth_36103255_25x25mm +RF_Shielding:Wuerth_36103305_30x30mm +RF_Shielding:Wuerth_36103505_50x50mm +RF_Shielding:Wuerth_36103605_60x60mm +RF_Shielding:Wuerth_36503205_20x20mm +RF_Shielding:Wuerth_36503255_25x25mm +RF_Shielding:Wuerth_36503305_30x30mm +RF_Shielding:Wuerth_36503505_50x50mm +RF_Shielding:Wuerth_36503605_60x60mm +RF_WiFi:USR-C322 Relay_SMD:Relay_2P2T_10x6mm_TE_IMxxG Relay_SMD:Relay_DPDT_AXICOM_IMSeries_JLeg Relay_SMD:Relay_DPDT_FRT5_SMD @@ -12525,10 +13792,10 @@ Relay_SMD:Relay_DPDT_Kemet_EE2_NUX_DoubleCoil Relay_SMD:Relay_DPDT_Kemet_EE2_NUX_NKX Relay_SMD:Relay_DPDT_Kemet_EE2_NU_DoubleCoil Relay_SMD:Relay_DPDT_Omron_G6H-2F -Relay_SMD:Relay_DPDT_Omron_G6K-2F-Y Relay_SMD:Relay_DPDT_Omron_G6K-2F -Relay_SMD:Relay_DPDT_Omron_G6K-2G-Y +Relay_SMD:Relay_DPDT_Omron_G6K-2F-Y Relay_SMD:Relay_DPDT_Omron_G6K-2G +Relay_SMD:Relay_DPDT_Omron_G6K-2G-Y Relay_SMD:Relay_DPDT_Omron_G6S-2F Relay_SMD:Relay_DPDT_Omron_G6S-2G Relay_SMD:Relay_DPDT_Omron_G6SK-2F @@ -12543,10 +13810,10 @@ Relay_THT:Relay_3PST_COTO_3650 Relay_THT:Relay_3PST_COTO_3660 Relay_THT:Relay_DPDT_AXICOM_IMSeries_Pitch3.2mm Relay_THT:Relay_DPDT_AXICOM_IMSeries_Pitch5.08mm +Relay_THT:Relay_DPDT_FRT5 Relay_THT:Relay_DPDT_Finder_30.22 Relay_THT:Relay_DPDT_Finder_40.52 Relay_THT:Relay_DPDT_Finder_40.62 -Relay_THT:Relay_DPDT_FRT5 Relay_THT:Relay_DPDT_Fujitsu_FTR-F1C Relay_THT:Relay_DPDT_Hongfa_HF115F-2Z-x4 Relay_THT:Relay_DPDT_Kemet_EC2_NJ @@ -12558,8 +13825,8 @@ Relay_THT:Relay_DPDT_Omron_G5V-2 Relay_THT:Relay_DPDT_Omron_G6A Relay_THT:Relay_DPDT_Omron_G6AK Relay_THT:Relay_DPDT_Omron_G6H-2 -Relay_THT:Relay_DPDT_Omron_G6K-2P-Y Relay_THT:Relay_DPDT_Omron_G6K-2P +Relay_THT:Relay_DPDT_Omron_G6K-2P-Y Relay_THT:Relay_DPDT_Omron_G6S-2 Relay_THT:Relay_DPDT_Omron_G6SK-2 Relay_THT:Relay_DPDT_Panasonic_JW2 @@ -12570,10 +13837,7 @@ Relay_THT:Relay_DPST_Fujitsu_FTR-F1A Relay_THT:Relay_DPST_Omron_G2RL-2A Relay_THT:Relay_DPST_Schrack-RT2-FormA_RM5mm Relay_THT:Relay_NCR_HHG1D-1 -Relay_THT:Relay_Socket_3PDT_Omron_PLE11-0 -Relay_THT:Relay_Socket_4PDT_Omron_PY14-02 -Relay_THT:Relay_Socket_DPDT_Finder_96.12 -Relay_THT:Relay_Socket_DPDT_Omron_PLE08-0 +Relay_THT:Relay_SPDT_CUI_SR5 Relay_THT:Relay_SPDT_Finder_32.21-x000 Relay_THT:Relay_SPDT_Finder_34.51_Horizontal Relay_THT:Relay_SPDT_Finder_34.51_Vertical @@ -12586,20 +13850,20 @@ Relay_THT:Relay_SPDT_Finder_40.61 Relay_THT:Relay_SPDT_Fujitsu_FTR-LYCA005x_FormC_Vertical Relay_THT:Relay_SPDT_HJR-4102 Relay_THT:Relay_SPDT_Hongfa_HF3F-L-xx-1ZL1T -Relay_THT:Relay_SPDT_Hongfa_HF3F-L-xx-1ZL2T-R Relay_THT:Relay_SPDT_Hongfa_HF3F-L-xx-1ZL2T +Relay_THT:Relay_SPDT_Hongfa_HF3F-L-xx-1ZL2T-R Relay_THT:Relay_SPDT_Hongfa_JQC-3FF_0XX-1Z Relay_THT:Relay_SPDT_HsinDa_Y14 Relay_THT:Relay_SPDT_Omron-G5LE-1 Relay_THT:Relay_SPDT_Omron-G5Q-1 -Relay_THT:Relay_SPDT_Omron_G2RL-1-E Relay_THT:Relay_SPDT_Omron_G2RL-1 +Relay_THT:Relay_SPDT_Omron_G2RL-1-E Relay_THT:Relay_SPDT_Omron_G5V-1 Relay_THT:Relay_SPDT_Omron_G6E Relay_THT:Relay_SPDT_Omron_G6EK +Relay_THT:Relay_SPDT_Panasonic_DR Relay_THT:Relay_SPDT_Panasonic_DR-L Relay_THT:Relay_SPDT_Panasonic_DR-L2 -Relay_THT:Relay_SPDT_Panasonic_DR Relay_THT:Relay_SPDT_Panasonic_JW1_FormC Relay_THT:Relay_SPDT_PotterBrumfield_T9AP5D52_12V30A Relay_THT:Relay_SPDT_RAYEX-L90 @@ -12615,12 +13879,12 @@ Relay_THT:Relay_SPDT_StandexMeder_SIL_Form1C Relay_THT:Relay_SPST-NO_Fujitsu_FTR-LYAA005x_FormA_Vertical Relay_THT:Relay_SPST_Finder_32.21-x300 Relay_THT:Relay_SPST_Hongfa_HF3F-L-xx-1HL1T -Relay_THT:Relay_SPST_Hongfa_HF3F-L-xx-1HL2T-R Relay_THT:Relay_SPST_Hongfa_HF3F-L-xx-1HL2T +Relay_THT:Relay_SPST_Hongfa_HF3F-L-xx-1HL2T-R Relay_THT:Relay_SPST_Hongfa_JQC-3FF_0XX-1H Relay_THT:Relay_SPST_Omron-G5Q-1A -Relay_THT:Relay_SPST_Omron_G2RL-1A-E Relay_THT:Relay_SPST_Omron_G2RL-1A +Relay_THT:Relay_SPST_Omron_G2RL-1A-E Relay_THT:Relay_SPST_Omron_G5NB Relay_THT:Relay_SPST_Omron_G5PZ Relay_THT:Relay_SPST_Panasonic_ADW11 @@ -12648,6 +13912,10 @@ Relay_THT:Relay_SPST_StandexMeder_SIL_Form1B Relay_THT:Relay_SPST_TE_PCH-1xxx2M Relay_THT:Relay_SPST_TE_PCN-1xxD3MHZ Relay_THT:Relay_SPST_Zettler-AZSR131 +Relay_THT:Relay_Socket_3PDT_Omron_PLE11-0 +Relay_THT:Relay_Socket_4PDT_Omron_PY14-02 +Relay_THT:Relay_Socket_DPDT_Finder_96.12 +Relay_THT:Relay_Socket_DPDT_Omron_PLE08-0 Relay_THT:Relay_StandexMeder_DIP_HighProfile Relay_THT:Relay_StandexMeder_DIP_LowProfile Relay_THT:Relay_StandexMeder_UMS @@ -12658,6 +13926,8 @@ Resistor_SMD:R_0201_0603Metric Resistor_SMD:R_0201_0603Metric_Pad0.64x0.40mm_HandSolder Resistor_SMD:R_0402_1005Metric Resistor_SMD:R_0402_1005Metric_Pad0.72x0.64mm_HandSolder +Resistor_SMD:R_0508_1220Metric +Resistor_SMD:R_0508_1220Metric_Pad1.12x2.15mm_HandSolder Resistor_SMD:R_0603_1608Metric Resistor_SMD:R_0603_1608Metric_Pad0.98x0.95mm_HandSolder Resistor_SMD:R_0612_1632Metric @@ -12674,6 +13944,8 @@ Resistor_SMD:R_1210_3225Metric Resistor_SMD:R_1210_3225Metric_Pad1.30x2.65mm_HandSolder Resistor_SMD:R_1218_3246Metric Resistor_SMD:R_1218_3246Metric_Pad1.22x4.75mm_HandSolder +Resistor_SMD:R_1225_3264Metric +Resistor_SMD:R_1225_3264Metric_Pad1.47x6.45mm_HandSolder Resistor_SMD:R_1812_4532Metric Resistor_SMD:R_1812_4532Metric_Pad1.30x3.40mm_HandSolder Resistor_SMD:R_2010_5025Metric @@ -12819,187 +14091,6 @@ Resistor_THT:R_Radial_Power_L13.0mm_W9.0mm_P5.00mm Resistor_THT:R_Radial_Power_L16.1mm_W9.0mm_P7.37mm Resistor_THT:R_Radial_Power_L7.0mm_W8.0mm_Px2.40mm_Py2.30mm Resistor_THT:R_Radial_Power_L9.0mm_W10.0mm_Px2.70mm_Py2.30mm -RF:Skyworks_SKY13575_639LF -RF:Skyworks_SKY65404-31 -RF_Antenna:Abracon_APAES868R8060C16-T -RF_Antenna:Abracon_PRO-OB-440 -RF_Antenna:Abracon_PRO-OB-471 -RF_Antenna:Antenova_SR4G013_GPS -RF_Antenna:Astrocast_AST50127-00 -RF_Antenna:AVX_M620720 -RF_Antenna:Coilcraft_MA5532-AE_RFID -RF_Antenna:Johanson_2450AT18x100 -RF_Antenna:Johanson_2450AT43F0100 -RF_Antenna:Molex_47948-0001_2.4Ghz -RF_Antenna:NiceRF_SW868-TH13_868Mhz -RF_Antenna:Pulse_W3000 -RF_Antenna:Pulse_W3011 -RF_Antenna:Texas_SWRA117D_2.4GHz_Left -RF_Antenna:Texas_SWRA117D_2.4GHz_Right -RF_Antenna:Texas_SWRA416_868MHz_915MHz -RF_Converter:Anaren_0805_2012Metric-6 -RF_Converter:Balun_Johanson_0896BM15A0001 -RF_Converter:Balun_Johanson_0900FM15K0039 -RF_Converter:Balun_Johanson_0900PC15J0013 -RF_Converter:Balun_Johanson_1.6x0.8mm -RF_Converter:Balun_Johanson_5400BL15B050E -RF_Converter:RF_Attenuator_Susumu_PAT1220 -RF_GPS:Linx_RXM-GPS -RF_GPS:OriginGPS_ORG1510 -RF_GPS:Quectel_L70-R -RF_GPS:Quectel_L76 -RF_GPS:Quectel_L80-R -RF_GPS:Quectel_L96 -RF_GPS:Sierra_XA11X0 -RF_GPS:Sierra_XM11X0 -RF_GPS:SIM28ML -RF_GPS:ublox_LEA -RF_GPS:ublox_MAX -RF_GPS:ublox_NEO -RF_GPS:ublox_SAM-M8Q -RF_GPS:ublox_SAM-M8Q_HandSolder -RF_GPS:ublox_ZED -RF_GPS:ublox_ZOE_M8 -RF_GSM:Quectel_BC66 -RF_GSM:Quectel_BC95 -RF_GSM:Quectel_BG95 -RF_GSM:Quectel_BG96 -RF_GSM:Quectel_M95 -RF_GSM:SIMCom_SIM800C -RF_GSM:SIMCom_SIM900 -RF_GSM:Telit_SE150A4 -RF_GSM:Telit_xL865 -RF_GSM:ublox_LENA-R8_LGA-100 -RF_GSM:ublox_SARA_LGA-96 -RF_Mini-Circuits:Mini-Circuits_BK377 -RF_Mini-Circuits:Mini-Circuits_BK377_LandPatternPL-005 -RF_Mini-Circuits:Mini-Circuits_CD541_H2.08mm -RF_Mini-Circuits:Mini-Circuits_CD542_H2.84mm -RF_Mini-Circuits:Mini-Circuits_CD542_LandPatternPL-052 -RF_Mini-Circuits:Mini-Circuits_CD542_LandPatternPL-094 -RF_Mini-Circuits:Mini-Circuits_CD636_H4.11mm -RF_Mini-Circuits:Mini-Circuits_CD636_LandPatternPL-035 -RF_Mini-Circuits:Mini-Circuits_CD637_H5.23mm -RF_Mini-Circuits:Mini-Circuits_CK605 -RF_Mini-Circuits:Mini-Circuits_CK605_LandPatternPL-012 -RF_Mini-Circuits:Mini-Circuits_DB1627 -RF_Mini-Circuits:Mini-Circuits_GP1212 -RF_Mini-Circuits:Mini-Circuits_GP1212_LandPatternPL-176 -RF_Mini-Circuits:Mini-Circuits_GP731 -RF_Mini-Circuits:Mini-Circuits_GP731_LandPatternPL-176 -RF_Mini-Circuits:Mini-Circuits_HF1139 -RF_Mini-Circuits:Mini-Circuits_HF1139_LandPatternPL-230 -RF_Mini-Circuits:Mini-Circuits_HQ1157 -RF_Mini-Circuits:Mini-Circuits_HZ1198 -RF_Mini-Circuits:Mini-Circuits_HZ1198_LandPatternPL-247 -RF_Mini-Circuits:Mini-Circuits_MMM168 -RF_Mini-Circuits:Mini-Circuits_MMM168_LandPatternPL-225 -RF_Mini-Circuits:Mini-Circuits_QQQ130_ClockwisePinNumbering -RF_Mini-Circuits:Mini-Circuits_QQQ130_LandPattern_PL-236_ClockwisePinNumbering -RF_Mini-Circuits:Mini-Circuits_TT1224_ClockwisePinNumbering -RF_Mini-Circuits:Mini-Circuits_TT1224_LandPatternPL-258_ClockwisePinNumbering -RF_Mini-Circuits:Mini-Circuits_TTT167 -RF_Mini-Circuits:Mini-Circuits_TTT167_LandPatternPL-079 -RF_Mini-Circuits:Mini-Circuits_YY161 -RF_Mini-Circuits:Mini-Circuits_YY161_LandPatternPL-049 -RF_Module:Ai-Thinker-Ra-01-LoRa -RF_Module:Astrocast_AST50147-00 -RF_Module:Atmel_ATSAMR21G18-MR210UA_NoRFPads -RF_Module:BLE112-A -RF_Module:BM78SPPS5xC2 -RF_Module:CMWX1ZZABZ -RF_Module:CYBLE-21Pin-10x10mm -RF_Module:DecaWave_DWM1001 -RF_Module:Digi_XBee_SMT -RF_Module:DWM1000 -RF_Module:E18-MS1-PCB -RF_Module:E73-2G4M04S -RF_Module:ESP-01 -RF_Module:ESP-07 -RF_Module:ESP-12E -RF_Module:ESP-WROOM-02 -RF_Module:ESP32-C3-DevKitM-1 -RF_Module:ESP32-C3-WROOM-02 -RF_Module:ESP32-C3-WROOM-02U -RF_Module:ESP32-C6-MINI-1 -RF_Module:ESP32-S2-MINI-1 -RF_Module:ESP32-S2-MINI-1U -RF_Module:ESP32-S2-WROVER -RF_Module:ESP32-S3-WROOM-1 -RF_Module:ESP32-S3-WROOM-1U -RF_Module:ESP32-S3-WROOM-2 -RF_Module:ESP32-WROOM-32 -RF_Module:ESP32-WROOM-32D -RF_Module:ESP32-WROOM-32E -RF_Module:ESP32-WROOM-32U -RF_Module:ESP32-WROOM-32UE -RF_Module:Garmin_M8-35_9.8x14.0mm_Layout6x6_P1.5mm -RF_Module:Heltec_HT-CT62 -RF_Module:HOPERF_RFM69HW -RF_Module:HOPERF_RFM9XW_SMD -RF_Module:HOPERF_RFM9XW_THT -RF_Module:IQRF_TRx2DA_KON-SIM-01 -RF_Module:IQRF_TRx2D_KON-SIM-01 -RF_Module:Jadak_Thingmagic_M6e-Nano -RF_Module:Laird_BL652 -RF_Module:MCU_Seeed_ESP32C3 -RF_Module:Microchip_BM83 -RF_Module:Microchip_RN4871 -RF_Module:MOD-nRF8001 -RF_Module:Modtronix_inAir9 -RF_Module:MonoWireless_TWE-L-WX -RF_Module:NINA-B111 -RF_Module:nRF24L01_Breakout -RF_Module:Particle_P1 -RF_Module:RAK3172 -RF_Module:RAK4200 -RF_Module:RAK811 -RF_Module:Raytac_MDBT42Q -RF_Module:Raytac_MDBT50Q -RF_Module:RFDigital_RFD77101 -RF_Module:RN2483 -RF_Module:RN42 -RF_Module:RN42N -RF_Module:ST-SiP-LGA-86-11x7.3mm -RF_Module:ST_SPBTLE -RF_Module:Taiyo-Yuden_EYSGJNZWY -RF_Module:TD1205 -RF_Module:TD1208 -RF_Module:WEMOS_C3_mini -RF_Module:WEMOS_D1_mini_light -RF_Module:ZETA-433-SO_SMD -RF_Module:ZETA-433-SO_THT -RF_Shielding:Laird_Technologies_97-2002_25.40x25.40mm -RF_Shielding:Laird_Technologies_97-2003_12.70x13.37mm -RF_Shielding:Laird_Technologies_BMI-S-101_13.66x12.70mm -RF_Shielding:Laird_Technologies_BMI-S-102_16.50x16.50mm -RF_Shielding:Laird_Technologies_BMI-S-103_26.21x26.21mm -RF_Shielding:Laird_Technologies_BMI-S-104_32.00x32.00mm -RF_Shielding:Laird_Technologies_BMI-S-105_38.10x25.40mm -RF_Shielding:Laird_Technologies_BMI-S-106_36.83x33.68mm -RF_Shielding:Laird_Technologies_BMI-S-107_44.37x44.37mm -RF_Shielding:Laird_Technologies_BMI-S-201-F_13.66x12.70mm -RF_Shielding:Laird_Technologies_BMI-S-202-F_16.50x16.50mm -RF_Shielding:Laird_Technologies_BMI-S-203-F_26.21x26.21mm -RF_Shielding:Laird_Technologies_BMI-S-204-F_32.00x32.00mm -RF_Shielding:Laird_Technologies_BMI-S-205-F_38.10x25.40mm -RF_Shielding:Laird_Technologies_BMI-S-206-F_36.83x33.68mm -RF_Shielding:Laird_Technologies_BMI-S-207-F_44.37x44.37mm -RF_Shielding:Laird_Technologies_BMI-S-208-F_39.60x39.60mm -RF_Shielding:Laird_Technologies_BMI-S-209-F_29.36x18.50mm -RF_Shielding:Laird_Technologies_BMI-S-210-F_44.00x30.50mm -RF_Shielding:Laird_Technologies_BMI-S-230-F_50.8x38.1mm -RF_Shielding:Wuerth_36103205_20x20mm -RF_Shielding:Wuerth_36103255_25x25mm -RF_Shielding:Wuerth_36103305_30x30mm -RF_Shielding:Wuerth_36103505_50x50mm -RF_Shielding:Wuerth_36103605_60x60mm -RF_Shielding:Wuerth_36503205_20x20mm -RF_Shielding:Wuerth_36503255_25x25mm -RF_Shielding:Wuerth_36503305_30x30mm -RF_Shielding:Wuerth_36503505_50x50mm -RF_Shielding:Wuerth_36503605_60x60mm -RF_WiFi:USR-C322 Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm_CircularMountingHoles Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm_MountingHoles @@ -13023,19 +14114,19 @@ Rotary_Encoder:RotaryEncoder_Bourns_Vertical_PEC12R-3x17F-Nxxxx Rotary_Encoder:RotaryEncoder_Bourns_Vertical_PEC12R-3x17F-Sxxxx Rotary_Encoder:RotaryEncoder_Bourns_Vertical_PEL12D-4x25S-Sxxxx Rotary_Encoder:RotaryEncoder_Bourns_Vertical_PEL12D-4xxxF-Sxxxx -Sensor:Aosong_DHT11_5.5x12.0_P2.54mm Sensor:ASAIR_AM2302_P2.54mm_Lead2.75mm_TabDown Sensor:ASAIR_AM2302_P2.54mm_Vertical +Sensor:Aosong_DHT11_5.5x12.0_P2.54mm Sensor:Avago_APDS-9960 Sensor:LuminOX_LOX-O2 Sensor:MQ-6 Sensor:Rohm_RPR-0521RS +Sensor:SHT1x +Sensor:SPEC_110-xxx_SMD-10Pin_20x20mm_P4.0mm Sensor:Senseair_S8_Down Sensor:Senseair_S8_Up Sensor:Sensirion_SCD4x-1EP_10.1x10.1mm_P1.25mm_EP4.8x4.8mm Sensor:Sensortech_MiCS_5x7mm_P1.25mm -Sensor:SHT1x -Sensor:SPEC_110-xxx_SMD-10Pin_20x20mm_P4.0mm Sensor:TGS-5141 Sensor:Winson_GM-402B_5x5mm_P1.27mm Sensor_Audio:CUI_CMC-4013-SMT @@ -13057,7 +14148,6 @@ Sensor_Current:Allegro_CB_PSS Sensor_Current:Allegro_PSOF-7_4.8x6.4mm_P1.60mm Sensor_Current:Allegro_QFN-12-10-1EP_3x3mm_P0.5mm Sensor_Current:Allegro_QSOP-24_3.9x8.7mm_P0.635mm -Sensor_Current:Allegro_SIP-3 Sensor_Current:Allegro_SIP-4 Sensor_Current:Diodes_SIP-3_4.1x1.5mm_P1.27mm Sensor_Current:Diodes_SIP-3_4.1x1.5mm_P2.65mm @@ -13088,6 +14178,7 @@ Sensor_Distance:ST_VL53L1x Sensor_Humidity:Sensirion_DFN-4-1EP_2x2mm_P1mm_EP0.7x1.6mm Sensor_Humidity:Sensirion_DFN-4_1.5x1.5mm_P0.8mm_SHT4x_NoCentralPad Sensor_Humidity:Sensirion_DFN-8-1EP_2.5x2.5mm_P0.5mm_EP1.1x1.7mm +Sensor_Humidity:Texas_S-PWSON-N6-HDC2080 Sensor_Motion:Analog_LGA-16_3.25x3mm_P0.5mm_LayoutBorder3x5y Sensor_Motion:InvenSense_QFN-24_3x3mm_P0.4mm Sensor_Motion:InvenSense_QFN-24_3x3mm_P0.4mm_NoMask @@ -13099,23 +14190,24 @@ Sensor_Pressure:CFSensor_XGZP6899x Sensor_Pressure:Freescale_98ARH99066A Sensor_Pressure:Freescale_98ARH99089A Sensor_Pressure:Honeywell_40PCxxxG1A +Sensor_Pressure:Honeywell_ABPDAN Sensor_Pressure:TE_MS5525DSO-DBxxxyS Sensor_Pressure:TE_MS5837-xxBA Sensor_Voltage:LEM_LV25-P Socket:3M_Textool_240-1288-00-0602J_2x20_P2.54mm -Socket:DIP_Socket-14_W4.3_W5.08_W7.62_W10.16_W10.9_3M_214-3339-00-0602J -Socket:DIP_Socket-16_W4.3_W5.08_W7.62_W10.16_W10.9_3M_216-3340-00-0602J -Socket:DIP_Socket-18_W4.3_W5.08_W7.62_W10.16_W10.9_3M_218-3341-00-0602J -Socket:DIP_Socket-20_W4.3_W5.08_W7.62_W10.16_W10.9_3M_220-3342-00-0602J -Socket:DIP_Socket-22_W6.9_W7.62_W10.16_W12.7_W13.5_3M_222-3343-00-0602J -Socket:DIP_Socket-24_W11.9_W12.7_W15.24_W17.78_W18.5_3M_224-1275-00-0602J -Socket:DIP_Socket-24_W4.3_W5.08_W7.62_W10.16_W10.9_3M_224-5248-00-0602J -Socket:DIP_Socket-28_W11.9_W12.7_W15.24_W17.78_W18.5_3M_228-1277-00-0602J -Socket:DIP_Socket-28_W6.9_W7.62_W10.16_W12.7_W13.5_3M_228-4817-00-0602J -Socket:DIP_Socket-32_W11.9_W12.7_W15.24_W17.78_W18.5_3M_232-1285-00-0602J -Socket:DIP_Socket-40_W11.9_W12.7_W15.24_W17.78_W18.5_3M_240-1280-00-0602J -Socket:DIP_Socket-40_W22.1_W22.86_W25.4_W27.94_W28.7_3M_240-3639-00-0602J -Socket:DIP_Socket-42_W11.9_W12.7_W15.24_W17.78_W18.5_3M_242-1281-00-0602J +Socket:DIP_Socket-14_W4.3mm_W5.08mm_W7.62mm_W10.16mm_W10.9mm_3M_214-3339-00-0602J +Socket:DIP_Socket-16_W4.3mm_W5.08mm_W7.62mm_W10.16mm_W10.9mm_3M_216-3340-00-0602J +Socket:DIP_Socket-18_W4.3mm_W5.08mm_W7.62mm_W10.16mm_W10.9mm_3M_218-3341-00-0602J +Socket:DIP_Socket-20_W4.3mm_W5.08mm_W7.62mm_W10.16mm_W10.9mm_3M_220-3342-00-0602J +Socket:DIP_Socket-22_W6.9mm_W7.62mm_W10.16mm_W12.7mm_W13.5mm_3M_222-3343-00-0602J +Socket:DIP_Socket-24_W11.9mm_W12.7mm_W15.24mm_W17.78mm_W18.5mm_3M_224-1275-00-0602J +Socket:DIP_Socket-24_W4.3mm_W5.08mm_W7.62mm_W10.16mm_W10.9mm_3M_224-5248-00-0602J +Socket:DIP_Socket-28_W11.9mm_W12.7mm_W15.24mm_W17.78mm_W18.5mm_3M_228-1277-00-0602J +Socket:DIP_Socket-28_W6.9mm_W7.62mm_W10.16mm_W12.7mm_W13.5mm_3M_228-4817-00-0602J +Socket:DIP_Socket-32_W11.9mm_W12.7mm_W15.24mm_W17.78mm_W18.5mm_3M_232-1285-00-0602J +Socket:DIP_Socket-40_W11.9mm_W12.7mm_W15.24mm_W17.78mm_W18.5mm_3M_240-1280-00-0602J +Socket:DIP_Socket-40_W22.1mm_W22.86mm_W25.4mm_W27.94mm_W28.7mm_3M_240-3639-00-0602J +Socket:DIP_Socket-42_W11.9mm_W12.7mm_W15.24mm_W17.78mm_W18.5mm_3M_242-1281-00-0602J Socket:Wells_648-0482211SA01 Symbol:CE-Logo_11.2x8mm_SilkScreen Symbol:CE-Logo_16.8x12mm_SilkScreen @@ -13123,13 +14215,13 @@ Symbol:CE-Logo_28x20mm_SilkScreen Symbol:CE-Logo_42x30mm_SilkScreen Symbol:CE-Logo_56.1x40mm_SilkScreen Symbol:CE-Logo_8.5x6mm_SilkScreen -Symbol:EasterEgg_EWG1308-2013_ClassA Symbol:ESD-Logo_13.2x12mm_SilkScreen Symbol:ESD-Logo_22x20mm_SilkScreen Symbol:ESD-Logo_33x30mm_SilkScreen Symbol:ESD-Logo_44.1x40mm_SilkScreen Symbol:ESD-Logo_6.6x6mm_SilkScreen Symbol:ESD-Logo_8.9x8mm_SilkScreen +Symbol:EasterEgg_EWG1308-2013_ClassA Symbol:FCC-Logo_14.6x12mm_SilkScreen Symbol:FCC-Logo_24.2x20mm_SilkScreen Symbol:FCC-Logo_36.3x30mm_SilkScreen @@ -13164,6 +14256,67 @@ Symbol:KiCad-Logo_6mm_Copper Symbol:KiCad-Logo_6mm_SilkScreen Symbol:KiCad-Logo_8mm_Copper Symbol:KiCad-Logo_8mm_SilkScreen +Symbol:LayerMarker_10_14x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_10_14x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_10_14x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_10_14x2.54mm_TextH1mm_P1.27mm_Named +Symbol:LayerMarker_10_14x2.54mm_TextH1mm_P1.27mm_Named_BottomMirrored +Symbol:LayerMarker_10_14x2.54mm_TextH1mm_P1.27mm_Named_LowerMirrored +Symbol:LayerMarker_12_16.5x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_12_16.5x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_12_16.5x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_14_19.1x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_14_19.1x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_14_19.1x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_16_21.6x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_16_21.6x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_16_21.6x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_18_24.1x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_18_24.1x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_18_24.1x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_20_26.7x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_20_26.7x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_20_26.7x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_22_29.2x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_22_29.2x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_22_29.2x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_24_31.8x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_24_31.8x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_24_31.8x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_26_34.3x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_26_34.3x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_26_34.3x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_28_36.8x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_28_36.8x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_28_36.8x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_2_3.81x2.54mm_TextH1mm_P1.27mm +Symbol:LayerMarker_2_3.81x2.54mm_TextH1mm_P1.27mm_BottomMirrored +Symbol:LayerMarker_2_3.81x2.54mm_TextH1mm_P1.27mm_Named +Symbol:LayerMarker_2_3.81x2.54mm_TextH1mm_P1.27mm_Named_BottomMirrored +Symbol:LayerMarker_30_39.4x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_30_39.4x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_30_39.4x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_32_41.9x2.54mm_TextH1mm_P1.27mm_AlNum +Symbol:LayerMarker_32_41.9x2.54mm_TextH1mm_P1.27mm_AlNum_BottomMirrored +Symbol:LayerMarker_32_41.9x2.54mm_TextH1mm_P1.27mm_AlNum_LowerMirrored +Symbol:LayerMarker_4_6.35x2.54mm_TextH1mm_P1.27mm +Symbol:LayerMarker_4_6.35x2.54mm_TextH1mm_P1.27mm_BottomMirrored +Symbol:LayerMarker_4_6.35x2.54mm_TextH1mm_P1.27mm_LowerMirrored +Symbol:LayerMarker_4_6.35x2.54mm_TextH1mm_P1.27mm_Named +Symbol:LayerMarker_4_6.35x2.54mm_TextH1mm_P1.27mm_Named_BottomMirrored +Symbol:LayerMarker_4_6.35x2.54mm_TextH1mm_P1.27mm_Named_LowerMirrored +Symbol:LayerMarker_6_8.89x2.54mm_TextH1mm_P1.27mm +Symbol:LayerMarker_6_8.89x2.54mm_TextH1mm_P1.27mm_BottomMirrored +Symbol:LayerMarker_6_8.89x2.54mm_TextH1mm_P1.27mm_LowerMirrored +Symbol:LayerMarker_6_8.89x2.54mm_TextH1mm_P1.27mm_Named +Symbol:LayerMarker_6_8.89x2.54mm_TextH1mm_P1.27mm_Named_BottomMirrored +Symbol:LayerMarker_6_8.89x2.54mm_TextH1mm_P1.27mm_Named_LowerMirrored +Symbol:LayerMarker_8_11.4x2.54mm_TextH1mm_P1.27mm +Symbol:LayerMarker_8_11.4x2.54mm_TextH1mm_P1.27mm_BottomMirrored +Symbol:LayerMarker_8_11.4x2.54mm_TextH1mm_P1.27mm_LowerMirrored +Symbol:LayerMarker_8_11.4x2.54mm_TextH1mm_P1.27mm_Named +Symbol:LayerMarker_8_11.4x2.54mm_TextH1mm_P1.27mm_Named_BottomMirrored +Symbol:LayerMarker_8_11.4x2.54mm_TextH1mm_P1.27mm_Named_LowerMirrored Symbol:OSHW-Logo2_14.6x12mm_Copper Symbol:OSHW-Logo2_14.6x12mm_SilkScreen Symbol:OSHW-Logo2_24.3x20mm_Copper @@ -13218,6 +14371,7 @@ Symbol:RoHS-Logo_30mm_SilkScreen Symbol:RoHS-Logo_40mm_SilkScreen Symbol:RoHS-Logo_6mm_SilkScreen Symbol:RoHS-Logo_8mm_SilkScreen +Symbol:Screw_Generic_2.0x3.0mm_SilkScreen Symbol:Smolhaj_Scale_0.1 Symbol:Symbol_Attention_Triangle_17x15mm_Copper Symbol:Symbol_Attention_Triangle_8x7mm_Copper @@ -13262,16 +14416,6 @@ Symbol:WEEE-Logo_28.1x40mm_SilkScreen Symbol:WEEE-Logo_4.2x6mm_SilkScreen Symbol:WEEE-Logo_5.6x8mm_SilkScreen Symbol:WEEE-Logo_8.4x12mm_SilkScreen -TerminalBlock:TerminalBlock_Altech_AK300-2_P5.00mm -TerminalBlock:TerminalBlock_Altech_AK300-3_P5.00mm -TerminalBlock:TerminalBlock_Altech_AK300-4_P5.00mm -TerminalBlock:TerminalBlock_bornier-2_P5.08mm -TerminalBlock:TerminalBlock_bornier-3_P5.08mm -TerminalBlock:TerminalBlock_bornier-4_P5.08mm -TerminalBlock:TerminalBlock_bornier-5_P5.08mm -TerminalBlock:TerminalBlock_bornier-6_P5.08mm -TerminalBlock:TerminalBlock_bornier-8_P5.08mm -TerminalBlock:TerminalBlock_Degson_DG246-3.81-03P TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-02P_1x02_P5.00mm TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-03P_1x03_P5.00mm TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-04P_1x04_P5.00mm @@ -13295,7 +14439,6 @@ TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-21P_1x21_P5.00mm TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-22P_1x22_P5.00mm TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-23P_1x23_P5.00mm TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-24P_1x24_P5.00mm -TerminalBlock:TerminalBlock_Wuerth_691311400102_P7.62mm TerminalBlock:TerminalBlock_Xinya_XY308-2.54-10P_1x10_P2.54mm_Horizontal TerminalBlock:TerminalBlock_Xinya_XY308-2.54-11P_1x11_P2.54mm_Horizontal TerminalBlock:TerminalBlock_Xinya_XY308-2.54-12P_1x12_P2.54mm_Horizontal @@ -13415,6 +14558,28 @@ TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-21_1x21_P5.08mm_Horizontal TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-22_1x22_P5.08mm_Horizontal TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-23_1x23_P5.08mm_Horizontal TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-24_1x24_P5.08mm_Horizontal +TerminalBlock_Degson:Degson_DG246-3.81-03P +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-01P_1x01_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-02P_1x02_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-03P_1x03_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-04P_1x04_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-05P_1x05_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-06P_1x06_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-07P_1x07_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-08P_1x08_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-09P_1x09_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-10P_1x10_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-11P_1x11_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-12P_1x12_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-14P_1x14_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-15P_1x15_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-16P_1x16_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-18P_1x18_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-19P_1x19_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-20P_1x20_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-24P_1x24_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-44P_1x44_P3.50mm_45Degree +TerminalBlock_Degson:TerminalBlock_Degson_DG250-3.5-45P_1x45_P3.50mm_45Degree TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-02_P10.00mm TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-03_P10.00mm TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-04_P10.00mm @@ -13452,11 +14617,11 @@ TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360322_1x01_Horizontal_Screw TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360381_1x01_Horizontal_ScrewM3.0 TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360410_1x01_Horizontal_ScrewM3.0 TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360425_1x01_Horizontal_ScrewM4.0_Boxed -TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05502HBWC_1x02_P5.00mm_Horizontal -TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05503HBWC_1x03_P5.00mm_Horizontal -TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05504HBWC_1x04_P5.00mm_Horizontal -TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05505HBWC_1x05_P5.00mm_Horizontal -TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05506HBWC_1x06_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05502HBLC_1x02_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05503HBLC_1x03_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05504HBLC_1x04_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05505HBLC_1x05_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05506HBLC_1x06_P5.00mm_Horizontal TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type055_RT01502HDWU_1x02_P5.00mm_Horizontal TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type055_RT01503HDWU_1x03_P5.00mm_Horizontal TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type055_RT01504HDWU_1x04_P5.00mm_Horizontal @@ -13505,6 +14670,28 @@ TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type701_RT11L02HGLU_1x02_P6. TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type701_RT11L03HGLU_1x03_P6.35mm_Horizontal TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type703_RT10N02HGLU_1x02_P9.52mm_Horizontal TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type703_RT10N03HGLU_1x03_P9.52mm_Horizontal +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x02_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x03_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x04_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x05_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x06_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x07_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x08_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x09_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x10_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x11_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500M_1x12_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x02_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x03_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x04_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x05_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x06_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x07_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x08_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x09_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x10_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x11_P9.5mm +TerminalBlock_Ningbo-Kagnex:TerminalBlock_Ningbo-Kagnex_HB9500_1x12_P9.5mm TerminalBlock_Philmore:TerminalBlock_Philmore_TB132_1x02_P5.00mm_Horizontal TerminalBlock_Philmore:TerminalBlock_Philmore_TB133_1x03_P5.00mm_Horizontal TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-10-5.08_1x10_P5.08mm_Horizontal @@ -13537,6 +14724,18 @@ TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-8-5.08_1x08_P5.08mm_Horizon TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-8_1x08_P5.00mm_Horizontal TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-9-5.08_1x09_P5.08mm_Horizontal TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-9_1x09_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-10-3.81_1x10_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-11-3.81_1x11_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-12-3.81_1x12_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-13-3.81_1x13_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-2-3.81_1x02_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-3-3.81_1x03_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-4-3.81_1x04_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-5-3.81_1x05_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-6-3.81_1x06_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-7-3.81_1x07_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-8-3.81_1x08_P3.81mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1-9-3.81_1x09_P3.81mm_Horizontal TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-10-5.08_1x10_P5.08mm_Horizontal TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-11-5.08_1x11_P5.08mm_Horizontal TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-12-5.08_1x12_P5.08mm_Horizontal @@ -13880,6 +15079,29 @@ TerminalBlock_WAGO:TerminalBlock_WAGO_236-614_1x14_P10.00mm_45Degree TerminalBlock_WAGO:TerminalBlock_WAGO_236-615_1x15_P10.00mm_45Degree TerminalBlock_WAGO:TerminalBlock_WAGO_236-616_1x16_P10.00mm_45Degree TerminalBlock_WAGO:TerminalBlock_WAGO_236-624_1x24_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1102_1x02_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1103_1x03_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1104_1x04_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1105_1x05_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1106_1x06_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1108_1x08_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1109_1x09_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1110_1x10_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1111_1x11_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-1112_1x12_P3.50mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3102_1x02_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3103_1x03_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3104_1x04_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3105_1x05_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3106_1x06_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3107_1x07_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3108_1x08_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3109_1x09_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3110_1x10_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3111_1x11_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3112_1x12_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3114_1x14_P3.50mm_Vertical +TerminalBlock_WAGO:TerminalBlock_WAGO_2601-3124_1x24_P3.50mm_Vertical TerminalBlock_WAGO:TerminalBlock_WAGO_804-101_1x01_P5.00mm_45Degree TerminalBlock_WAGO:TerminalBlock_WAGO_804-102_1x02_P5.00mm_45Degree TerminalBlock_WAGO:TerminalBlock_WAGO_804-103_1x03_P5.00mm_45Degree @@ -13911,6 +15133,7 @@ TerminalBlock_WAGO:TerminalBlock_WAGO_804-311_1x11_P7.50mm_45Degree TerminalBlock_WAGO:TerminalBlock_WAGO_804-312_1x12_P7.50mm_45Degree TerminalBlock_WAGO:TerminalBlock_WAGO_804-316_1x16_P7.50mm_45Degree TerminalBlock_WAGO:TerminalBlock_WAGO_804-324_1x24_P7.50mm_45Degree +TerminalBlock_Wuerth:Wuerth_691311400102_P7.62mm TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650073_THR TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650074_THR TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650094_THR @@ -13999,10 +15222,12 @@ Transformer_SMD:Transformer_ED8_4-Lead_10.5x8mm_P5mm Transformer_SMD:Transformer_Ethernet_Bel_S558-5999-T7-F Transformer_SMD:Transformer_Ethernet_Bourns_PT61017PEL Transformer_SMD:Transformer_Ethernet_Bourns_PT61020EL +Transformer_SMD:Transformer_Ethernet_Bourns_SM13126PEL +Transformer_SMD:Transformer_Ethernet_CNDtek_H1102N +Transformer_SMD:Transformer_Ethernet_HALO_TG111-MSC13 Transformer_SMD:Transformer_Ethernet_Halo_N2_SO-16_7.11x12.7mm Transformer_SMD:Transformer_Ethernet_Halo_N5_SO-16_7.11x12.7mm Transformer_SMD:Transformer_Ethernet_Halo_N6_SO-16_7.11x14.73mm -Transformer_SMD:Transformer_Ethernet_HALO_TG111-MSC13 Transformer_SMD:Transformer_Ethernet_Wuerth_749013011A Transformer_SMD:Transformer_Ethernet_YDS_30F-51NL_SO-24_7.1x15.1mm Transformer_SMD:Transformer_MACOM_SM-22 diff --git a/public/kicad/symbols.txt b/public/kicad/symbols.txt index 1cee1692..46a15ee1 100644 --- a/public/kicad/symbols.txt +++ b/public/kicad/symbols.txt @@ -1,6 +1,5 @@ -# This file contains all the KiCad symbols available in the official library -# Generated by symbols.sh -# on Sun Feb 16 21:42:01 CET 2025 +# Generated on Mon May 25 06:42:27 UTC 2026 +# This file contains all symbols available in the offical KiCAD library 4xxx:14528 4xxx:14529 4xxx:14538 @@ -347,6 +346,7 @@ 74xx:74AHCT374 74xx:74AHCT541 74xx:74AHCT595 +74xx:74AHCV17APW 74xx:74ALVC164245 74xx:74CB3Q16210DGG 74xx:74CB3Q16210DGV @@ -583,6 +583,8 @@ 74xx:74LS93 74xx:74LS95 74xx:74LV14 +74xx:74LV1T08GV +74xx:74LV1T08GW 74xx:74LV8154 74xx:74LVC125 74xx:74VHC9164FT @@ -784,6 +786,7 @@ 74xx_IEEE:74LS688 74xx_IEEE:74LS689 74xx_IEEE:74S140 +Amplifier_Audio:CXA1034P Amplifier_Audio:IR4301 Amplifier_Audio:IR4302 Amplifier_Audio:IR4311 @@ -838,6 +841,7 @@ Amplifier_Audio:PAM8302AAS Amplifier_Audio:PAM8302AAY Amplifier_Audio:PAM8403D Amplifier_Audio:PAM8406D +Amplifier_Audio:PAM8908 Amplifier_Audio:SSM2017P Amplifier_Audio:SSM2018 Amplifier_Audio:SSM2120 @@ -857,6 +861,7 @@ Amplifier_Audio:Si8241BB Amplifier_Audio:Si8241CB Amplifier_Audio:Si8244BB Amplifier_Audio:Si8244CB +Amplifier_Audio:TAS5805MPWP Amplifier_Audio:TAS5825MRHB Amplifier_Audio:TDA1308 Amplifier_Audio:TDA2003 @@ -894,6 +899,7 @@ Amplifier_Buffer:BUF634AxD Amplifier_Buffer:BUF634AxDDA Amplifier_Buffer:BUF634AxDRB Amplifier_Buffer:BUF634U +Amplifier_Buffer:BUF802 Amplifier_Buffer:EL2001CN Amplifier_Buffer:LH0002H Amplifier_Buffer:LM6321H @@ -985,6 +991,11 @@ Amplifier_Current:INA241B5xD Amplifier_Current:INA241B5xDDF Amplifier_Current:INA241B5xDGK Amplifier_Current:INA253 +Amplifier_Current:INA280A1xDCK +Amplifier_Current:INA280A2xDCK +Amplifier_Current:INA280A3xDCK +Amplifier_Current:INA280A4xDCK +Amplifier_Current:INA280A5xDCK Amplifier_Current:INA281A1 Amplifier_Current:INA281A2 Amplifier_Current:INA281A3 @@ -995,6 +1006,11 @@ Amplifier_Current:INA283 Amplifier_Current:INA284 Amplifier_Current:INA285 Amplifier_Current:INA286 +Amplifier_Current:INA290A1xDCK +Amplifier_Current:INA290A2xDCK +Amplifier_Current:INA290A3xDCK +Amplifier_Current:INA290A4xDCK +Amplifier_Current:INA290A5xDCK Amplifier_Current:INA293A1 Amplifier_Current:INA293A2 Amplifier_Current:INA293A3 @@ -1041,16 +1057,13 @@ Amplifier_Current:ZXCT1109 Amplifier_Current:ZXCT1110 Amplifier_Difference:AD628 Amplifier_Difference:AD8207 +Amplifier_Difference:AD8264 Amplifier_Difference:AD8276 Amplifier_Difference:AD8475ACPZ Amplifier_Difference:AD8475xRMZ Amplifier_Difference:ADA4938-1 Amplifier_Difference:ADA4940-1xCP Amplifier_Difference:ADA4940-2 -Amplifier_Difference:AMC1100DWV -Amplifier_Difference:AMC1200BDWV -Amplifier_Difference:AMC1300BDWV -Amplifier_Difference:AMC1300DWV Amplifier_Difference:INA105KP Amplifier_Difference:INA105KU Amplifier_Difference:LM733CH @@ -1095,6 +1108,9 @@ Amplifier_Instrumentation:INA326 Amplifier_Instrumentation:INA327 Amplifier_Instrumentation:INA333xxDGK Amplifier_Instrumentation:INA333xxDRG +Amplifier_Instrumentation:INA821xD +Amplifier_Instrumentation:INA821xDGK +Amplifier_Instrumentation:INA821xDRG Amplifier_Instrumentation:INA849D Amplifier_Instrumentation:INA849DGK Amplifier_Instrumentation:LTC1100xN8 @@ -1139,6 +1155,12 @@ Amplifier_Operational:ADA4622-2xCP Amplifier_Operational:ADA4622-4xCP Amplifier_Operational:ADA4625-1ARDZ Amplifier_Operational:ADA4625-2ARDZ +Amplifier_Operational:ADA4691-2ACBZ +Amplifier_Operational:ADA4691-2ACPZ +Amplifier_Operational:ADA4691-4ACPZ +Amplifier_Operational:ADA4692-2ACPZ +Amplifier_Operational:ADA4692-2ARZ +Amplifier_Operational:ADA4692-4ARUZ Amplifier_Operational:ADA4807-1 Amplifier_Operational:ADA4807-2ACP Amplifier_Operational:ADA4807-2ARM @@ -1150,6 +1172,7 @@ Amplifier_Operational:ADA4841-1YRJ Amplifier_Operational:ADA4870ARRZ Amplifier_Operational:ADA4898-1YRDZ Amplifier_Operational:ADA4898-2 +Amplifier_Operational:ADL5303ACP Amplifier_Operational:AS13704 Amplifier_Operational:CA3080 Amplifier_Operational:CA3080A @@ -1182,6 +1205,7 @@ Amplifier_Operational:LM318N Amplifier_Operational:LM321 Amplifier_Operational:LM324 Amplifier_Operational:LM324A +Amplifier_Operational:LM324Q Amplifier_Operational:LM358 Amplifier_Operational:LM358_DFN Amplifier_Operational:LM4250 @@ -1218,6 +1242,7 @@ Amplifier_Operational:LOG114AxRGV Amplifier_Operational:LPV811DBV Amplifier_Operational:LPV812DGK Amplifier_Operational:LT1012 +Amplifier_Operational:LT1014xSW Amplifier_Operational:LT1363 Amplifier_Operational:LT1492 Amplifier_Operational:LT1493 @@ -1232,6 +1257,7 @@ Amplifier_Operational:LTC6081xDD Amplifier_Operational:LTC6081xMS8 Amplifier_Operational:LTC6082xDHC Amplifier_Operational:LTC6082xGN +Amplifier_Operational:LTC6226xDC Amplifier_Operational:LTC6228xDC Amplifier_Operational:LTC6228xS6 Amplifier_Operational:LTC6228xS8 @@ -1337,6 +1363,10 @@ Amplifier_Operational:OPA1641 Amplifier_Operational:OPA1655D Amplifier_Operational:OPA1655DBV Amplifier_Operational:OPA1656ID +Amplifier_Operational:OPA1662D +Amplifier_Operational:OPA1662DGK +Amplifier_Operational:OPA1664D +Amplifier_Operational:OPA1664PW Amplifier_Operational:OPA1678 Amplifier_Operational:OPA1679 Amplifier_Operational:OPA1692xD @@ -1437,6 +1467,7 @@ Amplifier_Operational:RC4580 Amplifier_Operational:SA5532 Amplifier_Operational:SA5534 Amplifier_Operational:THS3491xDDA +Amplifier_Operational:THS4226DGQ Amplifier_Operational:THS4631D Amplifier_Operational:THS4631DDA Amplifier_Operational:THS4631DGN @@ -1472,9 +1503,13 @@ Amplifier_Operational:TLV2371D Amplifier_Operational:TLV2371DBV Amplifier_Operational:TLV2371P Amplifier_Operational:TLV2372 +Amplifier_Operational:TLV3541xDBV +Amplifier_Operational:TLV365DBV Amplifier_Operational:TLV6001DCK Amplifier_Operational:TLV9001IDCK Amplifier_Operational:TLV9004xRUCR +Amplifier_Operational:TLV9054xD +Amplifier_Operational:TLV9054xPW Amplifier_Operational:TLV9061xDBV Amplifier_Operational:TLV9061xDCK Amplifier_Operational:TLV9061xDPW @@ -1529,6 +1564,7 @@ Analog:MPY634KP Analog:MPY634KU Analog:PGA112 Analog:PGA113 +Analog:PGA281AxPW Analog_ADC:AD40xxBCPZ Analog_ADC:AD40xxBRMZ Analog_ADC:AD574A @@ -1543,6 +1579,10 @@ Analog_ADC:AD7324 Analog_ADC:AD7327 Analog_ADC:AD7328 Analog_ADC:AD7329 +Analog_ADC:AD7380-4 +Analog_ADC:AD7386-4 +Analog_ADC:AD7387-4 +Analog_ADC:AD7388-4 Analog_ADC:AD7606 Analog_ADC:AD7606-4 Analog_ADC:AD7606-6 @@ -1553,6 +1593,7 @@ Analog_ADC:AD7699BCP Analog_ADC:AD7722 Analog_ADC:AD7745 Analog_ADC:AD7746 +Analog_ADC:AD7779 Analog_ADC:AD7794 Analog_ADC:AD7795 Analog_ADC:AD7819 @@ -1562,11 +1603,20 @@ Analog_ADC:AD9283 Analog_ADC:ADC0800 Analog_ADC:ADC08060 Analog_ADC:ADC081C021CIMM +Analog_ADC:ADC082S021 +Analog_ADC:ADC082S051 +Analog_ADC:ADC082S101 Analog_ADC:ADC0832 Analog_ADC:ADC101C021CIMK Analog_ADC:ADC101C021CIMM +Analog_ADC:ADC102S021 +Analog_ADC:ADC102S051 +Analog_ADC:ADC102S101 Analog_ADC:ADC1173 Analog_ADC:ADC121C021CIMM +Analog_ADC:ADC122S021 +Analog_ADC:ADC122S051 +Analog_ADC:ADC122S101 Analog_ADC:ADC1283 Analog_ADC:ADC128D818 Analog_ADC:ADS1013IDGS @@ -1585,8 +1635,11 @@ Analog_ADC:ADS1232IPW Analog_ADC:ADS1234IPW Analog_ADC:ADS1243 Analog_ADC:ADS1251 -Analog_ADC:ADS127L01IPBS +Analog_ADC:ADS127L01xPBS Analog_ADC:ADS1298xPAG +Analog_ADC:ADS1299 +Analog_ADC:ADS131M04xPW +Analog_ADC:ADS131M08xPBS Analog_ADC:ADS7029 Analog_ADC:ADS7039 Analog_ADC:ADS7040xDCU @@ -1599,6 +1652,12 @@ Analog_ADC:ADS7828 Analog_ADC:ADS7866 Analog_ADC:ADS7867 Analog_ADC:ADS7868 +Analog_ADC:ADS7886xxDBV +Analog_ADC:ADS7886xxDCK +Analog_ADC:ADS7887xDBV +Analog_ADC:ADS7887xDCK +Analog_ADC:ADS7888xDBV +Analog_ADC:ADS7888xDCK Analog_ADC:ADS8681RUM Analog_ADC:ADS8684 Analog_ADC:ADS8685RUM @@ -1609,7 +1668,6 @@ Analog_ADC:CA3300 Analog_ADC:HX711 Analog_ADC:ICL7106CPL Analog_ADC:ICL7107CPL -Analog_ADC:INA234AxYBJ Analog_ADC:LTC1406CGN Analog_ADC:LTC1406IGN Analog_ADC:LTC1594CS @@ -1792,6 +1850,7 @@ Analog_DAC:DAC8165 Analog_DAC:DAC8501E Analog_DAC:DAC8531E Analog_DAC:DAC8531IDRB +Analog_DAC:DAC8532xDGK Analog_DAC:DAC8550IxDGK Analog_DAC:DAC8551IxDGK Analog_DAC:DAC8552 @@ -1930,7 +1989,13 @@ Analog_Switch:DG9421DV Analog_Switch:DG9422DV Analog_Switch:FSA3157L6X Analog_Switch:FSA3157P6X +Analog_Switch:HEF4051BT +Analog_Switch:HEF4051BTT +Analog_Switch:HEF4052BT +Analog_Switch:HEF4052BTT Analog_Switch:HEF4066BT +Analog_Switch:HEF4067BT +Analog_Switch:HEF4067BTT Analog_Switch:HI524 Analog_Switch:MAX14662 Analog_Switch:MAX14759 @@ -1967,6 +2032,10 @@ Analog_Switch:MAX40200ANS Analog_Switch:MAX40200AUK Analog_Switch:NC7SB3157L6X Analog_Switch:NC7SB3157P6X +Analog_Switch:NMUX1308BQ +Analog_Switch:NMUX1308PW +Analog_Switch:NMUX1309BQ +Analog_Switch:NMUX1309PW Analog_Switch:NX3L4051HR Analog_Switch:NX3L4051PW Analog_Switch:SN74CBT3253 @@ -1975,8 +2044,18 @@ Analog_Switch:TMUX1101DCK Analog_Switch:TMUX1102DBV Analog_Switch:TMUX1102DCK Analog_Switch:TMUX1108PW +Analog_Switch:TMUX1108RSV +Analog_Switch:TMUX1208PW +Analog_Switch:TMUX1208RSV +Analog_Switch:TMUX131RMG Analog_Switch:TMUX154EDGS Analog_Switch:TMUX154ERSW +Analog_Switch:TMUX1574DYY +Analog_Switch:TMUX1574PW +Analog_Switch:TMUX1574RSV +Analog_Switch:TMUX4051BQB +Analog_Switch:TMUX4051DYY +Analog_Switch:TMUX4051PW Analog_Switch:TS3A24159DGS Analog_Switch:TS3A24159DRC Analog_Switch:TS3A24159YZP @@ -1987,6 +2066,8 @@ Analog_Switch:TS3A5017RSV Analog_Switch:TS3A5223RSW Analog_Switch:TS3DS10224RUK Analog_Switch:TS3L501ERUA +Analog_Switch:TS5A23157DGS +Analog_Switch:TS5A23157RSE Analog_Switch:TS5A23159DGS Analog_Switch:TS5A23159RSE Analog_Switch:TS5A3159ADBVR @@ -2002,6 +2083,8 @@ Analog_Switch:TS5A63157DBV Audio:AD1853 Audio:AD1855 Audio:AD1955 +Audio:ADAU1361 +Audio:ADAU1761 Audio:ADAU1978xBCP Audio:ADAU1979xBCP Audio:AK5392VS @@ -2037,6 +2120,8 @@ Audio:CS8416-xSZ Audio:CS8416-xZZ Audio:CS8420 Audio:DSD1794A +Audio:ES8388 +Audio:ES9028PRO Audio:ISD25120P Audio:ISD25120S Audio:ISD2560E @@ -2058,6 +2143,9 @@ Audio:PCM1754DBQ Audio:PCM1780 Audio:PCM1792A Audio:PCM1794A +Audio:PCM1860DBT +Audio:PCM1862DBT +Audio:PCM1864DBT Audio:PCM2902 Audio:PCM3060 Audio:PCM5100 @@ -2083,6 +2171,8 @@ Audio:SAD1024 Audio:SAD512 Audio:SGTL5000XNAA3 Audio:SGTL5000XNLA3 +Audio:SN76489AN +Audio:SN76494AN Audio:SPN1001 Audio:SRC4392xPFB Audio:SSI2144 @@ -2106,6 +2196,11 @@ Audio:WM8731CLSEFL Audio:WM8731CSEFL Audio:WM8731SEDS Audio:YM2149 +Audio:YM2612 +Audio:YM3438 +Auxiliary_Items:Generic_Outline +Auxiliary_Items:Jumper_Shunt +Auxiliary_Items:MountingScrew Battery_Management:ADP5063 Battery_Management:ADP5090ACP Battery_Management:ADP5091 @@ -2139,8 +2234,9 @@ Battery_Management:BQ25173DSG Battery_Management:BQ25504 Battery_Management:BQ25570 Battery_Management:BQ25601 +Battery_Management:BQ25798 Battery_Management:BQ25886RGE -Battery_Management:BQ25887 +Battery_Management:BQ25887RGE Battery_Management:BQ25895RTW Battery_Management:BQ27441-G1 Battery_Management:BQ27441DRZR-G1A @@ -2148,6 +2244,7 @@ Battery_Management:BQ27441DRZR-G1B Battery_Management:BQ27441DRZT-G1A Battery_Management:BQ27441DRZT-G1B Battery_Management:BQ27750 +Battery_Management:BQ29330DBT Battery_Management:BQ297xy Battery_Management:BQ51050BRHL Battery_Management:BQ51050BYFP @@ -2158,8 +2255,17 @@ Battery_Management:BQ76200PW Battery_Management:BQ76920PW Battery_Management:BQ76930DBT Battery_Management:BQ76940DBT +Battery_Management:BQ7695201PFBR +Battery_Management:BQ7695202PFBR +Battery_Management:BQ7695203PFBR +Battery_Management:BQ7695204PFBR +Battery_Management:BQ76952PFBR Battery_Management:BQ78350DBT Battery_Management:BQ78350DBT-R1 +Battery_Management:CN3063 +Battery_Management:CN3158 +Battery_Management:CN3163 +Battery_Management:CN3170 Battery_Management:DS2745U Battery_Management:DW01A Battery_Management:LC709203FQH-01TWG @@ -2256,6 +2362,112 @@ Battery_Management:TP4056-42-ESOP8 Battery_Management:TP4057 Buffer:CDCV304 Buffer:PI6C5946002ZH +CPLD_Altera:EP1210 +CPLD_Altera:EP1810 +CPLD_Altera:EP300 +CPLD_Altera:EP310 +CPLD_Altera:EP320 +CPLD_Altera:EP600 +CPLD_Altera:EP910 +CPLD_Altera:EPM1270F256 +CPLD_Altera:EPM1270M256 +CPLD_Altera:EPM1270T144 +CPLD_Altera:EPM2210F256 +CPLD_Altera:EPM2210F324 +CPLD_Altera:EPM240F100 +CPLD_Altera:EPM240M100 +CPLD_Altera:EPM240T100 +CPLD_Altera:EPM240ZM100 +CPLD_Altera:EPM240ZM68 +CPLD_Altera:EPM570F100 +CPLD_Altera:EPM570F256 +CPLD_Altera:EPM570M100 +CPLD_Altera:EPM570M256 +CPLD_Altera:EPM570T100 +CPLD_Altera:EPM570T144 +CPLD_Altera:EPM570ZM100 +CPLD_Altera:EPM570ZM144 +CPLD_Altera:EPM570ZM256 +CPLD_Microchip:ATF1502AS-xAx44 +CPLD_Microchip:ATF1502ASL-xAx44 +CPLD_Microchip:ATF1502ASV-xAx44 +CPLD_Microchip:ATF1504AS-xAx44 +CPLD_Microchip:ATF1504ASL-xAx44 +CPLD_Microchip:ATF1504ASV-xAx44 +CPLD_Microchip:ATF1504ASVL-xAx44 +CPLD_Microchip:ATF1508ASx-xxJx84 +CPLD_Renesas:SLG46580 +CPLD_Renesas:SLG46582 +CPLD_Renesas:SLG46583 +CPLD_Renesas:SLG46826G +CPLD_Renesas:SLG47011V +CPLD_Xilinx:XC7336 +CPLD_Xilinx:XC95108PC84 +CPLD_Xilinx:XC95108PQ100 +CPLD_Xilinx:XC95144PQ100 +CPLD_Xilinx:XC95144XL-TQ100 +CPLD_Xilinx:XC95144XL-TQ144 +CPLD_Xilinx:XC9536XL-xxPCx44 +CPLD_Xilinx:XC9536XL-xxVQx44 +CPLD_Xilinx:XC9536XL-xxVQx64 +CPLD_Xilinx:XC9572XL-xxCSx48 +CPLD_Xilinx:XC9572XL-xxPCx44 +CPLD_Xilinx:XC9572XL-xxTQx100 +CPLD_Xilinx:XC9572XL-xxVQx44 +CPLD_Xilinx:XC9572XL-xxVQx64 +CPLD_Xilinx:XCR3064-VQ100 +CPLD_Xilinx:XCR3064-VQ44 +CPLD_Xilinx:XCR3064XL-VQ100 +CPLD_Xilinx:XCR3064XL-VQ44 +CPLD_Xilinx:XCR3128-VQ100 +CPLD_Xilinx:XCR3128XL-VQ100 +CPLD_Xilinx:XCR3256-TQ144 +CPLD_Xilinx:XCR3256XL-TQ144 +CPU:CDP1802ACE +CPU:CDP1802ACEX +CPU:CDP1802BCE +CPU:CDP1802BCEX +CPU:P4080-BGA1295 +CPU:TMPZ84C00A +CPU:Z084000xP +CPU:Z84C00xxP +CPU_NXP_68000:MC68000FN +CPU_NXP_68000:MC68000P +CPU_NXP_68000:MC68008FN +CPU_NXP_68000:MC68008P +CPU_NXP_68000:MC68010P +CPU_NXP_68000:MC68332 +CPU_NXP_6800:MC6800 +CPU_NXP_6800:MC6802 +CPU_NXP_6800:MC6809 +CPU_NXP_6800:MC6809E +CPU_NXP_6800:MC68A00 +CPU_NXP_6800:MC68A02 +CPU_NXP_6800:MC68A09 +CPU_NXP_6800:MC68A09E +CPU_NXP_6800:MC68B00 +CPU_NXP_6800:MC68B02 +CPU_NXP_6800:MC68B09 +CPU_NXP_6800:MC68B09E +CPU_NXP_IMX:MCIMX6D4AVT +CPU_NXP_IMX:MCIMX6D5EYM +CPU_NXP_IMX:MCIMX6D6AVT +CPU_NXP_IMX:MCIMX6D7CVT +CPU_NXP_IMX:MCIMX6DP4AVT +CPU_NXP_IMX:MCIMX6DP5EVT +CPU_NXP_IMX:MCIMX6DP5EYM +CPU_NXP_IMX:MCIMX6DP6AVT +CPU_NXP_IMX:MCIMX6DP7CVT +CPU_NXP_IMX:MCIMX6Q4AVT +CPU_NXP_IMX:MCIMX6Q5EYM +CPU_NXP_IMX:MCIMX6Q6AVT +CPU_NXP_IMX:MCIMX6Q7CVT +CPU_NXP_IMX:MCIMX6QP4AVT +CPU_NXP_IMX:MCIMX6QP5EVT +CPU_NXP_IMX:MCIMX6QP5EYM +CPU_NXP_IMX:MCIMX6QP6AVT +CPU_NXP_IMX:MCIMX6QP7CVT +CPU_PowerPC:MPC8641D Comparator:AD8561 Comparator:ADCMP350 Comparator:ADCMP354 @@ -2556,6 +2768,9 @@ Connector:DIN41612_02x32_AB Connector:DIN41612_02x32_AC Connector:DIN41612_02x32_AE Connector:DIN41612_02x32_ZB +Connector:DIN41612_03x32_C_Split +Connector:DP_Sink +Connector:DP_Source Connector:DVI-D_Dual_Link Connector:DVI-I_Dual_Link Connector:ExpressCard @@ -2694,6 +2909,7 @@ Connector:TestPoint_Alt Connector:TestPoint_Flag Connector:TestPoint_Probe Connector:TestPoint_Small +Connector:TestPoint_Square Connector:UEXT_Host Connector:UEXT_Slave Connector:USB3_A @@ -4109,6 +4325,14 @@ Converter_ACDC:TMF30105 Converter_ACDC:TMF30112 Converter_ACDC:TMF30115 Converter_ACDC:TMF30124 +Converter_ACDC:TMG07105 +Converter_ACDC:TMG07112 +Converter_ACDC:TMG07115 +Converter_ACDC:TMG07124 +Converter_ACDC:TMG15105 +Converter_ACDC:TMG15112 +Converter_ACDC:TMG15115 +Converter_ACDC:TMG15124 Converter_ACDC:TMLM04103 Converter_ACDC:TMLM04105 Converter_ACDC:TMLM04109 @@ -4132,6 +4356,18 @@ Converter_ACDC:TMLM20105 Converter_ACDC:TMLM20112 Converter_ACDC:TMLM20115 Converter_ACDC:TMLM20124 +Converter_ACDC:TMPW10-105 +Converter_ACDC:TMPW10-112 +Converter_ACDC:TMPW10-115 +Converter_ACDC:TMPW10-124 +Converter_ACDC:TMPW25-105 +Converter_ACDC:TMPW25-112 +Converter_ACDC:TMPW25-115 +Converter_ACDC:TMPW25-124 +Converter_ACDC:TMPW5-103 +Converter_ACDC:TMPW5-105 +Converter_ACDC:TMPW5-112 +Converter_ACDC:TMPW5-124 Converter_ACDC:TPP-15-103-D Converter_ACDC:TPP-15-105-D Converter_ACDC:TPP-15-109-D @@ -4179,7 +4415,7 @@ Converter_DCDC:ATA00F36S-L Converter_DCDC:ATA00H18S-L Converter_DCDC:ATA00H36S-L Converter_DCDC:Ag9905LP -Converter_DCDC:BD8314NUV +Converter_DCDC:C11204-01 Converter_DCDC:IA0305D Converter_DCDC:IA0305S Converter_DCDC:IA0503D @@ -4557,7 +4793,28 @@ Converter_DCDC:JTE0624D05 Converter_DCDC:JTE0624D12 Converter_DCDC:JTE0624D15 Converter_DCDC:JTE0624D24 +Converter_DCDC:LMZ13608 +Converter_DCDC:LMZ22003TZ +Converter_DCDC:LMZ22005TZ +Converter_DCDC:LMZ23603TZ +Converter_DCDC:LMZ23605TZ +Converter_DCDC:LMZM23600 +Converter_DCDC:LMZM23600V3 +Converter_DCDC:LMZM23600V5 +Converter_DCDC:LMZM23601 +Converter_DCDC:LMZM23601V3 +Converter_DCDC:LMZM23601V5 Converter_DCDC:LT1026 +Converter_DCDC:LTM4626 +Converter_DCDC:LTM4637xV +Converter_DCDC:LTM4637xY +Converter_DCDC:LTM4638 +Converter_DCDC:LTM4657 +Converter_DCDC:LTM4668 +Converter_DCDC:LTM4668A +Converter_DCDC:LTM4671 +Converter_DCDC:LTM8049 +Converter_DCDC:LTM8063 Converter_DCDC:MEE1S0303SC Converter_DCDC:MEE1S0305SC Converter_DCDC:MEE1S0309SC @@ -4614,6 +4871,8 @@ Converter_DCDC:MGJ2D242005SC Converter_DCDC:MGJ3T05150505MC Converter_DCDC:MGJ3T12150505MC Converter_DCDC:MGJ3T24150505MC +Converter_DCDC:MUN12AD01-SH +Converter_DCDC:MUN12AD03-SH Converter_DCDC:MYRGPxx0060x21RC Converter_DCDC:MYRGPxx0060x21RF Converter_DCDC:NCS1S1203SC @@ -4636,6 +4895,48 @@ Converter_DCDC:PTN78020H_EUK-7 Converter_DCDC:PTN78020W_EUK-7 Converter_DCDC:PTN78060H_EUW-7 Converter_DCDC:PTN78060W_EUW-7 +Converter_DCDC:R-781.5-0.5 +Converter_DCDC:R-781.8-0.5 +Converter_DCDC:R-781.8-1.0 +Converter_DCDC:R-7812-0.5 +Converter_DCDC:R-7815-0.5 +Converter_DCDC:R-782.5-0.5 +Converter_DCDC:R-782.5-1.0 +Converter_DCDC:R-783.3-0.5 +Converter_DCDC:R-783.3-1.0 +Converter_DCDC:R-785.0-0.5 +Converter_DCDC:R-785.0-1.0 +Converter_DCDC:R-786.5-0.5 +Converter_DCDC:R-78B1.2-2.0 +Converter_DCDC:R-78B1.5-2.0 +Converter_DCDC:R-78B1.8-2.0 +Converter_DCDC:R-78B12-2.0 +Converter_DCDC:R-78B15-2.0 +Converter_DCDC:R-78B2.5-2.0 +Converter_DCDC:R-78B3.3-2.0 +Converter_DCDC:R-78B5.0-2.0 +Converter_DCDC:R-78B9.0-2.0 +Converter_DCDC:R-78C1.8-1.0 +Converter_DCDC:R-78C12-1.0 +Converter_DCDC:R-78C15-1.0 +Converter_DCDC:R-78C3.3-1.0 +Converter_DCDC:R-78C5.0-1.0 +Converter_DCDC:R-78C9.0-1.0 +Converter_DCDC:R-78E12-0.5 +Converter_DCDC:R-78E15-0.5 +Converter_DCDC:R-78E3.3-0.5 +Converter_DCDC:R-78E3.3-1.0 +Converter_DCDC:R-78E5.0-0.5 +Converter_DCDC:R-78E5.0-1.0 +Converter_DCDC:R-78E9.0-0.5 +Converter_DCDC:R-78HB12-0.5 +Converter_DCDC:R-78HB15-0.5 +Converter_DCDC:R-78HB24-0.3 +Converter_DCDC:R-78HB3.3-0.5 +Converter_DCDC:R-78HB5.0-0.5 +Converter_DCDC:R-78HB6.5-0.5 +Converter_DCDC:R-78HB9.0-0.5 +Converter_DCDC:R-78S3.3-0.1 Converter_DCDC:RPA60-2405SFW Converter_DCDC:RPA60-2412SFW Converter_DCDC:RPA60-2415SFW @@ -4653,21 +4954,36 @@ Converter_DCDC:RPMH15-1.5 Converter_DCDC:RPMH24-1.5 Converter_DCDC:RPMH3.3-1.5 Converter_DCDC:RPMH5.0-1.5 +Converter_DCDC:TBA1-0310 +Converter_DCDC:TBA1-0311 +Converter_DCDC:TBA1-0510 +Converter_DCDC:TBA1-0511 Converter_DCDC:TBA1-0511E +Converter_DCDC:TBA1-0512 Converter_DCDC:TBA1-0512E +Converter_DCDC:TBA1-0513 Converter_DCDC:TBA1-0513E +Converter_DCDC:TBA1-0519 Converter_DCDC:TBA1-0521E Converter_DCDC:TBA1-0522E Converter_DCDC:TBA1-0523E +Converter_DCDC:TBA1-1211 Converter_DCDC:TBA1-1211E +Converter_DCDC:TBA1-1212 Converter_DCDC:TBA1-1212E +Converter_DCDC:TBA1-1213 Converter_DCDC:TBA1-1213E +Converter_DCDC:TBA1-1219 Converter_DCDC:TBA1-1221E Converter_DCDC:TBA1-1222E Converter_DCDC:TBA1-1223E +Converter_DCDC:TBA1-2411 Converter_DCDC:TBA1-2411E +Converter_DCDC:TBA1-2412 Converter_DCDC:TBA1-2412E +Converter_DCDC:TBA1-2413 Converter_DCDC:TBA1-2413E +Converter_DCDC:TBA1-2419 Converter_DCDC:TBA1-2421E Converter_DCDC:TBA1-2422E Converter_DCDC:TBA1-2423E @@ -4710,18 +5026,27 @@ Converter_DCDC:TEC2-1212WI Converter_DCDC:TEC2-1213WI Converter_DCDC:TEC2-1215WI Converter_DCDC:TEC2-1219WI +Converter_DCDC:TEC2-1221WI +Converter_DCDC:TEC2-1222WI +Converter_DCDC:TEC2-1223WI Converter_DCDC:TEC2-2410WI Converter_DCDC:TEC2-2411WI Converter_DCDC:TEC2-2412WI Converter_DCDC:TEC2-2413WI Converter_DCDC:TEC2-2415WI Converter_DCDC:TEC2-2419WI +Converter_DCDC:TEC2-2421WI +Converter_DCDC:TEC2-2422WI +Converter_DCDC:TEC2-2423WI Converter_DCDC:TEC2-4810WI Converter_DCDC:TEC2-4811WI Converter_DCDC:TEC2-4812WI Converter_DCDC:TEC2-4813WI Converter_DCDC:TEC2-4815WI Converter_DCDC:TEC2-4819WI +Converter_DCDC:TEC2-4821WI +Converter_DCDC:TEC2-4822WI +Converter_DCDC:TEC2-4823WI Converter_DCDC:TEC3-2410UI Converter_DCDC:TEC3-2411UI Converter_DCDC:TEC3-2412UI @@ -4729,6 +5054,15 @@ Converter_DCDC:TEC3-2413UI Converter_DCDC:TEC3-2421UI Converter_DCDC:TEC3-2422UI Converter_DCDC:TEC3-2423UI +Converter_DCDC:TEC6-2410UI +Converter_DCDC:TEC6-2411UI +Converter_DCDC:TEC6-2412UI +Converter_DCDC:TEC6-2413UI +Converter_DCDC:TEC6-2415UI +Converter_DCDC:TEC6-2419UI +Converter_DCDC:TEC6-2421UI +Converter_DCDC:TEC6-2422UI +Converter_DCDC:TEC6-2423UI Converter_DCDC:TEL12-1211 Converter_DCDC:TEL12-1212 Converter_DCDC:TEL12-1213 @@ -4810,6 +5144,38 @@ Converter_DCDC:TEN6-11015WIRH Converter_DCDC:TEN6-11021WIRH Converter_DCDC:TEN6-11022WIRH Converter_DCDC:TEN6-11023WIRH +Converter_DCDC:TEP150-7211UIR +Converter_DCDC:TEP150-7212UIR +Converter_DCDC:TEP150-7213UIR +Converter_DCDC:TEP150-7215UIR +Converter_DCDC:TEP150-7218UIR +Converter_DCDC:TEP200-7211UIR +Converter_DCDC:TEP200-7212UIR +Converter_DCDC:TEP200-7213UIR +Converter_DCDC:TEP200-7215UIR +Converter_DCDC:TEP200-7218UIR +Converter_DCDC:TES1-0510 +Converter_DCDC:TES1-0511 +Converter_DCDC:TES1-0512 +Converter_DCDC:TES1-0513 +Converter_DCDC:TES1-0519 +Converter_DCDC:TES1-0521 +Converter_DCDC:TES1-0522 +Converter_DCDC:TES1-0523 +Converter_DCDC:TES1-1211 +Converter_DCDC:TES1-1212 +Converter_DCDC:TES1-1213 +Converter_DCDC:TES1-1219 +Converter_DCDC:TES1-1221 +Converter_DCDC:TES1-1222 +Converter_DCDC:TES1-1223 +Converter_DCDC:TES1-2411 +Converter_DCDC:TES1-2412 +Converter_DCDC:TES1-2413 +Converter_DCDC:TES1-2419 +Converter_DCDC:TES1-2421 +Converter_DCDC:TES1-2422 +Converter_DCDC:TES1-2423 Converter_DCDC:THB10-1211 Converter_DCDC:THB10-1212 Converter_DCDC:THB10-1222 @@ -4822,6 +5188,48 @@ Converter_DCDC:THB10-4811 Converter_DCDC:THB10-4812 Converter_DCDC:THB10-4822 Converter_DCDC:THB10-4823 +Converter_DCDC:THL30-2410WI +Converter_DCDC:THL30-2411WI +Converter_DCDC:THL30-2412WI +Converter_DCDC:THL30-2413WI +Converter_DCDC:THL30-2415WI +Converter_DCDC:THL30-2422WI +Converter_DCDC:THL30-2423WI +Converter_DCDC:THL30-4810WI +Converter_DCDC:THL30-4811WI +Converter_DCDC:THL30-4812WI +Converter_DCDC:THL30-4813WI +Converter_DCDC:THL30-4815WI +Converter_DCDC:THL30-4822WI +Converter_DCDC:THL30-4823WI +Converter_DCDC:THL40-2411WI +Converter_DCDC:THL40-2412WI +Converter_DCDC:THL40-2413WI +Converter_DCDC:THL40-2415WI +Converter_DCDC:THL40-2422WI +Converter_DCDC:THL40-2423WI +Converter_DCDC:THL40-4811WI +Converter_DCDC:THL40-4812WI +Converter_DCDC:THL40-4813WI +Converter_DCDC:THL40-4815WI +Converter_DCDC:THL40-4822WI +Converter_DCDC:THL40-4823WI +Converter_DCDC:THN10-3610UIR +Converter_DCDC:THN10-3611UIR +Converter_DCDC:THN10-3612UIR +Converter_DCDC:THN10-3613UIR +Converter_DCDC:THN10-3615UIR +Converter_DCDC:THN10-3621UIR +Converter_DCDC:THN10-3622UIR +Converter_DCDC:THN10-3623UIR +Converter_DCDC:THN10-7210UIR +Converter_DCDC:THN10-7211UIR +Converter_DCDC:THN10-7212UIR +Converter_DCDC:THN10-7213UIR +Converter_DCDC:THN10-7215UIR +Converter_DCDC:THN10-7221UIR +Converter_DCDC:THN10-7222UIR +Converter_DCDC:THN10-7223UIR Converter_DCDC:THR40-7211WI Converter_DCDC:THR40-7212WI Converter_DCDC:THR40-7213WI @@ -4871,23 +5279,65 @@ Converter_DCDC:TME-2415S Converter_DCDC:TMR-0510 Converter_DCDC:TMR-0511 Converter_DCDC:TMR-0512 +Converter_DCDC:TMR-0521 +Converter_DCDC:TMR-0522 +Converter_DCDC:TMR-0523 Converter_DCDC:TMR-1210 Converter_DCDC:TMR-1211 Converter_DCDC:TMR-1212 +Converter_DCDC:TMR-1221 +Converter_DCDC:TMR-1222 +Converter_DCDC:TMR-1223 Converter_DCDC:TMR-2410 Converter_DCDC:TMR-2411 Converter_DCDC:TMR-2412 +Converter_DCDC:TMR-2421 +Converter_DCDC:TMR-2422 +Converter_DCDC:TMR-2423 Converter_DCDC:TMR-4810 Converter_DCDC:TMR-4811 Converter_DCDC:TMR-4812 +Converter_DCDC:TMR-4821 +Converter_DCDC:TMR-4822 +Converter_DCDC:TMR-4823 +Converter_DCDC:TMR10-2410WIR +Converter_DCDC:TMR10-2411WIR +Converter_DCDC:TMR10-2412WIR +Converter_DCDC:TMR10-2413WIR +Converter_DCDC:TMR10-2415WIR +Converter_DCDC:TMR10-2421WIR +Converter_DCDC:TMR10-2422WIR +Converter_DCDC:TMR10-2423WIR +Converter_DCDC:TMR10-4810WIR +Converter_DCDC:TMR10-4811WIR +Converter_DCDC:TMR10-4812WIR +Converter_DCDC:TMR10-4813WIR +Converter_DCDC:TMR10-4815WIR +Converter_DCDC:TMR10-4821WIR +Converter_DCDC:TMR10-4822WIR +Converter_DCDC:TMR10-4823WIR +Converter_DCDC:TMR10-7210WIR +Converter_DCDC:TMR10-7211WIR +Converter_DCDC:TMR10-7212WIR +Converter_DCDC:TMR10-7213WIR +Converter_DCDC:TMR10-7215WIR +Converter_DCDC:TMR10-7221WIR +Converter_DCDC:TMR10-7222WIR +Converter_DCDC:TMR10-7223WIR Converter_DCDC:TMR2-2410WI Converter_DCDC:TMR2-2411WI Converter_DCDC:TMR2-2412WI Converter_DCDC:TMR2-2413WI +Converter_DCDC:TMR2-2421WI +Converter_DCDC:TMR2-2422WI +Converter_DCDC:TMR2-2423WI Converter_DCDC:TMR2-4810WI Converter_DCDC:TMR2-4811WI Converter_DCDC:TMR2-4812WI Converter_DCDC:TMR2-4813WI +Converter_DCDC:TMR2-4821WI +Converter_DCDC:TMR2-4822WI +Converter_DCDC:TMR2-4823WI Converter_DCDC:TMR4-2411WI Converter_DCDC:TMR4-2412WI Converter_DCDC:TMR4-2413WI @@ -4909,10 +5359,32 @@ Converter_DCDC:TMU3-1213 Converter_DCDC:TMU3-2411 Converter_DCDC:TMU3-2412 Converter_DCDC:TMU3-2413 +Converter_DCDC:TMV0505D +Converter_DCDC:TMV0505S +Converter_DCDC:TMV0509S +Converter_DCDC:TMV0512D +Converter_DCDC:TMV0512S +Converter_DCDC:TMV0515D +Converter_DCDC:TMV0515S +Converter_DCDC:TMV1205D +Converter_DCDC:TMV1205S +Converter_DCDC:TMV1212D +Converter_DCDC:TMV1212S +Converter_DCDC:TMV1215D +Converter_DCDC:TMV1215S +Converter_DCDC:TMV2405D +Converter_DCDC:TMV2405S +Converter_DCDC:TMV2412D +Converter_DCDC:TMV2412S +Converter_DCDC:TMV2415D +Converter_DCDC:TMV2415S Converter_DCDC:TPS43060RTE Converter_DCDC:TPS54240DGQ Converter_DCDC:TPS54240DRC Converter_DCDC:TPS61022 +Converter_DCDC:TPS82130 +Converter_DCDC:TPS82140 +Converter_DCDC:TPS82150 Converter_DCDC:TPSM53602RDA Converter_DCDC:TPSM53603RDA Converter_DCDC:TPSM53604RDA @@ -4929,107 +5401,219 @@ Converter_DCDC:TRA3-2412 Converter_DCDC:TRA3-2413 Converter_DCDC:TRA3-2419 Converter_DCDC:TRI1-0511 +Converter_DCDC:TRI1-0511SM Converter_DCDC:TRI1-0512 +Converter_DCDC:TRI1-0512SM Converter_DCDC:TRI1-0513 +Converter_DCDC:TRI1-0513SM +Converter_DCDC:TRI1-0522SM +Converter_DCDC:TRI1-0523SM Converter_DCDC:TRI1-1211 +Converter_DCDC:TRI1-1211SM Converter_DCDC:TRI1-1212 +Converter_DCDC:TRI1-1212SM Converter_DCDC:TRI1-1213 +Converter_DCDC:TRI1-1213SM +Converter_DCDC:TRI1-1222SM +Converter_DCDC:TRI1-1223SM Converter_DCDC:TRI1-2411 +Converter_DCDC:TRI1-2411SM Converter_DCDC:TRI1-2412 +Converter_DCDC:TRI1-2412SM Converter_DCDC:TRI1-2413 -CPLD_Altera:EP1210 -CPLD_Altera:EP1810 -CPLD_Altera:EP300 -CPLD_Altera:EP310 -CPLD_Altera:EP320 -CPLD_Altera:EP600 -CPLD_Altera:EP910 -CPLD_Altera:EPM1270F256 -CPLD_Altera:EPM1270M256 -CPLD_Altera:EPM1270T144 -CPLD_Altera:EPM2210F256 -CPLD_Altera:EPM2210F324 -CPLD_Altera:EPM240F100 -CPLD_Altera:EPM240M100 -CPLD_Altera:EPM240T100 -CPLD_Altera:EPM240ZM100 -CPLD_Altera:EPM240ZM68 -CPLD_Altera:EPM570F100 -CPLD_Altera:EPM570F256 -CPLD_Altera:EPM570M100 -CPLD_Altera:EPM570M256 -CPLD_Altera:EPM570T100 -CPLD_Altera:EPM570T144 -CPLD_Altera:EPM570ZM100 -CPLD_Altera:EPM570ZM144 -CPLD_Altera:EPM570ZM256 -CPLD_Microchip:ATF1502AS-xAx44 -CPLD_Microchip:ATF1502ASL-xAx44 -CPLD_Microchip:ATF1502ASV-xAx44 -CPLD_Microchip:ATF1504AS-xAx44 -CPLD_Microchip:ATF1504ASL-xAx44 -CPLD_Microchip:ATF1504ASV-xAx44 -CPLD_Microchip:ATF1504ASVL-xAx44 -CPLD_Renesas:SLG46826G -CPLD_Xilinx:XC7336 -CPLD_Xilinx:XC95108PC84 -CPLD_Xilinx:XC95108PQ100 -CPLD_Xilinx:XC95144PQ100 -CPLD_Xilinx:XC95144XL-TQ100 -CPLD_Xilinx:XC95144XL-TQ144 -CPLD_Xilinx:XC9536PC44 -CPLD_Xilinx:XC9572XL-TQ100 -CPLD_Xilinx:XC9572XL-VQ64 -CPLD_Xilinx:XCR3064-VQ100 -CPLD_Xilinx:XCR3064-VQ44 -CPLD_Xilinx:XCR3064XL-VQ100 -CPLD_Xilinx:XCR3064XL-VQ44 -CPLD_Xilinx:XCR3128-VQ100 -CPLD_Xilinx:XCR3128XL-VQ100 -CPLD_Xilinx:XCR3256-TQ144 -CPLD_Xilinx:XCR3256XL-TQ144 -CPU:CDP1802ACE -CPU:CDP1802ACEX -CPU:CDP1802BCE -CPU:CDP1802BCEX -CPU:P4080-BGA1295 -CPU:Z80CPU -CPU_NXP_6800:MC6800 -CPU_NXP_6800:MC6802 -CPU_NXP_6800:MC6809 -CPU_NXP_6800:MC6809E -CPU_NXP_6800:MC68A00 -CPU_NXP_6800:MC68A02 -CPU_NXP_6800:MC68A09 -CPU_NXP_6800:MC68A09E -CPU_NXP_6800:MC68B00 -CPU_NXP_6800:MC68B02 -CPU_NXP_6800:MC68B09 -CPU_NXP_6800:MC68B09E -CPU_NXP_68000:68000D -CPU_NXP_68000:68008D -CPU_NXP_68000:68010D -CPU_NXP_68000:MC68000FN -CPU_NXP_68000:MC68332 -CPU_NXP_IMX:MCIMX6D4AVT -CPU_NXP_IMX:MCIMX6D5EYM -CPU_NXP_IMX:MCIMX6D6AVT -CPU_NXP_IMX:MCIMX6D7CVT -CPU_NXP_IMX:MCIMX6DP4AVT -CPU_NXP_IMX:MCIMX6DP5EVT -CPU_NXP_IMX:MCIMX6DP5EYM -CPU_NXP_IMX:MCIMX6DP6AVT -CPU_NXP_IMX:MCIMX6DP7CVT -CPU_NXP_IMX:MCIMX6Q4AVT -CPU_NXP_IMX:MCIMX6Q5EYM -CPU_NXP_IMX:MCIMX6Q6AVT -CPU_NXP_IMX:MCIMX6Q7CVT -CPU_NXP_IMX:MCIMX6QP4AVT -CPU_NXP_IMX:MCIMX6QP5EVT -CPU_NXP_IMX:MCIMX6QP5EYM -CPU_NXP_IMX:MCIMX6QP6AVT -CPU_NXP_IMX:MCIMX6QP7CVT -CPU_PowerPC:MPC8641D +Converter_DCDC:TRI1-2413SM +Converter_DCDC:TRI1-2422SM +Converter_DCDC:TRI1-2423SM +Converter_DCDC:TRI10-1210 +Converter_DCDC:TRI10-1211 +Converter_DCDC:TRI10-1212 +Converter_DCDC:TRI10-1213 +Converter_DCDC:TRI10-1215 +Converter_DCDC:TRI10-1222 +Converter_DCDC:TRI10-1223 +Converter_DCDC:TRI10-2410 +Converter_DCDC:TRI10-2411 +Converter_DCDC:TRI10-2412 +Converter_DCDC:TRI10-2413 +Converter_DCDC:TRI10-2415 +Converter_DCDC:TRI10-2422 +Converter_DCDC:TRI10-2423 +Converter_DCDC:TRI10-4810 +Converter_DCDC:TRI10-4811 +Converter_DCDC:TRI10-4812 +Converter_DCDC:TRI10-4813 +Converter_DCDC:TRI10-4815 +Converter_DCDC:TRI10-4822 +Converter_DCDC:TRI10-4823 +Converter_DCDC:TRI15-1211 +Converter_DCDC:TRI15-1212 +Converter_DCDC:TRI15-1213 +Converter_DCDC:TRI15-1215 +Converter_DCDC:TRI15-1222 +Converter_DCDC:TRI15-1223 +Converter_DCDC:TRI15-2411 +Converter_DCDC:TRI15-2412 +Converter_DCDC:TRI15-2413 +Converter_DCDC:TRI15-2415 +Converter_DCDC:TRI15-2422 +Converter_DCDC:TRI15-2423 +Converter_DCDC:TRI15-4811 +Converter_DCDC:TRI15-4812 +Converter_DCDC:TRI15-4813 +Converter_DCDC:TRI15-4815 +Converter_DCDC:TRI15-4822 +Converter_DCDC:TRI15-4823 +Converter_DCDC:TRI20-1211 +Converter_DCDC:TRI20-1212 +Converter_DCDC:TRI20-1213 +Converter_DCDC:TRI20-1215 +Converter_DCDC:TRI20-1222 +Converter_DCDC:TRI20-1223 +Converter_DCDC:TRI20-2411 +Converter_DCDC:TRI20-2412 +Converter_DCDC:TRI20-2413 +Converter_DCDC:TRI20-2415 +Converter_DCDC:TRI20-2422 +Converter_DCDC:TRI20-2423 +Converter_DCDC:TRI20-4811 +Converter_DCDC:TRI20-4812 +Converter_DCDC:TRI20-4813 +Converter_DCDC:TRI20-4815 +Converter_DCDC:TRI20-4822 +Converter_DCDC:TRI20-4823 +Converter_DCDC:TRN3-0510 +Converter_DCDC:TRN3-0511 +Converter_DCDC:TRN3-0512 +Converter_DCDC:TRN3-0513 +Converter_DCDC:TRN3-0515 +Converter_DCDC:TRN3-0521 +Converter_DCDC:TRN3-0522 +Converter_DCDC:TRN3-0523 +Converter_DCDC:TRN3-1210 +Converter_DCDC:TRN3-1211 +Converter_DCDC:TRN3-1212 +Converter_DCDC:TRN3-1213 +Converter_DCDC:TRN3-1215 +Converter_DCDC:TRN3-1221 +Converter_DCDC:TRN3-1222 +Converter_DCDC:TRN3-1223 +Converter_DCDC:TRN3-2410 +Converter_DCDC:TRN3-2411 +Converter_DCDC:TRN3-2412 +Converter_DCDC:TRN3-2413 +Converter_DCDC:TRN3-2415 +Converter_DCDC:TRN3-2421 +Converter_DCDC:TRN3-2422 +Converter_DCDC:TRN3-2423 +Converter_DCDC:TRN3-4810 +Converter_DCDC:TRN3-4811 +Converter_DCDC:TRN3-4812 +Converter_DCDC:TRN3-4813 +Converter_DCDC:TRN3-4815 +Converter_DCDC:TRN3-4821 +Converter_DCDC:TRN3-4822 +Converter_DCDC:TRN3-4823 +Converter_DCDC:TSR0.5-24120 +Converter_DCDC:TSR0.5-24120SM +Converter_DCDC:TSR0.5-2415 +Converter_DCDC:TSR0.5-24150 +Converter_DCDC:TSR0.5-24150SM +Converter_DCDC:TSR0.5-2415SM +Converter_DCDC:TSR0.5-2418 +Converter_DCDC:TSR0.5-2418SM +Converter_DCDC:TSR0.5-2425 +Converter_DCDC:TSR0.5-2425SM +Converter_DCDC:TSR0.5-2433 +Converter_DCDC:TSR0.5-2433SM +Converter_DCDC:TSR0.5-2450 +Converter_DCDC:TSR0.5-2450SM +Converter_DCDC:TSR0.5-2465 +Converter_DCDC:TSR0.5-2465SM +Converter_DCDC:TSR0.5-2490 +Converter_DCDC:TSR0.5-2490SM +Converter_DCDC:TSR0.6-48120WI +Converter_DCDC:TSR0.6-48150WI +Converter_DCDC:TSR0.6-48240WI +Converter_DCDC:TSR0.6-4833WI +Converter_DCDC:TSR0.6-4850WI +Converter_DCDC:TSR0.6-4865WI +Converter_DCDC:TSR0.6-4890WI +Converter_DCDC:TSR1-2433E +Converter_DCDC:TSR1-2450E +Converter_DCDC:TSR1.5-24120E +Converter_DCDC:TSR1.5-2433E +Converter_DCDC:TSR1.5-2450E +Converter_DCDC:TSR2-0512 +Converter_DCDC:TSR2-0515 +Converter_DCDC:TSR2-0518 +Converter_DCDC:TSR2-0525 +Converter_DCDC:TSR2-2412 +Converter_DCDC:TSR2-24120 +Converter_DCDC:TSR2-24120N +Converter_DCDC:TSR2-2412N +Converter_DCDC:TSR2-2415 +Converter_DCDC:TSR2-24150 +Converter_DCDC:TSR2-24150N +Converter_DCDC:TSR2-2415N +Converter_DCDC:TSR2-2418 +Converter_DCDC:TSR2-2418N +Converter_DCDC:TSR2-2425 +Converter_DCDC:TSR2-2425N +Converter_DCDC:TSR2-2433 +Converter_DCDC:TSR2-2433N +Converter_DCDC:TSR2-2450 +Converter_DCDC:TSR2-2450N +Converter_DCDC:TSR2-2465 +Converter_DCDC:TSR2-2465N +Converter_DCDC:TSR2-2490 +Converter_DCDC:TSR2-2490N +Converter_DCDC:TSR3-24120N +Converter_DCDC:TSR3-2412N +Converter_DCDC:TSR3-24150N +Converter_DCDC:TSR3-2415N +Converter_DCDC:TSR3-2418N +Converter_DCDC:TSR3-2425N +Converter_DCDC:TSR3-2433N +Converter_DCDC:TSR3-2450N +Converter_DCDC:TSR3-2465N +Converter_DCDC:TSR3-2490N +Converter_DCDC:TSR_1-2412 +Converter_DCDC:TSR_1-24120 +Converter_DCDC:TSR_1-2415 +Converter_DCDC:TSR_1-24150 +Converter_DCDC:TSR_1-2418 +Converter_DCDC:TSR_1-2425 +Converter_DCDC:TSR_1-2433 +Converter_DCDC:TSR_1-2450 +Converter_DCDC:TSR_1-2465 +Converter_DCDC:TSR_1-2490 +DSP_AnalogDevices:ADAU1450 +DSP_AnalogDevices:ADAU1451 +DSP_AnalogDevices:ADAU1452 +DSP_AnalogDevices:ADAU1701 +DSP_AnalogDevices:ADAU1702 +DSP_Freescale:DSP96002 +DSP_Microchip_DSPIC33:DSPIC33EP256MU810-xPT +DSP_Microchip_DSPIC33:DSPIC33FJ128GP204 +DSP_Microchip_DSPIC33:DSPIC33FJ128GP804 +DSP_Microchip_DSPIC33:DSPIC33FJ128MC204 +DSP_Microchip_DSPIC33:DSPIC33FJ128MC510A +DSP_Microchip_DSPIC33:DSPIC33FJ128MC710A +DSP_Microchip_DSPIC33:DSPIC33FJ128MC804 +DSP_Microchip_DSPIC33:DSPIC33FJ256MC510A +DSP_Microchip_DSPIC33:DSPIC33FJ256MC710A +DSP_Microchip_DSPIC33:DSPIC33FJ32GP304 +DSP_Microchip_DSPIC33:DSPIC33FJ32MC304 +DSP_Microchip_DSPIC33:DSPIC33FJ64GP204 +DSP_Microchip_DSPIC33:DSPIC33FJ64GP306A-IMR +DSP_Microchip_DSPIC33:DSPIC33FJ64GP804 +DSP_Microchip_DSPIC33:DSPIC33FJ64MC204 +DSP_Microchip_DSPIC33:DSPIC33FJ64MC510A +DSP_Microchip_DSPIC33:DSPIC33FJ64MC710A +DSP_Microchip_DSPIC33:DSPIC33FJ64MC802-xSP +DSP_Microchip_DSPIC33:DSPIC33FJ64MC804 +DSP_Motorola:DSP56301 +DSP_Texas:TMS320LF2406PZ Device:Ammeter_AC Device:Ammeter_DC Device:Antenna @@ -5057,9 +5641,11 @@ Device:C_Polarized_Small_US_Series_2C Device:C_Polarized_US Device:C_Polarized_US_Series_2C Device:C_Small +Device:C_Small_US Device:C_Trim Device:C_Trim_Differential Device:C_Trim_Small +Device:C_US Device:C_Variable Device:CircuitBreaker_1P Device:CircuitBreaker_1P_US @@ -5067,6 +5653,10 @@ Device:CircuitBreaker_2P Device:CircuitBreaker_2P_US Device:CircuitBreaker_3P Device:CircuitBreaker_3P_US +Device:Circulator_Left_3Port +Device:Circulator_Left_4Port +Device:Circulator_Right_3Port +Device:Circulator_Right_4Port Device:Crystal Device:Crystal_GND2 Device:Crystal_GND23 @@ -5625,6 +6215,7 @@ Diode:1SS355VM Diode:2BZX84Cxx Diode:5KPxxA Diode:5KPxxCA +Diode:ACDSV6-4448TI-G Diode:B120-E3 Diode:B130-E3 Diode:B140-E3 @@ -5854,19 +6445,19 @@ Diode:CDBA3100-HF Diode:CDBA340-HF Diode:CDBA360-HF Diode:CDBU40-HF +Diode:CDSV6-4148-G +Diode:CDSV6-4448TI-G +Diode:CMKD4448 +Diode:CMKD6001 Diode:CSD01060A Diode:CSD01060E Diode:CVFD20065A -Diode:Central_Semi_CMKD4448 -Diode:Central_Semi_CMKD6001 -Diode:Comchip_ACDSV6-4448TI-G -Diode:Comchip_CDSV6-4148-G -Diode:Comchip_CDSV6-4448TI-G Diode:DB3 Diode:DB4 Diode:DC34 Diode:DSB2810 Diode:DSB5712 +Diode:DSEI2X30-06C Diode:DZ2S030X0L Diode:DZ2S033X0L Diode:DZ2S036X0L @@ -5892,6 +6483,7 @@ Diode:ESD9B5.0ST5G Diode:ESH2PB Diode:ESH2PC Diode:ESH2PD +Diode:Fuxinsemi_PDS760 Diode:HN2D02FU Diode:IDDD04G65C6 Diode:IDDD06G65C6 @@ -5941,6 +6533,11 @@ Diode:NRVA4005T3G Diode:NRVA4006T3G Diode:NRVA4007T3G Diode:NSR0340HT1G +Diode:PESD3V3S1UL +Diode:PESD3V3U1UL +Diode:PESD5V0L1UL +Diode:PESD5V0L1ULD +Diode:PESD5V0S1UL Diode:PMEG030V030EPD Diode:PMEG030V050EPD Diode:PMEG040V030EPD @@ -6052,12 +6649,12 @@ Diode:SB150 Diode:SB160 Diode:SB5H100 Diode:SB5H90 -Diode:SD05_SOD323 +Diode:SD05C-01F Diode:SD103ATW -Diode:SD12_SOD323 -Diode:SD15_SOD323 -Diode:SD24_SOD323 -Diode:SD36_SOD323 +Diode:SD12C-01F +Diode:SD15C-01F +Diode:SD24C-01F +Diode:SD36C-01F Diode:SM15T36A Diode:SM15T36CA Diode:SM15T6V8A @@ -6281,6 +6878,7 @@ Diode:STTH2002G Diode:STTH212S Diode:STTH212U Diode:SZESD9B5.0ST5G +Diode:TSM24A Diode:Toshiba_HN1D01FU Diode:UF5400 Diode:UF5401 @@ -6482,6 +7080,7 @@ Diode_Bridge:KBU8M Diode_Bridge:MB2S Diode_Bridge:MB4S Diode_Bridge:MB6S +Diode_Bridge:MB8S Diode_Bridge:MBL104S Diode_Bridge:MBL106S Diode_Bridge:MBL108S @@ -6500,6 +7099,7 @@ Diode_Bridge:RMB2S Diode_Bridge:RMB4S Diode_Bridge:SC35VB160S-G Diode_Bridge:SC35VB80S-G +Diode_Bridge:VBO40-08NO6 Diode_Bridge:VS-KBPC1005 Diode_Bridge:VS-KBPC101 Diode_Bridge:VS-KBPC102 @@ -6579,6 +7179,7 @@ Display_Character:DE113-XX-XX Display_Character:DE114-RS-20 Display_Character:DE122-XX-XX Display_Character:DE170-XX-XX +Display_Character:DL1416 Display_Character:EA_T123X-I2C Display_Character:ELD-426SYGWA Display_Character:HDSM-441B @@ -6699,8 +7300,20 @@ Display_Graphic:EA_eDIPTFT70-A Display_Graphic:EA_eDIPTFT70-ATC Display_Graphic:EA_eDIPTFT70-ATP Display_Graphic:ERM19264 +Display_Graphic:ER_OLEDM0.91_1x-I2C Display_Graphic:NHD-C12832A1Z-FSRGB Display_Graphic:OLED-128O064D +Driver:DRV2510-Q1 +Driver:DRV2605LDGS +Driver:DRV8860 +Driver:DRV8860_PWPR +Driver:MAX1968xUI +Driver:MAX1969xUI +Driver:MAX4820xUP +Driver:MAX4821xUP +Driver:TPL9201_TSSOP +Driver:TUSS4470 +Driver_Display:315-5313A Driver_Display:82720 Driver_Display:ADS7843E Driver_Display:ADS7843E-2K5 @@ -6709,6 +7322,10 @@ Driver_Display:ADS7843IDBQRQ1 Driver_Display:AY0438X-L Driver_Display:AY0438X-P Driver_Display:CR2013-MI2120 +Driver_Display:HD44780UxxxFS +Driver_Display:HV5622PG +Driver_Display:TSC2007xPW +Driver_Display:TSC2007xYZG Driver_Display:XPT2046QF Driver_Display:XPT2046TS Driver_FET:1EDN7550B @@ -6721,9 +7338,8 @@ Driver_FET:ACPL-336J Driver_FET:ACPL-P343 Driver_FET:ACPL-W343 Driver_FET:AN34092B -Driver_FET:BSP75N -Driver_FET:BSP76 -Driver_FET:BTS4140N +Driver_FET:BDR2L00_DFN +Driver_FET:DS0026 Driver_FET:EL7202CN Driver_FET:EL7202CS Driver_FET:EL7212CN @@ -6732,6 +7348,8 @@ Driver_FET:EL7222CN Driver_FET:EL7222CS Driver_FET:FAN3111C Driver_FET:FAN3111E +Driver_FET:FAN3121xMX +Driver_FET:FAN3122xMX Driver_FET:FAN3268 Driver_FET:FAN3278 Driver_FET:FAN7371 @@ -6754,7 +7372,8 @@ Driver_FET:HIP4080A Driver_FET:HIP4081A Driver_FET:HIP4082xB Driver_FET:HIP4082xP -Driver_FET:ICL7667 +Driver_FET:ICL7667xBA +Driver_FET:ICL7667xPA Driver_FET:IR2010 Driver_FET:IR2010S Driver_FET:IR2011 @@ -6861,6 +7480,7 @@ Driver_FET:L6491 Driver_FET:LF2190N Driver_FET:LM2105D Driver_FET:LM2105DSG +Driver_FET:LM5106SD Driver_FET:LM5109AMA Driver_FET:LM5109ASD Driver_FET:LM5109BMA @@ -6879,8 +7499,10 @@ Driver_FET:MAX15013AxSA Driver_FET:MAX15013BxSA Driver_FET:MAX15013CxSA Driver_FET:MAX15013DxSA -Driver_FET:MC33152 -Driver_FET:MC34152 +Driver_FET:MAX626xSA +Driver_FET:MAX627xSA +Driver_FET:MAX628xSA +Driver_FET:MC3x152 Driver_FET:MCP1415 Driver_FET:MCP1415R Driver_FET:MCP1416 @@ -6897,7 +7519,6 @@ Driver_FET:MIC4427 Driver_FET:MIC4428 Driver_FET:MIC4604YM Driver_FET:NCD5702 -Driver_FET:NCV8402xST Driver_FET:PE29101 Driver_FET:PE29102 Driver_FET:PM8834 @@ -6915,6 +7536,8 @@ Driver_FET:TLP250 Driver_FET:UCC21520ADW Driver_FET:UCC21520DW Driver_FET:UCC27511ADBV +Driver_FET:UCC27524D +Driver_FET:UCC27524DGN Driver_FET:UCC27714D Driver_FET:ZXGD3001E6 Driver_FET:ZXGD3002E6 @@ -6922,8 +7545,7 @@ Driver_FET:ZXGD3003E6 Driver_FET:ZXGD3004E6 Driver_FET:ZXGD3006E6 Driver_FET:ZXGD3009E6 -Driver_Haptic:DRV2510-Q1 -Driver_Haptic:DRV2605LDGS +Driver_LED:AL5819W6 Driver_LED:AL8860MP Driver_LED:AL8860WT Driver_LED:AP3019AKTR @@ -7003,6 +7625,8 @@ Driver_LED:MPQ2483DQ Driver_LED:MPQ3362GJ-AEC1 Driver_LED:NCP5623DTBR2G Driver_LED:NCR401U +Driver_LED:PAM2841G +Driver_LED:PAM2841S Driver_LED:PCA9531PW Driver_LED:PCA9635 Driver_LED:PCA9685BS @@ -7040,8 +7664,16 @@ Driver_LED:TLC5971PWP Driver_LED:TLC5971RGE Driver_LED:TLC5973 Driver_LED:TPS61165DBV +Driver_LED:TPS61165DRV +Driver_LED:TPS92200D1DDC +Driver_LED:TPS92200D2DDC +Driver_LED:TPS92511DDA +Driver_LED:TPS92612DBV Driver_LED:TPS92692PWP Driver_LED:WS2811 +Driver_LED:ZXLD383 +Driver_LED:ZXSC310 +Driver_LED:ZXSC400 Driver_LED:iC-HTG Driver_Motor:A4950E Driver_Motor:A4950K @@ -7049,13 +7681,21 @@ Driver_Motor:A4952_LY Driver_Motor:A4953_LJ Driver_Motor:A4954 Driver_Motor:AMT49413 +Driver_Motor:CP2119L +Driver_Motor:CP3119M Driver_Motor:DRV8212P +Driver_Motor:DRV8220DRL +Driver_Motor:DRV8220DSG +Driver_Motor:DRV8231ADSG Driver_Motor:DRV8308 +Driver_Motor:DRV8311H +Driver_Motor:DRV8311P +Driver_Motor:DRV8311S Driver_Motor:DRV8412 Driver_Motor:DRV8432 +Driver_Motor:DRV8434PWP Driver_Motor:DRV8461SPWP Driver_Motor:DRV8662 -Driver_Motor:DRV8711 Driver_Motor:DRV8800PWP Driver_Motor:DRV8800RTY Driver_Motor:DRV8801PWP @@ -7089,6 +7729,8 @@ Driver_Motor:LMD18200 Driver_Motor:MAX22201 Driver_Motor:MAX22202 Driver_Motor:MAX22207 +Driver_Motor:MAX22208xTU +Driver_Motor:MAX22208xUU Driver_Motor:MP6536DU Driver_Motor:PAC5527QM Driver_Motor:PG001M @@ -7111,6 +7753,7 @@ Driver_Motor:STSPIN220 Driver_Motor:STSPIN230 Driver_Motor:STSPIN233 Driver_Motor:STSPIN240 +Driver_Motor:STSPIN32F0A Driver_Motor:TB6612FNG Driver_Motor:TC78H670FTG Driver_Motor:TMC2041-LA @@ -7121,6 +7764,7 @@ Driver_Motor:TMC2130-TA Driver_Motor:TMC2160 Driver_Motor:TMC2202-WA Driver_Motor:TMC2208-LA +Driver_Motor:TMC2209-LA Driver_Motor:TMC2224-LA Driver_Motor:TMC2226-SA Driver_Motor:TMC262 @@ -7131,122 +7775,15 @@ Driver_Motor:VNH2SP30 Driver_Motor:VNH5019A-E Driver_Motor:ZXBM5210-S Driver_Motor:ZXBM5210-SP -Driver_Relay:DRV8860 -Driver_Relay:DRV8860_PWPR -Driver_Relay:MAX4820xUP -Driver_Relay:MAX4821xUP -Driver_Relay:TPL9201_TSSOP -Driver_TEC:MAX1968xUI -Driver_TEC:MAX1969xUI -DSP_AnalogDevices:ADAU1450 -DSP_AnalogDevices:ADAU1451 -DSP_AnalogDevices:ADAU1452 -DSP_AnalogDevices:ADAU1701 -DSP_AnalogDevices:ADAU1702 -DSP_Freescale:DSP96002 -DSP_Microchip_DSPIC33:DSPIC33EP256MU810-xPT -DSP_Microchip_DSPIC33:DSPIC33FJ128GP204 -DSP_Microchip_DSPIC33:DSPIC33FJ128GP804 -DSP_Microchip_DSPIC33:DSPIC33FJ128MC204 -DSP_Microchip_DSPIC33:DSPIC33FJ128MC510A -DSP_Microchip_DSPIC33:DSPIC33FJ128MC710A -DSP_Microchip_DSPIC33:DSPIC33FJ128MC804 -DSP_Microchip_DSPIC33:DSPIC33FJ256MC510A -DSP_Microchip_DSPIC33:DSPIC33FJ256MC710A -DSP_Microchip_DSPIC33:DSPIC33FJ32GP304 -DSP_Microchip_DSPIC33:DSPIC33FJ32MC304 -DSP_Microchip_DSPIC33:DSPIC33FJ64GP204 -DSP_Microchip_DSPIC33:DSPIC33FJ64GP306A-IMR -DSP_Microchip_DSPIC33:DSPIC33FJ64GP804 -DSP_Microchip_DSPIC33:DSPIC33FJ64MC204 -DSP_Microchip_DSPIC33:DSPIC33FJ64MC510A -DSP_Microchip_DSPIC33:DSPIC33FJ64MC710A -DSP_Microchip_DSPIC33:DSPIC33FJ64MC802-xSP -DSP_Microchip_DSPIC33:DSPIC33FJ64MC804 -DSP_Motorola:DSP56301 -DSP_Texas:TMS320LF2406PZ -Fiber_Optic:AFBR-1624Z -Fiber_Optic:AFBR-2624Z -Filter:0603USB-142 -Filter:0603USB-222 -Filter:0603USB-251 -Filter:0603USB-601 -Filter:0603USB-951 -Filter:0850BM14E0016 -Filter:0900FM15K0039 -Filter:1FP41-4R -Filter:1FP42-3R -Filter:1FP44-2R -Filter:1FP45-0R -Filter:1FP45-1R -Filter:1FP61-4R -Filter:1FP62-3R -Filter:1FP64-2R -Filter:1FP65-0R -Filter:1FP65-1R -Filter:B39162B8813P810 -Filter:BNX025 -Filter:Choke_CommonMode_FerriteCore_1234 -Filter:Choke_CommonMode_FerriteCore_1243 -Filter:Choke_CommonMode_FerriteCore_1324 -Filter:Choke_CommonMode_FerriteCore_1342 -Filter:Choke_CommonMode_FerriteCore_1423 -Filter:Choke_CommonMode_PulseElectronics_PH9455x105NL -Filter:Choke_CommonMode_PulseElectronics_PH9455x155NL -Filter:Choke_CommonMode_PulseElectronics_PH9455x156NL -Filter:Choke_CommonMode_PulseElectronics_PH9455x205NL -Filter:Choke_CommonMode_PulseElectronics_PH9455x356NL -Filter:Choke_CommonMode_PulseElectronics_PH9455x405NL -Filter:Choke_CommonMode_PulseElectronics_PH9455x705NL -Filter:Choke_CommonMode_PulseElectronics_PH9455x826NL -Filter:Choke_Schaffner_RN102-0.3-02-12M -Filter:Choke_Schaffner_RN102-0.3-02-22M -Filter:Choke_Schaffner_RN102-0.6-02-4M4 -Filter:Choke_Schaffner_RN102-1-02-3M0 -Filter:Choke_Schaffner_RN102-1.5-02-1M6 -Filter:Choke_Schaffner_RN102-2-02-1M1 -Filter:Choke_Wurth_WE-CNSW_744232090 -Filter:FN405-0.5-02 -Filter:FN405-1-02 -Filter:FN405-10-02 -Filter:FN405-3-02 -Filter:FN405-6-02 -Filter:FN406-0.5-02 -Filter:FN406-1-02 -Filter:FN406-3-02 -Filter:FN406-6-02 -Filter:FN406-8.4-02 -Filter:FN406B-0.5-02 -Filter:FN406B-1-02 -Filter:FN406B-3-02 -Filter:FN406B-6-02 -Filter:FN406B-8.4-02 -Filter:LTC1562xG-2 -Filter:LTC1562xxG -Filter:P300PL104M275xC222 -Filter:P300PL104M275xC332 -Filter:P300PL104M275xC472 -Filter:P300PL154M275xC222 -Filter:P300PL154M275xC332 -Filter:P300PL154M275xC472 -Filter:SAFFA1G58KA0F0A -Filter:SAFFA1G96FN0F0A -Filter:SAFFA2G14FA0F0A -Filter:SAFFA881MFL0F0A -Filter:SAFFA942MFM0F0A -Filter:SAFFB1G58KA0F0A -Filter:SAFFB1G96FN0F0A -Filter:SAFFB2G14FA0F0A -Filter:SAFFB881MFL0F0A -Filter:SAFFB942MFM0F0A -Filter:SF14-1575F5UUA1 -Filter:SF14-1575F5UUC1 +FPGA_Altera_MAX10:10M04SCE144 +FPGA_Altera_MAX10:10M08SCE144 FPGA_CologneChip_GateMate:CCGM1A1 FPGA_Efinix_Trion:T8Q144xx FPGA_Lattice:ICE40HX1K-TQ144 FPGA_Lattice:ICE40HX4K-BG121 FPGA_Lattice:ICE40HX4K-TQ144 FPGA_Lattice:ICE40HX8K-BG121 +FPGA_Lattice:ICE40LP384-SG32 FPGA_Lattice:ICE40UL1K-SWG16 FPGA_Lattice:ICE40UP5K-SG48ITR FPGA_Lattice:ICE5LP1K-SG48 @@ -7267,13 +7804,13 @@ FPGA_Lattice:LFE5UM5G-85F-8BG756x FPGA_Lattice:LFXP2-5E-5TN144 FPGA_Lattice:LFXP2-5E-6TN144 FPGA_Lattice:LFXP2-5E-7TN144 -FPGA_Microsemi:A3P030-VQG100 -FPGA_Microsemi:A3P060-VQG100 +FPGA_Microsemi:A3P030-xVQx100 +FPGA_Microsemi:A3P060-xVQx100 FPGA_Microsemi:A3P1000-PQG208 FPGA_Microsemi:A3P125-PQG208 -FPGA_Microsemi:A3P125-VQG100 +FPGA_Microsemi:A3P125-xVQx100 FPGA_Microsemi:A3P250-PQG208 -FPGA_Microsemi:A3P250-VQG100 +FPGA_Microsemi:A3P250-xVQx100 FPGA_Microsemi:A3P400-PQG208 FPGA_Microsemi:A3P600-PQG208 FPGA_Microsemi:ACT1020PL44 @@ -7282,8 +7819,8 @@ FPGA_Microsemi:ACT1225PL84 FPGA_Microsemi:EX128-TQ100 FPGA_Microsemi:EX128-TQ64 FPGA_Microsemi:EX256-TQ100 -FPGA_Microsemi:EX64-TQ100 FPGA_Microsemi:EX64-TQ64 +FPGA_Microsemi:EX64-xTQx100 FPGA_Microsemi:M2GL090T-FG484 FPGA_Xilinx:XC2018-PC68 FPGA_Xilinx:XC2018-PC84 @@ -7512,6 +8049,145 @@ FPGA_Xilinx_Virtex7:XC7VX690T-FFG1930 FPGA_Xilinx_Virtex7:XC7VX980T-FFG1926 FPGA_Xilinx_Virtex7:XC7VX980T-FFG1928 FPGA_Xilinx_Virtex7:XC7VX980T-FFG1930 +Fiber_Optic:AFBR-1624Z +Fiber_Optic:AFBR-2624Z +Filter:0603USB-142 +Filter:0603USB-222 +Filter:0603USB-251 +Filter:0603USB-601 +Filter:0603USB-951 +Filter:0850BM14E0016 +Filter:0900FM15K0039 +Filter:1FP41-4R +Filter:1FP42-3R +Filter:1FP44-2R +Filter:1FP45-0R +Filter:1FP45-1R +Filter:1FP61-4R +Filter:1FP62-3R +Filter:1FP64-2R +Filter:1FP65-0R +Filter:1FP65-1R +Filter:B39162B8813P810 +Filter:BNX025 +Filter:Choke_CommonMode_FerriteCore_1234 +Filter:Choke_CommonMode_FerriteCore_1243 +Filter:Choke_CommonMode_FerriteCore_1324 +Filter:Choke_CommonMode_FerriteCore_1342 +Filter:Choke_CommonMode_FerriteCore_1423 +Filter:Choke_CommonMode_PulseElectronics_PH9455x105NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x155NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x156NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x205NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x356NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x405NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x705NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x826NL +Filter:Choke_Schaffner_RN102-0.3-02-12M +Filter:Choke_Schaffner_RN102-0.3-02-22M +Filter:Choke_Schaffner_RN102-0.6-02-4M4 +Filter:Choke_Schaffner_RN102-1-02-3M0 +Filter:Choke_Schaffner_RN102-1.5-02-1M6 +Filter:Choke_Schaffner_RN102-2-02-1M1 +Filter:Choke_Wurth_WE-CNSW_744232090 +Filter:FN405-0.5-02 +Filter:FN405-1-02 +Filter:FN405-10-02 +Filter:FN405-3-02 +Filter:FN405-6-02 +Filter:FN406-0.5-02 +Filter:FN406-1-02 +Filter:FN406-3-02 +Filter:FN406-6-02 +Filter:FN406-8.4-02 +Filter:FN406B-0.5-02 +Filter:FN406B-1-02 +Filter:FN406B-3-02 +Filter:FN406B-6-02 +Filter:FN406B-8.4-02 +Filter:LTC1063xN8 +Filter:LTC1063xSW +Filter:LTC1065xN8 +Filter:LTC1065xSW +Filter:LTC1562xG-2 +Filter:LTC1562xxG +Filter:MAX291xPA +Filter:MAX291xSA +Filter:MAX291xWE +Filter:MAX292xPA +Filter:MAX292xSA +Filter:MAX292xWE +Filter:MAX293xPA +Filter:MAX293xSA +Filter:MAX293xWE +Filter:MAX294xPA +Filter:MAX294xSA +Filter:MAX294xWE +Filter:MAX295xPA +Filter:MAX295xSA +Filter:MAX295xWE +Filter:MAX296xPA +Filter:MAX296xSA +Filter:MAX296xWE +Filter:MAX297xPA +Filter:MAX297xSA +Filter:MAX297xWE +Filter:MAX7400xPA +Filter:MAX7400xSA +Filter:MAX7401xPA +Filter:MAX7401xSA +Filter:MAX7403xPA +Filter:MAX7403xSA +Filter:MAX7404xPA +Filter:MAX7404xSA +Filter:MAX7405xPA +Filter:MAX7405xSA +Filter:MAX7407xPA +Filter:MAX7407xSA +Filter:MAX7408xPA +Filter:MAX7408xUA +Filter:MAX7409xPA +Filter:MAX7409xUA +Filter:MAX7410xPA +Filter:MAX7410xUA +Filter:MAX7411xPA +Filter:MAX7411xUA +Filter:MAX7412xPA +Filter:MAX7412xUA +Filter:MAX7413xPA +Filter:MAX7413xUA +Filter:MAX7414xPA +Filter:MAX7414xUA +Filter:MAX7415xPA +Filter:MAX7415xUA +Filter:MAX7418xUA +Filter:MAX7419xUA +Filter:MAX7420xUA +Filter:MAX7421xUA +Filter:MAX7422xUA +Filter:MAX7423xUA +Filter:MAX7424xUA +Filter:MAX7425xUA +Filter:MAX7480xPA +Filter:MAX7480xSA +Filter:P300PL104M275xC222 +Filter:P300PL104M275xC332 +Filter:P300PL104M275xC472 +Filter:P300PL154M275xC222 +Filter:P300PL154M275xC332 +Filter:P300PL154M275xC472 +Filter:SAFFA1G58KA0F0A +Filter:SAFFA1G96FN0F0A +Filter:SAFFA2G14FA0F0A +Filter:SAFFA881MFL0F0A +Filter:SAFFA942MFM0F0A +Filter:SAFFB1G58KA0F0A +Filter:SAFFB1G96FN0F0A +Filter:SAFFB2G14FA0F0A +Filter:SAFFB881MFL0F0A +Filter:SAFFB942MFM0F0A +Filter:SF14-1575F5UUA1 +Filter:SF14-1575F5UUC1 GPU:MC6845 GPU:MC68A45 GPU:MC68B45 @@ -7710,8 +8386,10 @@ Interface_CAN_LIN:MCP2562-E-SN Interface_CAN_LIN:MCP2562-H-MF Interface_CAN_LIN:MCP2562-H-P Interface_CAN_LIN:MCP2562-H-SN -Interface_CAN_LIN:MCP25625-x-SS +Interface_CAN_LIN:MCP25625x-x-ML +Interface_CAN_LIN:MCP25625x-x-SS Interface_CAN_LIN:PCA82C251 +Interface_CAN_LIN:SIT1057TK-3 Interface_CAN_LIN:SN65HVD1050D Interface_CAN_LIN:SN65HVD230 Interface_CAN_LIN:SN65HVD231 @@ -7733,8 +8411,8 @@ Interface_CAN_LIN:TCAN337 Interface_CAN_LIN:TCAN337G Interface_CAN_LIN:TCAN4550RGY Interface_CAN_LIN:TCAN4551RGYRQ1 -Interface_CAN_LIN:TJA1021T -Interface_CAN_LIN:TJA1021TK +Interface_CAN_LIN:TJA1021xT +Interface_CAN_LIN:TJA1021xTK Interface_CAN_LIN:TJA1029T Interface_CAN_LIN:TJA1029TK Interface_CAN_LIN:TJA1042T @@ -7760,6 +8438,7 @@ Interface_CAN_LIN:TJA1145TK-FD Interface_CurrentLoop:XTR111AxDGQ Interface_CurrentLoop:XTR115U Interface_CurrentLoop:XTR116U +Interface_Ethernet:DP83825I Interface_Ethernet:DP83848C Interface_Ethernet:DP83848I Interface_Ethernet:ENC28J60x-ML @@ -7798,6 +8477,8 @@ Interface_Ethernet:WGI210AT Interface_Expansion:AS1115-BQFT Interface_Expansion:AS1115-BSST Interface_Expansion:AW9523B +Interface_Expansion:DS2484Q +Interface_Expansion:DS2484R Interface_Expansion:LTC4314xGN Interface_Expansion:LTC4314xUDC Interface_Expansion:LTC4316xDD @@ -7811,20 +8492,33 @@ Interface_Expansion:MCP23008-xML Interface_Expansion:MCP23008-xP Interface_Expansion:MCP23008-xSO Interface_Expansion:MCP23008-xSS -Interface_Expansion:MCP23017_ML -Interface_Expansion:MCP23017_SO -Interface_Expansion:MCP23017_SP -Interface_Expansion:MCP23017_SS -Interface_Expansion:MCP23S17_ML -Interface_Expansion:MCP23S17_SO -Interface_Expansion:MCP23S17_SP -Interface_Expansion:MCP23S17_SS +Interface_Expansion:MCP23017x-x-ML +Interface_Expansion:MCP23017x-x-SO +Interface_Expansion:MCP23017x-x-SP +Interface_Expansion:MCP23017x-x-SS +Interface_Expansion:MCP23018x-x-MJ +Interface_Expansion:MCP23018x-x-SO +Interface_Expansion:MCP23018x-x-SP +Interface_Expansion:MCP23018x-x-SS +Interface_Expansion:MCP23S17x-x-ML +Interface_Expansion:MCP23S17x-x-SO +Interface_Expansion:MCP23S17x-x-SP +Interface_Expansion:MCP23S17x-x-SS +Interface_Expansion:MCP23S18x-x-MJ +Interface_Expansion:MCP23S18x-x-SO +Interface_Expansion:MCP23S18x-x-SP Interface_Expansion:P82B96 Interface_Expansion:PCA9506BS Interface_Expansion:PCA9516 Interface_Expansion:PCA9536D Interface_Expansion:PCA9536DP Interface_Expansion:PCA9537 +Interface_Expansion:PCA9538BS +Interface_Expansion:PCA9538D +Interface_Expansion:PCA9538PW +Interface_Expansion:PCA9539xBS +Interface_Expansion:PCA9539xD +Interface_Expansion:PCA9539xPW Interface_Expansion:PCA9544AD Interface_Expansion:PCA9544APW Interface_Expansion:PCA9547BS @@ -7841,6 +8535,9 @@ Interface_Expansion:PCA9557BS Interface_Expansion:PCA9557D Interface_Expansion:PCA9557PW Interface_Expansion:PCA9847PW +Interface_Expansion:PCAL6408ABS +Interface_Expansion:PCAL6408AHK +Interface_Expansion:PCAL6408APW Interface_Expansion:PCAL6416AHF Interface_Expansion:PCAL6416APW Interface_Expansion:PCAL6534EV @@ -7853,7 +8550,12 @@ Interface_Expansion:PCF8574TS Interface_Expansion:PCF8575DBR Interface_Expansion:PCF8584 Interface_Expansion:PCF8591 +Interface_Expansion:SC18IS604PW +Interface_Expansion:SC18IS606PW Interface_Expansion:STMPE1600 +Interface_Expansion:TCA6408APW +Interface_Expansion:TCA6408ARGT +Interface_Expansion:TCA6408ARSV Interface_Expansion:TCA9534 Interface_Expansion:TCA9535DBR Interface_Expansion:TCA9535DBT @@ -7862,6 +8564,8 @@ Interface_Expansion:TCA9535PWR Interface_Expansion:TCA9535RGER Interface_Expansion:TCA9535RTWR Interface_Expansion:TCA9544A +Interface_Expansion:TCA9546AD +Interface_Expansion:TCA9546APW Interface_Expansion:TCA9548AMRGER Interface_Expansion:TCA9548APWR Interface_Expansion:TCA9548ARGER @@ -7874,6 +8578,8 @@ Interface_Expansion:TCA9555DBT Interface_Expansion:TCA9555PWR Interface_Expansion:TCA9555RGER Interface_Expansion:TCA9555RTWR +Interface_Expansion:TCAL6408PW +Interface_Expansion:TCAL6408RSV Interface_Expansion:TPIC6595 Interface_Expansion:XRA1201IG24 Interface_Expansion:XRA1201IL24 @@ -7954,6 +8660,7 @@ Interface_UART:68C681 Interface_UART:8250 Interface_UART:8252 Interface_UART:ADM101E +Interface_UART:ADM1485xRZ Interface_UART:ADM1491EBR Interface_UART:ADM205 Interface_UART:ADM206 @@ -7972,6 +8679,7 @@ Interface_UART:ADM2582E Interface_UART:ADM2587E Interface_UART:ADM2682E Interface_UART:ADM2687E +Interface_UART:ADM3485xR Interface_UART:ADM3488ExR Interface_UART:ADM3490ExR Interface_UART:ADM3491ExR @@ -8081,6 +8789,9 @@ Interface_UART:SC16IS760xPW Interface_UART:SC16IS762IBS Interface_UART:SC16IS762IPW Interface_UART:SN65HVD11HD +Interface_UART:SN65HVD1780 +Interface_UART:SN65HVD1781 +Interface_UART:SN65HVD1782 Interface_UART:SN65LBC176D Interface_UART:SN65LBC176P Interface_UART:SN65LBC176QD @@ -8102,6 +8813,7 @@ Interface_UART:SSP3085 Interface_UART:ST202ExD Interface_UART:ST232ExD Interface_UART:ST485E +Interface_UART:STR485 Interface_UART:THVD1400D Interface_UART:THVD1420D Interface_UART:THVD1450D @@ -8113,12 +8825,16 @@ Interface_UART:Z8530 Interface_USB:ADUM3160 Interface_USB:ADUM4160 Interface_USB:AP33771 +Interface_USB:AP33772SDKZ-xx-FA02 Interface_USB:BQ24392 +Interface_USB:CH221K Interface_USB:CH224K Interface_USB:CH236D Interface_USB:CH246D Interface_USB:CH330N +Interface_USB:CH334F Interface_USB:CH334R +Interface_USB:CH334U Interface_USB:CH340C Interface_USB:CH340E Interface_USB:CH340G @@ -8126,10 +8842,12 @@ Interface_USB:CH340K Interface_USB:CH340N Interface_USB:CH340T Interface_USB:CH340X +Interface_USB:CH341A Interface_USB:CH343G Interface_USB:CH343P Interface_USB:CH344Q Interface_USB:CH9102F +Interface_USB:CP2102C-Axx-xQFN24 Interface_USB:CP2102N-Axx-xQFN20 Interface_USB:CP2102N-Axx-xQFN24 Interface_USB:CP2102N-Axx-xQFN28 @@ -8162,6 +8880,7 @@ Interface_USB:FT221XQ Interface_USB:FT221XS Interface_USB:FT2232D Interface_USB:FT2232HL +Interface_USB:FT2232HPQ Interface_USB:FT2232HQ Interface_USB:FT230XQ Interface_USB:FT230XS @@ -8183,6 +8902,10 @@ Interface_USB:FUSB302B11MPX Interface_USB:FUSB302BMPX Interface_USB:FUSB303BTMX Interface_USB:FUSB307BMPX +Interface_USB:GL3224-ONY +Interface_USB:HD3SS6126 +Interface_USB:HUSB238A-xxxxx-QN16R +Interface_USB:HUSB238_xxxDD Interface_USB:IP2721 Interface_USB:MA8601 Interface_USB:MCP2200-E-SS @@ -8201,9 +8924,11 @@ Interface_USB:MCP2221AxP Interface_USB:MCP2221AxSL Interface_USB:MCP2221AxST Interface_USB:MP5034GJ +Interface_USB:MS2130 Interface_USB:STULPI01A Interface_USB:STULPI01B Interface_USB:STUSB4500QTR +Interface_USB:TCPP03-M20 Interface_USB:TPS2500DRC Interface_USB:TPS2501DRC Interface_USB:TPS2513 @@ -8213,6 +8938,7 @@ Interface_USB:TPS2514A Interface_USB:TPS2560 Interface_USB:TPS2561 Interface_USB:TPS25730D +Interface_USB:TPS25810RVC Interface_USB:TS3USB30EDGSR Interface_USB:TS3USB30ERSWR Interface_USB:TS3USBCA410 @@ -8225,6 +8951,7 @@ Interface_USB:TUSB322I Interface_USB:TUSB4041I Interface_USB:TUSB7340 Interface_USB:TUSB8041 +Interface_USB:TUSB8043A Interface_USB:UPD720202K8-7x1-BAA Interface_USB:USB2504 Interface_USB:USB2514B_Bi @@ -8301,6 +9028,10 @@ Isolator:ADuM7642A Isolator:ADuM7642C Isolator:ADuM7643A Isolator:ADuM7643C +Isolator:CA-IS3721CHG +Isolator:CA-IS3721CHS +Isolator:CA-IS3721CLG +Isolator:CA-IS3721CLS Isolator:CNY17-1 Isolator:CNY17-2 Isolator:CNY17-3 @@ -8357,6 +9088,12 @@ Isolator:ISO1541 Isolator:ISO1642DWR Isolator:ISO1643DWR Isolator:ISO1644DWR +Isolator:ISO6720BD +Isolator:ISO6720FBD +Isolator:ISO6721BD +Isolator:ISO6721FBD +Isolator:ISO6721RBD +Isolator:ISO6721RFBD Isolator:ISO6731 Isolator:ISO6740 Isolator:ISO6741 @@ -8375,6 +9112,10 @@ Isolator:ISO7341C Isolator:ISO7341FC Isolator:ISO7342C Isolator:ISO7342FC +Isolator:ISO7720D +Isolator:ISO7720DWV +Isolator:ISO7721D +Isolator:ISO7721DWV Isolator:ISO7760DBQ Isolator:ISO7760DW Isolator:ISO7760FDBQ @@ -8424,6 +9165,7 @@ Isolator:MOCD211M Isolator:MOCD213M Isolator:MOCD217M Isolator:NSL-32 +Isolator:Optocoupler_DC_PhotoNPN_AKEC Isolator:PC3H4 Isolator:PC3H4A Isolator:PC817 @@ -8432,6 +9174,10 @@ Isolator:PC837 Isolator:PC847 Isolator:PS8802-1 Isolator:PS8802-2 +Isolator:SFH601-1 +Isolator:SFH601-2 +Isolator:SFH601-2X007 +Isolator:SFH601-3X017 Isolator:SFH617A-1 Isolator:SFH617A-1X001 Isolator:SFH617A-1X006 @@ -8653,8 +9399,15 @@ Isolator_Analog:ACPL-C79B Isolator_Analog:ACPL-C870 Isolator_Analog:ACPL-C87A Isolator_Analog:ACPL-C87B +Isolator_Analog:AMC1100DWV +Isolator_Analog:AMC1200BDWV +Isolator_Analog:AMC1300BDWV +Isolator_Analog:AMC1300DWV +Isolator_Analog:AMC1350DWV +Isolator_Analog:AMC1351DWV Isolator_Analog:AMC3330 Isolator_Analog:IL300 +Isolator_Analog:ISO224xDWV Isolator_Analog:LOC112 Isolator_Analog:LOC112P Isolator_Analog:LOC112S @@ -8701,8 +9454,12 @@ LED:Inolux_IN-PI554FCH LED:Inolux_IN-PI556FCH LED:LD271 LED:LD274 -LED:LED_Cree_XHP50_12V -LED:LED_Cree_XHP50_6V +LED:LED_Cree_XHP50_12V_HighDensity +LED:LED_Cree_XHP50_12V_HighIntensity +LED:LED_Cree_XHP50_3V_HighDensity +LED:LED_Cree_XHP50_3V_HighIntensity +LED:LED_Cree_XHP50_6V_HighDensity +LED:LED_Cree_XHP50_6V_HighIntensity LED:LED_Cree_XHP70_12V LED:LED_Cree_XHP70_6V LED:LTST-C235KGKRKT @@ -8720,6 +9477,7 @@ LED:SFH482 LED:SK6805 LED:SK6812 LED:SK6812MINI +LED:SK6812MINI-E LED:SMLVN6RGB LED:TSAL4400 LED:WS2812 @@ -8730,7 +9488,10 @@ LED:WS2813 LED:WS2822S Logic_LevelTranslator:74LVC2T45DC Logic_LevelTranslator:74LVCH2T45DC +Logic_LevelTranslator:CD40109BPW Logic_LevelTranslator:FXMA108 +Logic_LevelTranslator:LSF0108BQ +Logic_LevelTranslator:LSF0108PW Logic_LevelTranslator:NCN4555MN Logic_LevelTranslator:NLSV2T244D Logic_LevelTranslator:NLSV2T244DM @@ -8758,6 +9519,7 @@ Logic_LevelTranslator:TXB0101YZP Logic_LevelTranslator:TXB0102DCT Logic_LevelTranslator:TXB0102DCU Logic_LevelTranslator:TXB0102YZP +Logic_LevelTranslator:TXB0104BQA Logic_LevelTranslator:TXB0104D Logic_LevelTranslator:TXB0104PW Logic_LevelTranslator:TXB0104RGY @@ -8783,15 +9545,21 @@ Logic_LevelTranslator:TXS0104ED Logic_LevelTranslator:TXS0104EPW Logic_LevelTranslator:TXS0108EPW Logic_LevelTranslator:TXS02612RTW +Logic_Programmable:ATF16V8Bxx-xxPU Logic_Programmable:GAL16V8 Logic_Programmable:PAL16L8 -Logic_Programmable:PAL20 +Logic_Programmable:PAL16R8 +Logic_Programmable:PAL16RP8A +Logic_Programmable:PAL16RP8A_Programming +Logic_Programmable:PAL20L10xxNS Logic_Programmable:PAL20L8 +Logic_Programmable:PAL20R8xx-P Logic_Programmable:PAL20RS10 Logic_Programmable:PAL24 +Logic_Programmable:PALCE16V8 Logic_Programmable:PEEL22CV10AP Logic_Programmable:PEEL22CV10AS -MCU_AnalogDevices:ADUC816 +MCU_AnalogDevices:ADUC816BSZ MCU_AnalogDevices:MAX32660GTP MCU_AnalogDevices:MAX32670GTL MCU_Cypress:CY7C68013A-56LTX @@ -8834,6 +9602,8 @@ MCU_Dialog:DA14691 MCU_Dialog:DA14695 MCU_Espressif:ESP32-C3 MCU_Espressif:ESP32-PICO-D4 +MCU_Espressif:ESP32-PICO-V3 +MCU_Espressif:ESP32-PICO-V3-02 MCU_Espressif:ESP32-S2 MCU_Espressif:ESP32-S3 MCU_Espressif:ESP8266EX @@ -9756,6 +10526,7 @@ MCU_Microchip_PIC12:PIC12LF1840T48-xST MCU_Microchip_PIC16:PIC16C505-IP MCU_Microchip_PIC16:PIC16C505-ISL MCU_Microchip_PIC16:PIC16C505-IST +MCU_Microchip_PIC16:PIC16F13145-xP MCU_Microchip_PIC16:PIC16F1454-IML MCU_Microchip_PIC16:PIC16F1454-IP MCU_Microchip_PIC16:PIC16F1454-ISL @@ -10360,6 +11131,7 @@ MCU_Module:CHIP MCU_Module:CHIP-PRO MCU_Module:Carambola2 MCU_Module:Electrosmith_Daisy_Seed_Rev4 +MCU_Module:Flipper_Zero MCU_Module:Google_Coral MCU_Module:Maple_Mini MCU_Module:NUCLEO144-F207ZG @@ -10398,17 +11170,6 @@ MCU_Module:VisionSOM-6UL MCU_Module:VisionSOM-6ULL MCU_Module:VisionSOM-RT MCU_Module:VisionSOM-STM32MP1 -MCU_Nordic:nRF51x22-QFxx -MCU_Nordic:nRF52810-QCxx -MCU_Nordic:nRF52810-QFxx -MCU_Nordic:nRF52811-QCxx -MCU_Nordic:nRF52820-QDxx -MCU_Nordic:nRF52832-QFxx -MCU_Nordic:nRF52833_QDxx -MCU_Nordic:nRF52833_QIxx -MCU_Nordic:nRF52840 -MCU_Nordic:nRF5340-QKxx -MCU_Nordic:nRF9160-SIxA MCU_NXP_ColdFire:MCF5211CAE66 MCU_NXP_ColdFire:MCF5212CAE66 MCU_NXP_ColdFire:MCF5213-LQFP100 @@ -10677,6 +11438,7 @@ MCU_NXP_LPC:LPC2142FBD64 MCU_NXP_LPC:LPC2144FBD64 MCU_NXP_LPC:LPC2146FBD64 MCU_NXP_LPC:LPC2148FBD64 +MCU_NXP_LPC:LPC433xxET100 MCU_NXP_LPC:LPC811M001JDH16 MCU_NXP_LPC:LPC812M001JDH16 MCU_NXP_LPC:LPC812M101JD20 @@ -10881,63 +11643,28 @@ MCU_NXP_S08:MC9S08SL32xTJ MCU_NXP_S08:MC9S08SL32xTL MCU_NXP_S08:MC9S08SV16CLC MCU_NXP_S08:MC9S08SV8CLC +MCU_Nordic:nRF51x22-QFxx +MCU_Nordic:nRF52810-QCxx +MCU_Nordic:nRF52810-QFxx +MCU_Nordic:nRF52811-QCxx +MCU_Nordic:nRF52820-QDxx +MCU_Nordic:nRF52832-QFxx +MCU_Nordic:nRF52833_QDxx +MCU_Nordic:nRF52833_QIxx +MCU_Nordic:nRF52840 +MCU_Nordic:nRF5340-QKxx +MCU_Nordic:nRF9160-SIxA MCU_Parallax:P8X32A-D40 MCU_Parallax:P8X32A-M44 MCU_Parallax:P8X32A-Q44 MCU_Puya:PY32F002AF15P +MCU_Puya:PY32F002AW15U MCU_RaspberryPi:RP2040 +MCU_RaspberryPi:RP2350A +MCU_RaspberryPi:RP2350B +MCU_RaspberryPi:RP2354A +MCU_RaspberryPi:RP2354B MCU_Renesas_Synergy_S1:R7FS12878xA01CFL -MCU_SiFive:FE310-G000 -MCU_SiFive:FE310-G002 -MCU_SiFive:FU540-C000 -MCU_SiliconLabs:C8051F320-GQ -MCU_SiliconLabs:C8051F321-GM -MCU_SiliconLabs:C8051F380-GQ -MCU_SiliconLabs:C8051F381-GM -MCU_SiliconLabs:C8051F381-GQ -MCU_SiliconLabs:C8051F382-GQ -MCU_SiliconLabs:C8051F383-GM -MCU_SiliconLabs:C8051F383-GQ -MCU_SiliconLabs:C8051F384-GQ -MCU_SiliconLabs:C8051F385-GM -MCU_SiliconLabs:C8051F385-GQ -MCU_SiliconLabs:C8051F386-GQ -MCU_SiliconLabs:C8051F387-GM -MCU_SiliconLabs:C8051F387-GQ -MCU_SiliconLabs:C8051F38C-GM -MCU_SiliconLabs:C8051F38C-GQ -MCU_SiliconLabs:EFM32G230F128G-E-QFN64 -MCU_SiliconLabs:EFM32HG108F32G-C-QFN24 -MCU_SiliconLabs:EFM32HG108F64G-C-QFN24 -MCU_SiliconLabs:EFM32HG308F32G-C-QFN24 -MCU_SiliconLabs:EFM32HG308F64G-C-QFN24 -MCU_SiliconLabs:EFM32ZG108F16-B-QFN24 -MCU_SiliconLabs:EFM32ZG108F32-B-QFN24 -MCU_SiliconLabs:EFM32ZG108F4-B-QFN24 -MCU_SiliconLabs:EFM32ZG108F8-B-QFN24 -MCU_SiliconLabs:EFM32ZG110F16-B-QFN24 -MCU_SiliconLabs:EFM32ZG110F32-B-QFN24 -MCU_SiliconLabs:EFM32ZG110F4-B-QFN24 -MCU_SiliconLabs:EFM32ZG110F8-B-QFN24 -MCU_SiliconLabs:EFM8BB10F2A-A-QFN20 -MCU_SiliconLabs:EFM8BB10F2G-A-QFN20 -MCU_SiliconLabs:EFM8BB10F2I-A-QFN20 -MCU_SiliconLabs:EFM8BB10F4A-A-QFN20 -MCU_SiliconLabs:EFM8BB10F4G-A-QFN20 -MCU_SiliconLabs:EFM8BB10F4I-A-QFN20 -MCU_SiliconLabs:EFM8BB10F8A-A-QFN20 -MCU_SiliconLabs:EFM8BB10F8G-A-QFN20 -MCU_SiliconLabs:EFM8BB10F8G-A-QSOP24 -MCU_SiliconLabs:EFM8BB10F8G-A-SOIC16 -MCU_SiliconLabs:EFM8BB10F8I-A-QFN20 -MCU_SiliconLabs:EFM8BB10F8I-A-QSOP24 -MCU_SiliconLabs:EFM8BB10F8I-A-SOIC16 -MCU_SiliconLabs:EFM8LB12F32E-C-QFP32 -MCU_SiliconLabs:EFM8LB12F64E-C-QFP32 -MCU_SiliconLabs:EFM8UB30F40G-A-QFN20 -MCU_SiliconLabs:EFM8UB31F40G-A-QFN24 -MCU_SiliconLabs:EFM8UB31F40G-A-QSOP24 -MCU_SiliconLabs:EFR32xG23xxxxF512xM48 MCU_STC:IAP15W205S-35x-SOP16 MCU_STC:IRC15W207S-35x-SOP16 MCU_STC:STC15W201S-35x-SOP16 @@ -13804,6 +14531,57 @@ MCU_ST_STM8:STM8S208MB MCU_ST_STM8:STM8S208R6 MCU_ST_STM8:STM8S208R8 MCU_ST_STM8:STM8S208RB +MCU_SiFive:FE310-G000 +MCU_SiFive:FE310-G002 +MCU_SiFive:FU540-C000 +MCU_SiliconLabs:C8051F320-GQ +MCU_SiliconLabs:C8051F321-GM +MCU_SiliconLabs:C8051F380-GQ +MCU_SiliconLabs:C8051F381-GM +MCU_SiliconLabs:C8051F381-GQ +MCU_SiliconLabs:C8051F382-GQ +MCU_SiliconLabs:C8051F383-GM +MCU_SiliconLabs:C8051F383-GQ +MCU_SiliconLabs:C8051F384-GQ +MCU_SiliconLabs:C8051F385-GM +MCU_SiliconLabs:C8051F385-GQ +MCU_SiliconLabs:C8051F386-GQ +MCU_SiliconLabs:C8051F387-GM +MCU_SiliconLabs:C8051F387-GQ +MCU_SiliconLabs:C8051F38C-GM +MCU_SiliconLabs:C8051F38C-GQ +MCU_SiliconLabs:EFM32G230F128G-E-QFN64 +MCU_SiliconLabs:EFM32HG108F32G-C-QFN24 +MCU_SiliconLabs:EFM32HG108F64G-C-QFN24 +MCU_SiliconLabs:EFM32HG308F32G-C-QFN24 +MCU_SiliconLabs:EFM32HG308F64G-C-QFN24 +MCU_SiliconLabs:EFM32ZG108F16-B-QFN24 +MCU_SiliconLabs:EFM32ZG108F32-B-QFN24 +MCU_SiliconLabs:EFM32ZG108F4-B-QFN24 +MCU_SiliconLabs:EFM32ZG108F8-B-QFN24 +MCU_SiliconLabs:EFM32ZG110F16-B-QFN24 +MCU_SiliconLabs:EFM32ZG110F32-B-QFN24 +MCU_SiliconLabs:EFM32ZG110F4-B-QFN24 +MCU_SiliconLabs:EFM32ZG110F8-B-QFN24 +MCU_SiliconLabs:EFM8BB10F2A-A-QFN20 +MCU_SiliconLabs:EFM8BB10F2G-A-QFN20 +MCU_SiliconLabs:EFM8BB10F2I-A-QFN20 +MCU_SiliconLabs:EFM8BB10F4A-A-QFN20 +MCU_SiliconLabs:EFM8BB10F4G-A-QFN20 +MCU_SiliconLabs:EFM8BB10F4I-A-QFN20 +MCU_SiliconLabs:EFM8BB10F8A-A-QFN20 +MCU_SiliconLabs:EFM8BB10F8G-A-QFN20 +MCU_SiliconLabs:EFM8BB10F8G-A-QSOP24 +MCU_SiliconLabs:EFM8BB10F8G-A-SOIC16 +MCU_SiliconLabs:EFM8BB10F8I-A-QFN20 +MCU_SiliconLabs:EFM8BB10F8I-A-QSOP24 +MCU_SiliconLabs:EFM8BB10F8I-A-SOIC16 +MCU_SiliconLabs:EFM8LB12F32E-C-QFP32 +MCU_SiliconLabs:EFM8LB12F64E-C-QFP32 +MCU_SiliconLabs:EFM8UB30F40G-A-QFN20 +MCU_SiliconLabs:EFM8UB31F40G-A-QFN24 +MCU_SiliconLabs:EFM8UB31F40G-A-QSOP24 +MCU_SiliconLabs:EFR32xG23xxxxF512xM48 MCU_Texas:LM3S6911-EQC50 MCU_Texas:LM3S6911-IQC50 MCU_Texas:LM4F110B2QR @@ -14187,19 +14965,22 @@ MCU_Texas_MSP430:MSP430G2855IRHA40 MCU_Texas_MSP430:MSP430G2955IDA38 MCU_Texas_MSP430:MSP430G2955IRHA40 MCU_Texas_SimpleLink:CC1312R1F3RGZ -MCU_WCH_CH32V0:CH32V003AxMx -MCU_WCH_CH32V0:CH32V003FxPx -MCU_WCH_CH32V0:CH32V003FxUx -MCU_WCH_CH32V0:CH32V003JxMx -MCU_WCH_CH32V2:CH32V203CxTx -MCU_WCH_CH32V2:CH32V203F6P6 -MCU_WCH_CH32V2:CH32V203GxUx -MCU_WCH_CH32V3:CH32V30xCxTx -MCU_WCH_CH32V3:CH32V30xFxPx -MCU_WCH_CH32V3:CH32V30xRxTx -MCU_WCH_CH32V3:CH32V30xVxTx -MCU_WCH_CH32V3:CH32V30xWxUx -MCU_WCH_CH32X0:CH32X035G8U6 +MCU_Trident:T32CZ20B20GQ40 +MCU_WCH_RiscV:CH32V003AxMx +MCU_WCH_RiscV:CH32V003FxPx +MCU_WCH_RiscV:CH32V003FxUx +MCU_WCH_RiscV:CH32V003JxMx +MCU_WCH_RiscV:CH32V005D6U6 +MCU_WCH_RiscV:CH32V203CxTx +MCU_WCH_RiscV:CH32V203F6P6 +MCU_WCH_RiscV:CH32V203GxUx +MCU_WCH_RiscV:CH32V30xCxTx +MCU_WCH_RiscV:CH32V30xFxPx +MCU_WCH_RiscV:CH32V30xRxTx +MCU_WCH_RiscV:CH32V30xVxTx +MCU_WCH_RiscV:CH32V30xWxUx +MCU_WCH_RiscV:CH32X035G8U6 +MCU_WCH_RiscV:CH592F Mechanical:DIN_Rail_Adapter Mechanical:Fiducial Mechanical:Heatsink @@ -14208,14 +14989,18 @@ Mechanical:Heatsink_Pad_2Pin Mechanical:Heatsink_Pad_3Pin Mechanical:Housing Mechanical:Housing_Pad +Mechanical:Mechanical_Shape Mechanical:MountingHole Mechanical:MountingHole_Pad Mechanical:MountingHole_Pad_MP +Mechanical:MouseBite Memory_EEPROM:24AA02-OT Memory_EEPROM:24AA025E-OT Memory_EEPROM:24AA025E-SN Memory_EEPROM:24AA02E-OT Memory_EEPROM:24AA02E-SN +Memory_EEPROM:24C02Cx-x-MNY +Memory_EEPROM:24C02Cx-x-SN Memory_EEPROM:24LC00 Memory_EEPROM:24LC01 Memory_EEPROM:24LC02 @@ -14343,6 +15128,8 @@ Memory_Flash:AT45DB161B-RC-2.5 Memory_Flash:AT45DB161B-TC Memory_Flash:AT45DB161B-TC-2.5 Memory_Flash:AT45DB161D-SU +Memory_Flash:EPCQ16ASI8N +Memory_Flash:EPCQ32ASI8N Memory_Flash:GD25D05CT Memory_Flash:GD25D10CT Memory_Flash:GD25QxxxEY @@ -14363,6 +15150,7 @@ Memory_Flash:MX25R3235FM2xx1 Memory_Flash:MX25R3235FZNxx0 Memory_Flash:MX25R3235FZNxx1 Memory_Flash:SST25VF080B-50-4x-S2Ax +Memory_Flash:SST26VF064Bxx-xxxx-MF Memory_Flash:SST39SF010 Memory_Flash:SST39SF020 Memory_Flash:SST39SF040 @@ -14404,6 +15192,7 @@ Memory_NVRAM:MB85RS16 Memory_NVRAM:MB85RS1MT Memory_NVRAM:MB85RS256B Memory_NVRAM:MB85RS2MT +Memory_NVRAM:MB85RS2MTAPNF Memory_NVRAM:MB85RS512T Memory_NVRAM:MB85RS64 Memory_NVRAM:MR20H40 @@ -14412,6 +15201,17 @@ Memory_NVRAM:STK14C88 Memory_NVRAM:STK14C88-3 Memory_NVRAM:STK14C88C Memory_NVRAM:STK14C88C-3 +Memory_RAM:23AA02M-IP +Memory_RAM:23AA02M-ISN +Memory_RAM:23AA02M-IST +Memory_RAM:APS1604M-3SQRx-SN +Memory_RAM:APS1604M-3SQRx-ZR +Memory_RAM:APS1604M-SQRx-SN +Memory_RAM:APS1604M-SQRx-ZR +Memory_RAM:APS6404L-3SQRx-SN +Memory_RAM:APS6404L-3SQRx-ZR +Memory_RAM:APS6404L-SQRx-SN +Memory_RAM:APS6404L-SQRx-ZR Memory_RAM:AS4C256M16D3 Memory_RAM:AS4C4M16SA Memory_RAM:AS6C1008-xxB @@ -14435,7 +15235,9 @@ Memory_RAM:CY62128Exx-xxZ Memory_RAM:CY62256-70PC Memory_RAM:CY7C199 Memory_RAM:ESP-PSRAM32 +Memory_RAM:H5AN8G6NAFR-xxC Memory_RAM:H5AN8G8NAFR-UHC +Memory_RAM:HM50256P Memory_RAM:HM62256BLP Memory_RAM:HM628128D_DIP32_SOP32 Memory_RAM:HM628128D_TSOP32 @@ -14477,6 +15279,12 @@ Memory_RAM:M48Tx2 Memory_RAM:M48Zx2 Memory_RAM:MK4116N Memory_RAM:MK4164N +Memory_RAM:MT40A512M16JY +Memory_RAM:MT40A512M16LY +Memory_RAM:MT40A512M16TB +Memory_RAM:MT41K256M16HA +Memory_RAM:MT41K256M16LY +Memory_RAM:MT41K256M16TW Memory_RAM:MT48LC16M16A2P Memory_RAM:MT48LC16M16A2TG Memory_RAM:MT48LC32M8A2P @@ -14485,10 +15293,16 @@ Memory_RAM:MT48LC64M4A2P Memory_RAM:MT48LC64M4A2TG Memory_RAM:R1LP0108ESF Memory_RAM:R1LP0108ESN +Memory_RAM:TC51832P +Memory_RAM:TMM41256AP Memory_RAM:W9812G6KH-5 Memory_RAM:W9812G6KH-6 Memory_RAM:W9812G6KH-6I Memory_RAM:W9812G6KH-75 +Memory_RAM:uPD41264C +Memory_RAM:uPD4168C +Memory_RAM:uPD42832C +Memory_RAM:uPD4364CX Memory_ROM:XC18V01SO20 Memory_ROM:XCF08P Memory_UniqueID:DS2401P @@ -14637,6 +15451,7 @@ Oscillator:XUY53 Oscillator:XUY71 Oscillator:XUY72 Oscillator:XUY73 +Potentiometer_Digital:AD5160 Potentiometer_Digital:AD5253 Potentiometer_Digital:AD5254 Potentiometer_Digital:AD5272BCP @@ -14651,6 +15466,7 @@ Potentiometer_Digital:DS1267_DIP Potentiometer_Digital:DS1267_SOIC Potentiometer_Digital:DS1267_TSSOP Potentiometer_Digital:DS1882E +Potentiometer_Digital:DS3502 Potentiometer_Digital:MAX5436 Potentiometer_Digital:MAX5438 Potentiometer_Digital:MCP4011-xxxxMS @@ -14677,6 +15493,8 @@ Potentiometer_Digital:MCP4151-xxxx-P Potentiometer_Digital:MCP4152-xxxx-P Potentiometer_Digital:MCP4161-xxxx-P Potentiometer_Digital:MCP4162-xxxx-P +Potentiometer_Digital:MCP41U83x-xxxx-7N +Potentiometer_Digital:MCP41U83x-xxxx-ST Potentiometer_Digital:MCP42010 Potentiometer_Digital:MCP42050 Potentiometer_Digital:MCP42100 @@ -14697,107 +15515,6 @@ Potentiometer_Digital:TPL0401B-10-Q1 Potentiometer_Digital:X9118 Potentiometer_Digital:X9250 Potentiometer_Digital:X9258 -power:+10V -power:+12C -power:+12L -power:+12LF -power:+12P -power:+12V -power:+12VA -power:+15V -power:+1V0 -power:+1V1 -power:+1V2 -power:+1V35 -power:+1V5 -power:+1V8 -power:+24V -power:+28V -power:+2V5 -power:+2V8 -power:+3.3V -power:+3.3VA -power:+3.3VADC -power:+3.3VDAC -power:+3.3VP -power:+36V -power:+3V0 -power:+3V3 -power:+3V8 -power:+48V -power:+4V -power:+5C -power:+5F -power:+5P -power:+5V -power:+5VA -power:+5VD -power:+5VL -power:+5VP -power:+6V -power:+7.5V -power:+8V -power:+9V -power:+9VA -power:+BATT -power:+VDC -power:+VSW -power:-10V -power:-12V -power:-12VA -power:-15V -power:-24V -power:-2V5 -power:-36V -power:-3V3 -power:-48V -power:-5V -power:-5VA -power:-6V -power:-8V -power:-9V -power:-9VA -power:-BATT -power:-VDC -power:-VSW -power:AC -power:Earth -power:Earth_Clean -power:Earth_Protective -power:GND -power:GND1 -power:GND2 -power:GND3 -power:GNDA -power:GNDD -power:GNDPWR -power:GNDREF -power:GNDS -power:HT -power:LINE -power:NEUT -power:PRI_HI -power:PRI_LO -power:PRI_MID -power:PWR_FLAG -power:VAA -power:VAC -power:VBUS -power:VCC -power:VCCQ -power:VCOM -power:VD -power:VDC -power:VDD -power:VDDA -power:VDDF -power:VEE -power:VMEM -power:VPP -power:VS -power:VSS -power:VSSA -power:Vdrive Power_Management:AAT4610BIGV-1-T1 Power_Management:AAT4610BIGV-T1 Power_Management:AAT4616IGV-1-T1 @@ -14949,6 +15666,8 @@ Power_Management:LM5067MM-1 Power_Management:LM5067MM-2 Power_Management:LM5067MMX-2 Power_Management:LM5067MWX-1 +Power_Management:LM5069MM-1 +Power_Management:LM5069MM-2 Power_Management:LM66100DCK Power_Management:LM74700 Power_Management:LMG3410 @@ -15009,6 +15728,13 @@ Power_Management:MIC2091-2YM5 Power_Management:MIC2544-2YM Power_Management:MIC2587-1 Power_Management:MIC2587R-1 +Power_Management:MIC94090YC6 +Power_Management:MIC94091YC6 +Power_Management:MIC94092YC6 +Power_Management:MIC94093YC6 +Power_Management:MIC94094YC6 +Power_Management:MIC94095YC6 +Power_Management:MP5087A Power_Management:NIS5420MTxTXG Power_Management:NPC45560-H Power_Management:NPC45560-L @@ -15018,6 +15744,7 @@ Power_Management:RT9742AGJ5F Power_Management:RT9742ANGJ5F Power_Management:RT9742BGJ5F Power_Management:RT9742BNGJ5F +Power_Management:RT9742SNGV Power_Management:SN6505ADBV Power_Management:SN6505BDBV Power_Management:SN6507DGQ @@ -15025,6 +15752,9 @@ Power_Management:STM6600 Power_Management:STM6601 Power_Management:SiP32431DR3 Power_Management:SiP32432DR3 +Power_Management:SiP32508DT +Power_Management:SiP32509DT +Power_Management:SiP32510DT Power_Management:TEA1708T Power_Management:TLE8102SG Power_Management:TLE8104E @@ -15037,8 +15767,10 @@ Power_Management:TPS2065CDBV Power_Management:TPS2065CDBVx-2 Power_Management:TPS2069CDBV Power_Management:TPS2116DRL +Power_Management:TPS22810DBV Power_Management:TPS22810DRV Power_Management:TPS22917DBV +Power_Management:TPS22917LDBV Power_Management:TPS22929D Power_Management:TPS22993 Power_Management:TPS2412D @@ -15046,6 +15778,14 @@ Power_Management:TPS2412PW Power_Management:TPS2419D Power_Management:TPS2419PW Power_Management:TPS2592xx +Power_Management:TPS2596xx +Power_Management:TPS26400PWP +Power_Management:TPS26400RHF +Power_Management:TPS26600PWP +Power_Management:TPS26600RHF +Power_Management:TPS26601RHF +Power_Management:TPS26602PWP +Power_Management:TPS26602RHF Power_Management:TPS26630RGE Power_Management:TPS26631PWP Power_Management:TPS26631RGE @@ -15071,6 +15811,7 @@ Power_Protection:ECMF04-4HSWM10 Power_Protection:EMI2121MTTAG Power_Protection:EMI8132 Power_Protection:ESD224DQA +Power_Protection:ESD3324P Power_Protection:ESDA14V2SC5 Power_Protection:ESDA5V3L Power_Protection:ESDA5V3SC5 @@ -15078,6 +15819,7 @@ Power_Protection:ESDA6V1-5SC6 Power_Protection:ESDA6V1BC6 Power_Protection:ESDA6V1SC5 Power_Protection:ESDLC5V0PB8 +Power_Protection:ESDS304 Power_Protection:IP3319CX6 Power_Protection:IP4234CZ6 Power_Protection:IP4251CZ8-4-TTL @@ -15129,31 +15871,31 @@ Power_Protection:SP0505BAJT Power_Protection:SP7538P Power_Protection:SRV05-4 Power_Protection:SZNUP2105L -Power_Protection:TBU-CA-025-050-WH -Power_Protection:TBU-CA-025-100-WH -Power_Protection:TBU-CA-025-200-WH -Power_Protection:TBU-CA-025-300-WH -Power_Protection:TBU-CA-025-500-WH -Power_Protection:TBU-CA-040-050-WH -Power_Protection:TBU-CA-040-100-WH -Power_Protection:TBU-CA-040-200-WH -Power_Protection:TBU-CA-040-300-WH -Power_Protection:TBU-CA-040-500-WH -Power_Protection:TBU-CA-050-050-WH -Power_Protection:TBU-CA-050-100-WH -Power_Protection:TBU-CA-050-200-WH -Power_Protection:TBU-CA-050-300-WH -Power_Protection:TBU-CA-050-500-WH -Power_Protection:TBU-CA-065-050-WH -Power_Protection:TBU-CA-065-100-WH -Power_Protection:TBU-CA-065-200-WH -Power_Protection:TBU-CA-065-300-WH -Power_Protection:TBU-CA-065-500-WH -Power_Protection:TBU-CA-085-050-WH -Power_Protection:TBU-CA-085-100-WH -Power_Protection:TBU-CA-085-200-WH -Power_Protection:TBU-CA-085-300-WH -Power_Protection:TBU-CA-085-500-WH +Power_Protection:TBU-CA025-050-WH +Power_Protection:TBU-CA025-100-WH +Power_Protection:TBU-CA025-200-WH +Power_Protection:TBU-CA025-300-WH +Power_Protection:TBU-CA025-500-WH +Power_Protection:TBU-CA040-050-WH +Power_Protection:TBU-CA040-100-WH +Power_Protection:TBU-CA040-200-WH +Power_Protection:TBU-CA040-300-WH +Power_Protection:TBU-CA040-500-WH +Power_Protection:TBU-CA050-050-WH +Power_Protection:TBU-CA050-100-WH +Power_Protection:TBU-CA050-200-WH +Power_Protection:TBU-CA050-300-WH +Power_Protection:TBU-CA050-500-WH +Power_Protection:TBU-CA065-050-WH +Power_Protection:TBU-CA065-100-WH +Power_Protection:TBU-CA065-200-WH +Power_Protection:TBU-CA065-300-WH +Power_Protection:TBU-CA065-500-WH +Power_Protection:TBU-CA085-050-WH +Power_Protection:TBU-CA085-100-WH +Power_Protection:TBU-CA085-200-WH +Power_Protection:TBU-CA085-300-WH +Power_Protection:TBU-CA085-500-WH Power_Protection:TPD1E05U06DPY Power_Protection:TPD1E05U06DYA Power_Protection:TPD2E2U06DCK @@ -15267,6 +16009,8 @@ Power_Supervisor:MCP130-xxxFxTO Power_Supervisor:MCP130-xxxHxTO Power_Supervisor:MCP130-xxxxSN Power_Supervisor:MCP130-xxxxTT +Power_Supervisor:MIC2779H-xxM5 +Power_Supervisor:MIC2779L-xxM5 Power_Supervisor:MIC811JUY Power_Supervisor:MIC811LUY Power_Supervisor:MIC811MUY @@ -15288,9 +16032,571 @@ Power_Supervisor:TLV810EA29DBZ Power_Supervisor:TPS3430WDRC Power_Supervisor:TPS3702 Power_Supervisor:TPS3808DBV +Power_Supervisor:TPS3823-xxDBV Power_Supervisor:TPS3831 Power_Supervisor:TPS3839DBZ Power_Supervisor:TPS3839DQN +RF:0900PC15J0013 +RF:ADC-10-1R +RF:ADCH-80 +RF:ADCH-80A +RF:ADL5904 +RF:ADP-2-1W +RF:AMK-2-13 +RF:AX5043 +RF:CC1000 +RF:CC1200 +RF:CC2500 +RF:DC4759J5020AHF-1 +RF:DC4759J5020AHF-2 +RF:DW1000 +RF:F113 +RF:F115 +RF:F117 +RF:HMC394LP4 +RF:HMC431 +RF:LAT-3 +RF:LRPS-2-1 +RF:LTC5507ES6 +RF:MAADSS0008 +RF:MAAVSS0004 +RF:MC12080 +RF:MC12093D +RF:MICRF112YMM +RF:MICRF220AYQS +RF:MRF89XA +RF:NRF24L01 +RF:NRF24L01_Breakout +RF:PAT1220-C-0DB +RF:PAT1220-C-10DB +RF:PAT1220-C-1DB +RF:PAT1220-C-2DB +RF:PAT1220-C-3DB +RF:PAT1220-C-4DB +RF:PAT1220-C-5DB +RF:PAT1220-C-6DB +RF:PAT1220-C-7DB +RF:PAT1220-C-8DB +RF:PAT1220-C-9DB +RF:PD4859J5050S2HF +RF:RMK-3-451 +RF:RMK-5-51 +RF:SE5004L +RF:SX1231IMLTRT +RF:SX1261IMLTRT +RF:SX1262IMLTRT +RF:SX1272 +RF:SX1273 +RF:SX1276 +RF:SX1277 +RF:SX1278 +RF:SX1279 +RF:SYPD-1 +RF:SYPD-2 +RF:SYPD-52 +RF:Si4012-C1001xT +RF:Si4460 +RF:Si4461 +RF:Si4463 +RF:Si4464 +RF:TCP-2-10X +RF:nRF24L01P +RF_AM_FM:LA1185 +RF_AM_FM:MCS3142 +RF_AM_FM:SA605D +RF_AM_FM:SA605DK +RF_AM_FM:SA636DK +RF_AM_FM:Si4362 +RF_AM_FM:Si4730-D60-GU +RF_AM_FM:Si4731-D60-GU +RF_AM_FM:Si4734-D60-GU +RF_AM_FM:Si4735-D60-GU +RF_AM_FM:ZETA-433-SO +RF_AM_FM:ZETA-868-SO +RF_AM_FM:ZETA-915-SO +RF_Amplifier:AD8313xRM +RF_Amplifier:ADL5541 +RF_Amplifier:ADL5542 +RF_Amplifier:ADL5610 +RF_Amplifier:BGA2800 +RF_Amplifier:BGA2801 +RF_Amplifier:BGA2803 +RF_Amplifier:BGA2815 +RF_Amplifier:BGA2817 +RF_Amplifier:BGA2818 +RF_Amplifier:BGA2850 +RF_Amplifier:BGA2851 +RF_Amplifier:BGA2865 +RF_Amplifier:BGA2866 +RF_Amplifier:BGA2867 +RF_Amplifier:BGA2869 +RF_Amplifier:BGA2870 +RF_Amplifier:BGA2874 +RF_Amplifier:CMX901 +RF_Amplifier:GALI-1 +RF_Amplifier:GALI-19 +RF_Amplifier:GALI-2 +RF_Amplifier:GALI-21 +RF_Amplifier:GALI-24 +RF_Amplifier:GALI-29 +RF_Amplifier:GALI-3 +RF_Amplifier:GALI-33 +RF_Amplifier:GALI-39 +RF_Amplifier:GALI-4 +RF_Amplifier:GALI-49 +RF_Amplifier:GALI-4F +RF_Amplifier:GALI-5 +RF_Amplifier:GALI-51 +RF_Amplifier:GALI-51F +RF_Amplifier:GALI-52 +RF_Amplifier:GALI-55 +RF_Amplifier:GALI-59 +RF_Amplifier:GALI-5F +RF_Amplifier:GALI-6 +RF_Amplifier:GALI-6F +RF_Amplifier:GALI-74 +RF_Amplifier:GALI-84 +RF_Amplifier:GALI-S66 +RF_Amplifier:GVA-123 +RF_Amplifier:GVA-60 +RF_Amplifier:GVA-62 +RF_Amplifier:GVA-63 +RF_Amplifier:GVA-81 +RF_Amplifier:GVA-82 +RF_Amplifier:GVA-83 +RF_Amplifier:GVA-84 +RF_Amplifier:GVA-93 +RF_Amplifier:HMC1099PM5E +RF_Amplifier:HMC8500PM5E +RF_Amplifier:MAX2679 +RF_Amplifier:MAX2679B +RF_Amplifier:MMZ09332BT1 +RF_Amplifier:PGA-102 +RF_Amplifier:PGA-1021 +RF_Amplifier:PGA-103 +RF_Amplifier:PGA-105 +RF_Amplifier:PGA-106-75 +RF_Amplifier:PGA-106R-75 +RF_Amplifier:PGA-122-75 +RF_Amplifier:PGA-32-75 +RF_Amplifier:PHA-1 +RF_Amplifier:PHA-101 +RF_Amplifier:PHA-13HLN +RF_Amplifier:PHA-13LN +RF_Amplifier:PHA-1H +RF_Amplifier:PHA-23HLN +RF_Amplifier:PHA-23LN +RF_Amplifier:QPL9547 +RF_Amplifier:SGL0622Z +RF_Amplifier:SKY65404 +RF_Amplifier:SPF5189Z +RF_Amplifier:TRF37A73 +RF_Bluetooth:BL652 +RF_Bluetooth:BM78SPPS5MC2 +RF_Bluetooth:BM78SPPS5NC2 +RF_Bluetooth:BTM112 +RF_Bluetooth:BTM222 +RF_Bluetooth:MOD-nRF8001 +RF_Bluetooth:Microchip_BM83 +RF_Bluetooth:RFD77101 +RF_Bluetooth:RN42 +RF_Bluetooth:RN42N +RF_Bluetooth:RN4871 +RF_Bluetooth:SPBTLE-RF +RF_Bluetooth:SPBTLE-RF0 +RF_Bluetooth:nRF8001 +RF_Filter:B3715 +RF_Filter:BFCN-1445 +RF_Filter:BFCN-1525 +RF_Filter:BFCN-152W-75 +RF_Filter:BFCN-1560 +RF_Filter:BFCN-1575 +RF_Filter:BFCN-1690 +RF_Filter:BFCN-1840 +RF_Filter:BFCN-1855 +RF_Filter:BFCN-1860 +RF_Filter:BFCN-1900 +RF_Filter:BFCN-1945 +RF_Filter:BFCN-2275 +RF_Filter:BFCN-2360 +RF_Filter:BFCN-2435 +RF_Filter:BFCN-2450 +RF_Filter:BFCN-2500 +RF_Filter:BFCN-2555 +RF_Filter:BFCN-2700 +RF_Filter:BFCN-2840 +RF_Filter:BFCN-2850 +RF_Filter:BFCN-2900 +RF_Filter:BFCN-2910 +RF_Filter:BFCN-2975 +RF_Filter:BFCN-3010 +RF_Filter:BFCN-3085 +RF_Filter:BFCN-3085A +RF_Filter:BFCN-3115 +RF_Filter:BFCN-3600 +RF_Filter:BFCN-3700 +RF_Filter:BFCN-4100 +RF_Filter:BFCN-4440 +RF_Filter:BFCN-4800 +RF_Filter:BFCN-5100 +RF_Filter:BFCN-5200 +RF_Filter:BFCN-5540 +RF_Filter:BFCN-5750 +RF_Filter:BFCN-7200 +RF_Filter:BFCN-7331 +RF_Filter:BFCN-7350 +RF_Filter:BFCN-7500 +RF_Filter:BFCN-7700 +RF_Filter:BFCN-7900 +RF_Filter:BFCN-8000 +RF_Filter:BFCN-8350 +RF_Filter:BFCN-8450 +RF_Filter:BFCN-8650 +RF_Filter:BPF-A355 +RF_Filter:HFCN-1000 +RF_Filter:HFCN-1080 +RF_Filter:HFCN-1100 +RF_Filter:HFCN-1150 +RF_Filter:HFCN-1200 +RF_Filter:HFCN-1200D +RF_Filter:HFCN-1300 +RF_Filter:HFCN-1300D +RF_Filter:HFCN-1320 +RF_Filter:HFCN-1320D +RF_Filter:HFCN-1322 +RF_Filter:HFCN-1500 +RF_Filter:HFCN-1500D +RF_Filter:HFCN-1600 +RF_Filter:HFCN-1600D +RF_Filter:HFCN-1760 +RF_Filter:HFCN-1810 +RF_Filter:HFCN-1810D +RF_Filter:HFCN-1910 +RF_Filter:HFCN-1910D +RF_Filter:HFCN-2000 +RF_Filter:HFCN-2100 +RF_Filter:HFCN-2100D +RF_Filter:HFCN-2275 +RF_Filter:HFCN-2700 +RF_Filter:HFCN-2700A +RF_Filter:HFCN-2700AD +RF_Filter:HFCN-3100 +RF_Filter:HFCN-3100D +RF_Filter:HFCN-3500 +RF_Filter:HFCN-3500D +RF_Filter:HFCN-3800 +RF_Filter:HFCN-3800D +RF_Filter:HFCN-440 +RF_Filter:HFCN-4400 +RF_Filter:HFCN-4400D +RF_Filter:HFCN-4600 +RF_Filter:HFCN-5050 +RF_Filter:HFCN-5500 +RF_Filter:HFCN-5500D +RF_Filter:HFCN-6010 +RF_Filter:HFCN-650 +RF_Filter:HFCN-650D +RF_Filter:HFCN-672 +RF_Filter:HFCN-7150 +RF_Filter:HFCN-740 +RF_Filter:HFCN-740D +RF_Filter:HFCN-7971 +RF_Filter:HFCN-8400 +RF_Filter:HFCN-8400D +RF_Filter:HFCN-880 +RF_Filter:HFCN-880D +RF_Filter:HFCN-9700 +RF_Filter:LFCN-1000 +RF_Filter:LFCN-1000D +RF_Filter:LFCN-105 +RF_Filter:LFCN-113 +RF_Filter:LFCN-120 +RF_Filter:LFCN-1200 +RF_Filter:LFCN-1200D +RF_Filter:LFCN-123 +RF_Filter:LFCN-1282 +RF_Filter:LFCN-1325 +RF_Filter:LFCN-1400 +RF_Filter:LFCN-1400D +RF_Filter:LFCN-1450 +RF_Filter:LFCN-1500 +RF_Filter:LFCN-1500D +RF_Filter:LFCN-1525 +RF_Filter:LFCN-1525D +RF_Filter:LFCN-1575 +RF_Filter:LFCN-1575D +RF_Filter:LFCN-160 +RF_Filter:LFCN-1700 +RF_Filter:LFCN-1700D +RF_Filter:LFCN-180 +RF_Filter:LFCN-1800 +RF_Filter:LFCN-1800D +RF_Filter:LFCN-190 +RF_Filter:LFCN-2000 +RF_Filter:LFCN-2000D +RF_Filter:LFCN-225 +RF_Filter:LFCN-2250 +RF_Filter:LFCN-2250D +RF_Filter:LFCN-225D +RF_Filter:LFCN-2290 +RF_Filter:LFCN-2400 +RF_Filter:LFCN-2400D +RF_Filter:LFCN-2500 +RF_Filter:LFCN-2500D +RF_Filter:LFCN-2600 +RF_Filter:LFCN-2600D +RF_Filter:LFCN-2750 +RF_Filter:LFCN-2750D +RF_Filter:LFCN-2850 +RF_Filter:LFCN-2850D +RF_Filter:LFCN-3000 +RF_Filter:LFCN-3000D +RF_Filter:LFCN-320 +RF_Filter:LFCN-320D +RF_Filter:LFCN-3400 +RF_Filter:LFCN-3400D +RF_Filter:LFCN-3800 +RF_Filter:LFCN-3800D +RF_Filter:LFCN-400 +RF_Filter:LFCN-400D +RF_Filter:LFCN-4400 +RF_Filter:LFCN-4400D +RF_Filter:LFCN-490 +RF_Filter:LFCN-490D +RF_Filter:LFCN-5000 +RF_Filter:LFCN-5000D +RF_Filter:LFCN-530 +RF_Filter:LFCN-530D +RF_Filter:LFCN-5500 +RF_Filter:LFCN-5500D +RF_Filter:LFCN-575 +RF_Filter:LFCN-575D +RF_Filter:LFCN-5850 +RF_Filter:LFCN-5850D +RF_Filter:LFCN-6000 +RF_Filter:LFCN-6000D +RF_Filter:LFCN-630 +RF_Filter:LFCN-630D +RF_Filter:LFCN-6400 +RF_Filter:LFCN-6400D +RF_Filter:LFCN-6700 +RF_Filter:LFCN-6700D +RF_Filter:LFCN-7200 +RF_Filter:LFCN-7200D +RF_Filter:LFCN-722 +RF_Filter:LFCN-80 +RF_Filter:LFCN-800 +RF_Filter:LFCN-800D +RF_Filter:LFCN-8400 +RF_Filter:LFCN-8440 +RF_Filter:LFCN-900 +RF_Filter:LFCN-900D +RF_Filter:LFCN-9170 +RF_Filter:LFCN-95 +RF_Filter:LPF-B0R3 +RF_Filter:RBP-280 +RF_Filter:RBPF-246 +RF_Filter:RLP-30 +RF_Filter:SCHF-31 +RF_Filter:STA0232A +RF_Filter:STA1090EC +RF_Filter:SXBP-100 +RF_Filter:SXBP-140 +RF_Filter:SXBP-202 +RF_Filter:SXBP-27R5 +RF_Filter:TA0232A +RF_Filter:TA0970B +RF_GPS:L70-R +RF_GPS:L80-R +RF_GPS:LEA-M8F +RF_GPS:LEA-M8S +RF_GPS:LEA-M8T +RF_GPS:MAX-8C +RF_GPS:MAX-8Q +RF_GPS:MAX-M10S +RF_GPS:MAX-M8C +RF_GPS:MAX-M8Q +RF_GPS:MAX-M8W +RF_GPS:MIA-M10Q +RF_GPS:NEO-8Q +RF_GPS:NEO-M8M +RF_GPS:NEO-M8N +RF_GPS:NEO-M8P +RF_GPS:NEO-M8Q +RF_GPS:NEO-M8T +RF_GPS:NEO-M9N +RF_GPS:RXM-GPS-FM +RF_GPS:RXM-GPS-RM +RF_GPS:SAM-M8Q +RF_GPS:SIM28ML +RF_GPS:ZED-F9P +RF_GPS:ZOE-M8G +RF_GPS:ZOE-M8Q +RF_GSM:BC66 +RF_GSM:BC95 +RF_GSM:BG95-M1 +RF_GSM:BG95-M2 +RF_GSM:BG95-M3 +RF_GSM:BG95-M4 +RF_GSM:BG95-M5 +RF_GSM:BG95-M6 +RF_GSM:BG95-M8 +RF_GSM:BG95-MF +RF_GSM:LENA-R8001 +RF_GSM:M95 +RF_GSM:SARA-U201 +RF_GSM:SARA-U260 +RF_GSM:SARA-U270 +RF_GSM:SARA-U280 +RF_GSM:SE150A4 +RF_GSM:SIM7020C +RF_GSM:SIM7020E +RF_GSM:SIM800C +RF_GSM:SIM900 +RF_GSM:UL865 +RF_Mixer:AD831AP +RF_Mixer:ADE-6 +RF_Mixer:ADEX-10 +RF_Mixer:ADL5801 +RF_Mixer:ADL5802 +RF_Mixer:HMC213A +RF_Mixer:HMC213B +RF_Mixer:LT5560 +RF_Module:AST50147-xx +RF_Module:ATSAMR21G18-MR210UA_NoRFPads +RF_Module:AX-SIP-SFEU +RF_Module:AX-SIP-SFEU-API +RF_Module:Ai-Thinker-Ra-01 +RF_Module:Ai-Thinker-Ra-02 +RF_Module:CMWX1ZZABZ-078 +RF_Module:CMWX1ZZABZ-091 +RF_Module:D52MxxM8 +RF_Module:DCTR-52DA +RF_Module:DCTR-52DAT +RF_Module:DWM1000 +RF_Module:DWM1001 +RF_Module:DWM3000 +RF_Module:E18-MS1-PCB +RF_Module:E73-2G4M04S-52810 +RF_Module:E73-2G4M04S-52832 +RF_Module:ESP-07 +RF_Module:ESP-12E +RF_Module:ESP-12F +RF_Module:ESP-WROOM-02 +RF_Module:ESP32-C3-DevKitM-1 +RF_Module:ESP32-C3-WROOM-02 +RF_Module:ESP32-C3-WROOM-02U +RF_Module:ESP32-C6-MINI-1 +RF_Module:ESP32-S2-WROVER +RF_Module:ESP32-S2-WROVER-I +RF_Module:ESP32-S3-MINI-1 +RF_Module:ESP32-S3-MINI-1U +RF_Module:ESP32-S3-WROOM-1 +RF_Module:ESP32-S3-WROOM-2 +RF_Module:ESP32-WROOM-32 +RF_Module:ESP32-WROOM-32D +RF_Module:ESP32-WROOM-32E +RF_Module:ESP32-WROOM-32E-R2 +RF_Module:ESP32-WROOM-32U +RF_Module:ESP32-WROOM-32UE +RF_Module:ESP32-WROOM-32UE-R2 +RF_Module:HT-CT62 +RF_Module:Jadak_Thingmagic_M6e-Nano +RF_Module:MDBT42Q-512K +RF_Module:MDBT50Q-1MV2 +RF_Module:MDBT50Q-512K +RF_Module:MDBT50Q-P1MV2 +RF_Module:MDBT50Q-P512K +RF_Module:MDBT50Q-U1MV2 +RF_Module:MDBT50Q-U512K +RF_Module:MM002 +RF_Module:Particle_P1 +RF_Module:RAK3172-xx-43-SM-xI +RF_Module:RAK3172-xx-47-SM-xI +RF_Module:RAK3172-xx-8-SM-xI +RF_Module:RAK3172-xx-9-SM-xI +RF_Module:RAK4200 +RF_Module:RAK811-HF-AS923 +RF_Module:RAK811-HF-AU915 +RF_Module:RAK811-HF-EU868 +RF_Module:RAK811-HF-IN865 +RF_Module:RAK811-HF-KR920 +RF_Module:RAK811-HF-US915 +RF_Module:RAK811-LF-CN470 +RF_Module:RAK811-LF-EU433 +RF_Module:RFM69HCW +RF_Module:RFM69HW +RF_Module:RFM69W +RF_Module:RFM95W-868S2 +RF_Module:RFM95W-915S2 +RF_Module:RFM96W-315S2 +RF_Module:RFM96W-433S2 +RF_Module:RFM97W-868S2 +RF_Module:RFM97W-915S2 +RF_Module:RFM98W-315S2 +RF_Module:RFM98W-433S2 +RF_Module:STM32WB5MMG +RF_Module:TD1205 +RF_Module:TD1208 +RF_Module:TR-52DA +RF_Module:TR-52DAT +RF_Module:TR-72DA +RF_Module:TR-72DAT +RF_Module:WEMOS_C3_mini +RF_Module:WEMOS_D1_mini +RF_Module:iM880A +RF_Module:iM880B +RF_NFC:PN5321A3HN_C1xx +RF_NFC:ST25DV04K-IER8C3 +RF_NFC:ST25DV04K-JFR6D3 +RF_NFC:ST25DV16K-IER8C3 +RF_NFC:ST25DV16K-JFR6D3 +RF_NFC:ST25DV64K-IER8C3 +RF_NFC:ST25DV64K-JFR6D3 +RF_NFC:ST25R3911B-AQF +RF_NFC:ST25R3911B-AQW +RF_RFID:HTRC11001T +RF_RFID:PN5120A0HN1 +RF_Switch:ADG901BCPZ +RF_Switch:ADG901BRMZ +RF_Switch:ADG902BRMZ +RF_Switch:ADG918BCPZ +RF_Switch:ADG918BRM +RF_Switch:ADG919BCPZ +RF_Switch:ADG919BRMZ +RF_Switch:AS179-92LF +RF_Switch:BGS12WN6E6327 +RF_Switch:HMC7992 +RF_Switch:HMC849A +RF_Switch:KSW-2-46 +RF_Switch:KSWA-2-46 +RF_Switch:KSWHA-1-20 +RF_Switch:MASW-007221 +RF_Switch:MASWSS0115 +RF_Switch:MASWSS0136 +RF_Switch:MASWSS0143 +RF_Switch:MASWSS0151 +RF_Switch:MASWSS0166 +RF_Switch:MASWSS0176 +RF_Switch:MASWSS0178 +RF_Switch:MASWSS0179 +RF_Switch:MASWSS0192 +RF_Switch:MSW-2-20 +RF_Switch:MSW2-50 +RF_Switch:MSWA-2-20 +RF_Switch:MSWA2-50 +RF_Switch:SKY13380-350LF +RF_Switch:SKY13575-639LF +RF_WiFi:HF-A11-SMT +RF_WiFi:USR-C322 +RF_ZigBee:AT86RF233-Z +RF_ZigBee:CC2520 +RF_ZigBee:MC13192 +RF_ZigBee:MW-R-DP-W +RF_ZigBee:MW-R-WX +RF_ZigBee:TWE-L-DP-W +RF_ZigBee:TWE-L-WX +RF_ZigBee:XBee_SMT Reference_Current:LM134H Reference_Current:LM234Z-3 Reference_Current:LM234Z-6 @@ -16078,6 +17384,13 @@ Regulator_Linear:AZ1117-2.85 Regulator_Linear:AZ1117-3.3 Regulator_Linear:AZ1117-5.0 Regulator_Linear:AZ1117-ADJ +Regulator_Linear:AZ1117CH2-1.2 +Regulator_Linear:AZ1117CH2-1.5 +Regulator_Linear:AZ1117CH2-1.8 +Regulator_Linear:AZ1117CH2-2.5 +Regulator_Linear:AZ1117CH2-3.3 +Regulator_Linear:AZ1117CH2-5.0 +Regulator_Linear:AZ1117CH2-ADJ Regulator_Linear:AZ1117D-ADJ Regulator_Linear:AZ1117H-ADJ Regulator_Linear:AZ1117R-ADJ @@ -16229,6 +17542,10 @@ Regulator_Linear:LD1117S18TR_SOT223 Regulator_Linear:LD1117S25TR_SOT223 Regulator_Linear:LD1117S33TR_SOT223 Regulator_Linear:LD1117S50TR_SOT223 +Regulator_Linear:LD1117V +Regulator_Linear:LD1117V18 +Regulator_Linear:LD1117V33 +Regulator_Linear:LD1117V50 Regulator_Linear:LD39015M08R Regulator_Linear:LD39015M10R Regulator_Linear:LD39015M125R @@ -16281,6 +17598,9 @@ Regulator_Linear:LDK130PU29R_DFN6 Regulator_Linear:LDK130PU30R_DFN6 Regulator_Linear:LDK130PU32R_DFN6 Regulator_Linear:LDK130PU33R_DFN6 +Regulator_Linear:LDO40LPU33RY +Regulator_Linear:LDO40LPU50RY +Regulator_Linear:LDO40LPURY Regulator_Linear:LF120_TO220 Regulator_Linear:LF120_TO252 Regulator_Linear:LF15_TO220 @@ -16619,6 +17939,7 @@ Regulator_Linear:LT1963EQ Regulator_Linear:LT1964-5 Regulator_Linear:LT1964-BYP Regulator_Linear:LT1964-SD +Regulator_Linear:LT3008xDC-3.3 Regulator_Linear:LT3010 Regulator_Linear:LT3010-5 Regulator_Linear:LT3011xDD @@ -16911,6 +18232,23 @@ Regulator_Linear:MIC5356-MMYML Regulator_Linear:MIC5356-S4YMME Regulator_Linear:MIC5356-SCYMME Regulator_Linear:MIC5356-SGYMME +Regulator_Linear:MIC5365-1.0YC5 +Regulator_Linear:MIC5365-1.2YC5 +Regulator_Linear:MIC5365-1.2YD5 +Regulator_Linear:MIC5365-1.3YC5 +Regulator_Linear:MIC5365-1.5YC5 +Regulator_Linear:MIC5365-1.8YC5 +Regulator_Linear:MIC5365-1.8YD5 +Regulator_Linear:MIC5365-2.0YC5 +Regulator_Linear:MIC5365-2.5YC5 +Regulator_Linear:MIC5365-2.6YC5 +Regulator_Linear:MIC5365-2.7YC5 +Regulator_Linear:MIC5365-2.85YC5 +Regulator_Linear:MIC5365-2.85YD5 +Regulator_Linear:MIC5365-2.8YC5 +Regulator_Linear:MIC5365-2.8YD5 +Regulator_Linear:MIC5365-2.9YC5 +Regulator_Linear:MIC5365-3.0YC5 Regulator_Linear:MIC5365-3.3YC5 Regulator_Linear:MIC5365-3.3YD5 Regulator_Linear:MIC5366-3.3YC5 @@ -17164,6 +18502,7 @@ Regulator_Linear:TLV733285PDBV Regulator_Linear:TLV73328PDBV Regulator_Linear:TLV73330PDBV Regulator_Linear:TLV73333PDBV +Regulator_Linear:TLV75101PDSQ Regulator_Linear:TLV75509PDBV Regulator_Linear:TLV75509PDRV Regulator_Linear:TLV75510PDBV @@ -17306,6 +18645,11 @@ Regulator_Linear:TPS76928 Regulator_Linear:TPS76930 Regulator_Linear:TPS76933 Regulator_Linear:TPS76950 +Regulator_Linear:TPS77012PDBV +Regulator_Linear:TPS77018PDBV +Regulator_Linear:TPS77025PDBV +Regulator_Linear:TPS77028PDBV +Regulator_Linear:TPS77033PDBV Regulator_Linear:TPS77701_HTSSOP20 Regulator_Linear:TPS77701_SO8 Regulator_Linear:TPS77715_HTSSOP20 @@ -17362,15 +18706,20 @@ Regulator_Linear:TPS7A0530PDBZ Regulator_Linear:TPS7A0531PDBV Regulator_Linear:TPS7A0533PDBV Regulator_Linear:TPS7A0533PDBZ +Regulator_Linear:TPS7A20xxxDBV Regulator_Linear:TPS7A20xxxDQN Regulator_Linear:TPS7A3301RGW Regulator_Linear:TPS7A39 Regulator_Linear:TPS7A4101DGN +Regulator_Linear:TPS7A4501KTT +Regulator_Linear:TPS7A45xxKTT Regulator_Linear:TPS7A4701xRGW Regulator_Linear:TPS7A7001DDA Regulator_Linear:TPS7A7200RGW Regulator_Linear:TPS7A90 Regulator_Linear:TPS7A91 +Regulator_Linear:TPS7B8133DRV +Regulator_Linear:TPS7B8150DRV Regulator_Linear:UA78M05QDCYRQ1 Regulator_Linear:UA78M08QDCYRQ1 Regulator_Linear:UA78M10QDCYRQ1 @@ -17406,6 +18755,9 @@ Regulator_SwitchedCapacitor:LTC1503xS8-1.8 Regulator_SwitchedCapacitor:LTC1503xS8-2 Regulator_SwitchedCapacitor:LTC1751 Regulator_SwitchedCapacitor:LTC1754 +Regulator_SwitchedCapacitor:LTC3221EDC +Regulator_SwitchedCapacitor:LTC3221EDC-3.3 +Regulator_SwitchedCapacitor:LTC3221EDC-5 Regulator_SwitchedCapacitor:LTC3260xDE Regulator_SwitchedCapacitor:LTC3260xMSE Regulator_SwitchedCapacitor:LTC660 @@ -17464,12 +18816,19 @@ Regulator_Switching:ADuM6000 Regulator_Switching:AOZ1280CI Regulator_Switching:AOZ1282CI Regulator_Switching:AOZ1282CI-1 +Regulator_Switching:AOZ1284PI Regulator_Switching:AOZ6663DI Regulator_Switching:AOZ6663DI-01 Regulator_Switching:AP3012 Regulator_Switching:AP3211K Regulator_Switching:AP3402 Regulator_Switching:AP3441SHE +Regulator_Switching:AP61200Z6 +Regulator_Switching:AP61201Z6 +Regulator_Switching:AP61202Z6 +Regulator_Switching:AP61203Z6 +Regulator_Switching:AP61300Z6 +Regulator_Switching:AP61302Z6 Regulator_Switching:AP62150WU Regulator_Switching:AP62150Z6 Regulator_Switching:AP62250WU @@ -17497,6 +18856,7 @@ Regulator_Switching:APE1707S-12-HF Regulator_Switching:APE1707S-33-HF Regulator_Switching:APE1707S-50-HF Regulator_Switching:APE1707S-HF +Regulator_Switching:BD8314NUV Regulator_Switching:BD9001F Regulator_Switching:BD9778F Regulator_Switching:BD9778HFP @@ -17512,6 +18872,8 @@ Regulator_Switching:CRE1S1212SC Regulator_Switching:CRE1S2405SC Regulator_Switching:CRE1S2412SC Regulator_Switching:DIO6970 +Regulator_Switching:EA3036CQB +Regulator_Switching:EA3058QD Regulator_Switching:FSBH0170 Regulator_Switching:FSBH0170A Regulator_Switching:FSBH0170W @@ -17566,6 +18928,9 @@ Regulator_Switching:L4962E-A Regulator_Switching:L4962EH-A Regulator_Switching:L5973D Regulator_Switching:L7980A +Regulator_Switching:L7983PU33R +Regulator_Switching:L7983PU50R +Regulator_Switching:L7983PUR Regulator_Switching:LD7575 Regulator_Switching:LGS5116B Regulator_Switching:LGS5145 @@ -17716,6 +19081,8 @@ Regulator_Switching:LM5022MM Regulator_Switching:LM5088-1 Regulator_Switching:LM5088-2 Regulator_Switching:LM5118MH +Regulator_Switching:LM51561HPWP +Regulator_Switching:LM5156HPWP Regulator_Switching:LM5161PWP Regulator_Switching:LM5164DDA Regulator_Switching:LM5165 @@ -17728,6 +19095,9 @@ Regulator_Switching:LM5175PWP Regulator_Switching:LM5175RHF Regulator_Switching:LM5176PWP Regulator_Switching:LM5176RHF +Regulator_Switching:LM76002 +Regulator_Switching:LM76003 +Regulator_Switching:LM76005 Regulator_Switching:LMR10510XMF Regulator_Switching:LMR10510YMF Regulator_Switching:LMR10510YSD @@ -17752,17 +19122,6 @@ Regulator_Switching:LMR62014XMF Regulator_Switching:LMR62421XMF Regulator_Switching:LMR62421XSD Regulator_Switching:LMR64010XMF -Regulator_Switching:LMZ13608 -Regulator_Switching:LMZ22003TZ -Regulator_Switching:LMZ22005TZ -Regulator_Switching:LMZ23603TZ -Regulator_Switching:LMZ23605TZ -Regulator_Switching:LMZM23600 -Regulator_Switching:LMZM23600V3 -Regulator_Switching:LMZM23600V5 -Regulator_Switching:LMZM23601 -Regulator_Switching:LMZM23601V3 -Regulator_Switching:LMZM23601V5 Regulator_Switching:LNK302D Regulator_Switching:LNK302G Regulator_Switching:LNK302P @@ -17957,16 +19316,6 @@ Regulator_Switching:LTC3638xMSE Regulator_Switching:LTC3639xMSE Regulator_Switching:LTC3886 Regulator_Switching:LTC7138xMSE -Regulator_Switching:LTM4626 -Regulator_Switching:LTM4637xV -Regulator_Switching:LTM4637xY -Regulator_Switching:LTM4638 -Regulator_Switching:LTM4657 -Regulator_Switching:LTM4668 -Regulator_Switching:LTM4668A -Regulator_Switching:LTM4671 -Regulator_Switching:LTM8049 -Regulator_Switching:LTM8063 Regulator_Switching:LV2862XDDC Regulator_Switching:LV2862YDDC Regulator_Switching:MAX15062A @@ -17975,6 +19324,9 @@ Regulator_Switching:MAX15062C Regulator_Switching:MAX1522 Regulator_Switching:MAX1523 Regulator_Switching:MAX1524 +Regulator_Switching:MAX15462A +Regulator_Switching:MAX15462B +Regulator_Switching:MAX15462C Regulator_Switching:MAX17501AxTB Regulator_Switching:MAX17501BxTB Regulator_Switching:MAX17501ExTB @@ -17984,6 +19336,9 @@ Regulator_Switching:MAX17501HxTB Regulator_Switching:MAX17572 Regulator_Switching:MAX17574 Regulator_Switching:MAX17620ATA +Regulator_Switching:MAX17634AATP +Regulator_Switching:MAX17634BATP +Regulator_Switching:MAX17634CATP Regulator_Switching:MAX1771xSA Regulator_Switching:MAX5035AUPA Regulator_Switching:MAX5035AUSA @@ -18047,8 +19402,6 @@ Regulator_Switching:MP2303ADN Regulator_Switching:MP2303ADP Regulator_Switching:MPM3550EGLE Regulator_Switching:MT3608 -Regulator_Switching:MUN12AD01-SH -Regulator_Switching:MUN12AD03-SH Regulator_Switching:NBM5100A Regulator_Switching:NBM7100A Regulator_Switching:NCP1070P065 @@ -18157,51 +19510,10 @@ Regulator_Switching:PAM2306AYPCB Regulator_Switching:PAM2306AYPKB Regulator_Switching:PAM2306AYPKE Regulator_Switching:PAM2306DYPAA -Regulator_Switching:R-781.5-0.5 -Regulator_Switching:R-781.8-0.5 -Regulator_Switching:R-781.8-1.0 -Regulator_Switching:R-7812-0.5 -Regulator_Switching:R-7815-0.5 -Regulator_Switching:R-782.5-0.5 -Regulator_Switching:R-782.5-1.0 -Regulator_Switching:R-783.3-0.5 -Regulator_Switching:R-783.3-1.0 -Regulator_Switching:R-785.0-0.5 -Regulator_Switching:R-785.0-1.0 -Regulator_Switching:R-786.5-0.5 -Regulator_Switching:R-78B1.2-2.0 -Regulator_Switching:R-78B1.5-2.0 -Regulator_Switching:R-78B1.8-2.0 -Regulator_Switching:R-78B12-2.0 -Regulator_Switching:R-78B15-2.0 -Regulator_Switching:R-78B2.5-2.0 -Regulator_Switching:R-78B3.3-2.0 -Regulator_Switching:R-78B5.0-2.0 -Regulator_Switching:R-78B9.0-2.0 -Regulator_Switching:R-78C1.8-1.0 -Regulator_Switching:R-78C12-1.0 -Regulator_Switching:R-78C15-1.0 -Regulator_Switching:R-78C3.3-1.0 -Regulator_Switching:R-78C5.0-1.0 -Regulator_Switching:R-78C9.0-1.0 -Regulator_Switching:R-78E12-0.5 -Regulator_Switching:R-78E15-0.5 -Regulator_Switching:R-78E3.3-0.5 -Regulator_Switching:R-78E3.3-1.0 -Regulator_Switching:R-78E5.0-0.5 -Regulator_Switching:R-78E5.0-1.0 -Regulator_Switching:R-78E9.0-0.5 -Regulator_Switching:R-78HB12-0.5 -Regulator_Switching:R-78HB15-0.5 -Regulator_Switching:R-78HB24-0.3 -Regulator_Switching:R-78HB3.3-0.5 -Regulator_Switching:R-78HB5.0-0.5 -Regulator_Switching:R-78HB6.5-0.5 -Regulator_Switching:R-78HB9.0-0.5 -Regulator_Switching:R-78S3.3-0.1 Regulator_Switching:SC33063AD Regulator_Switching:SC34063AP Regulator_Switching:SC4503TSK +Regulator_Switching:SCT2650STE Regulator_Switching:SIC431A Regulator_Switching:SIC431B Regulator_Switching:SIC431C @@ -18241,6 +19553,7 @@ Regulator_Switching:TL497A Regulator_Switching:TL5001 Regulator_Switching:TL5001A Regulator_Switching:TLV61046ADB +Regulator_Switching:TLV61047DDC Regulator_Switching:TLV61070ADBV Regulator_Switching:TLV61225DC Regulator_Switching:TLV62080DSGx @@ -18446,6 +19759,7 @@ Regulator_Switching:TOP271VG Regulator_Switching:TOS06-05SIL Regulator_Switching:TOS06-12SIL Regulator_Switching:TPS51363 +Regulator_Switching:TPS51396A Regulator_Switching:TPS5403 Regulator_Switching:TPS54061DRB Regulator_Switching:TPS54202DDC @@ -18461,6 +19775,7 @@ Regulator_Switching:TPS54336ADDA Regulator_Switching:TPS54340DDA Regulator_Switching:TPS54360DDA Regulator_Switching:TPS54560BDDA +Regulator_Switching:TPS54561 Regulator_Switching:TPS560200 Regulator_Switching:TPS562200 Regulator_Switching:TPS562202 @@ -18468,15 +19783,18 @@ Regulator_Switching:TPS562202S Regulator_Switching:TPS562203 Regulator_Switching:TPS562206 Regulator_Switching:TPS563200 +Regulator_Switching:TPS563201 Regulator_Switching:TPS563202S Regulator_Switching:TPS563203 Regulator_Switching:TPS563206 +Regulator_Switching:TPS563208 Regulator_Switching:TPS563240DDC Regulator_Switching:TPS563300 Regulator_Switching:TPS56339DDC Regulator_Switching:TPS565208 Regulator_Switching:TPS56528DDA Regulator_Switching:TPS568215RNN +Regulator_Switching:TPS568230 Regulator_Switching:TPS61040DBV Regulator_Switching:TPS61040DDC Regulator_Switching:TPS61040DRV @@ -18593,38 +19911,6 @@ Regulator_Switching:TPS63061 Regulator_Switching:TPS63900 Regulator_Switching:TPS65130RGE Regulator_Switching:TPS65131RGE -Regulator_Switching:TPS82130 -Regulator_Switching:TPS82140 -Regulator_Switching:TPS82150 -Regulator_Switching:TSR0.6-48120WI -Regulator_Switching:TSR0.6-48150WI -Regulator_Switching:TSR0.6-48240WI -Regulator_Switching:TSR0.6-4833WI -Regulator_Switching:TSR0.6-4850WI -Regulator_Switching:TSR0.6-4865WI -Regulator_Switching:TSR0.6-4890WI -Regulator_Switching:TSR1-2433E -Regulator_Switching:TSR1-2450E -Regulator_Switching:TSR2-24120N -Regulator_Switching:TSR2-2412N -Regulator_Switching:TSR2-24150N -Regulator_Switching:TSR2-2415N -Regulator_Switching:TSR2-2418N -Regulator_Switching:TSR2-2425N -Regulator_Switching:TSR2-2433N -Regulator_Switching:TSR2-2450N -Regulator_Switching:TSR2-2465N -Regulator_Switching:TSR2-2490N -Regulator_Switching:TSR_1-2412 -Regulator_Switching:TSR_1-24120 -Regulator_Switching:TSR_1-2415 -Regulator_Switching:TSR_1-24150 -Regulator_Switching:TSR_1-2418 -Regulator_Switching:TSR_1-2425 -Regulator_Switching:TSR_1-2433 -Regulator_Switching:TSR_1-2450 -Regulator_Switching:TSR_1-2465 -Regulator_Switching:TSR_1-2490 Regulator_Switching:VIPer22ADIP-E Regulator_Switching:VIPer22AS Regulator_Switching:VIPer25HN @@ -18914,11 +20200,45 @@ Relay_SolidState:AQH3213 Relay_SolidState:AQH3213A Relay_SolidState:AQH3223 Relay_SolidState:AQH3223A +Relay_SolidState:AQY280EH +Relay_SolidState:AQY280EHA +Relay_SolidState:AQY280S +Relay_SolidState:AQY282EH +Relay_SolidState:AQY282EHA +Relay_SolidState:AQY282GS +Relay_SolidState:AQY282S +Relay_SolidState:AQY284EH +Relay_SolidState:AQY284EHA +Relay_SolidState:AQY284S Relay_SolidState:ASSR-1218 Relay_SolidState:BC2213A Relay_SolidState:CPC1002N +Relay_SolidState:CPC1004N +Relay_SolidState:CPC1006N +Relay_SolidState:CPC1008N +Relay_SolidState:CPC1009N +Relay_SolidState:CPC1010N +Relay_SolidState:CPC1014N +Relay_SolidState:CPC1016N Relay_SolidState:CPC1017N +Relay_SolidState:CPC1018N +Relay_SolidState:CPC1019N +Relay_SolidState:CPC1020N +Relay_SolidState:CPC1025N +Relay_SolidState:CPC1030N +Relay_SolidState:CPC1035N +Relay_SolidState:CPC1106N +Relay_SolidState:CPC1114N Relay_SolidState:CPC1117N +Relay_SolidState:CPC1125N +Relay_SolidState:CPC1130N +Relay_SolidState:CPC1135N +Relay_SolidState:CPC1150N +Relay_SolidState:CPC2014N +Relay_SolidState:CPC2017N +Relay_SolidState:CPC2025N +Relay_SolidState:CPC2030N +Relay_SolidState:CPC2125N Relay_SolidState:FOD420 Relay_SolidState:FOD4208 Relay_SolidState:FOD4216 @@ -18981,560 +20301,6 @@ Relay_SolidState:TLP3543 Relay_SolidState:TLP3544 Relay_SolidState:TLP3545 Relay_SolidState:TLP3546 -RF:0900PC15J0013 -RF:ADC-10-1R -RF:ADCH-80 -RF:ADCH-80A -RF:ADL5904 -RF:ADP-2-1W -RF:AMK-2-13 -RF:AX5043 -RF:CC1000 -RF:CC1200 -RF:CC2500 -RF:DC4759J5020AHF-1 -RF:DC4759J5020AHF-2 -RF:DW1000 -RF:F113 -RF:F115 -RF:F117 -RF:HMC394LP4 -RF:HMC431 -RF:LAT-3 -RF:LRPS-2-1 -RF:LTC5507ES6 -RF:MAADSS0008 -RF:MAAVSS0004 -RF:MC12080 -RF:MC12093D -RF:MICRF112YMM -RF:MICRF220AYQS -RF:MRF89XA -RF:NRF24L01 -RF:NRF24L01_Breakout -RF:PAT1220-C-0DB -RF:PAT1220-C-10DB -RF:PAT1220-C-1DB -RF:PAT1220-C-2DB -RF:PAT1220-C-3DB -RF:PAT1220-C-4DB -RF:PAT1220-C-5DB -RF:PAT1220-C-6DB -RF:PAT1220-C-7DB -RF:PAT1220-C-8DB -RF:PAT1220-C-9DB -RF:PD4859J5050S2HF -RF:RMK-3-451 -RF:RMK-5-51 -RF:SE5004L -RF:SX1231IMLTRT -RF:SX1261IMLTRT -RF:SX1262IMLTRT -RF:SX1272 -RF:SX1273 -RF:SX1276 -RF:SX1277 -RF:SX1278 -RF:SX1279 -RF:SYPD-1 -RF:SYPD-2 -RF:SYPD-52 -RF:Si4460 -RF:Si4461 -RF:Si4463 -RF:Si4464 -RF:TCP-2-10X -RF:nRF24L01P -RF_Amplifier:AD8313xRM -RF_Amplifier:ADL5541 -RF_Amplifier:ADL5542 -RF_Amplifier:ADL5610 -RF_Amplifier:BGA2800 -RF_Amplifier:BGA2801 -RF_Amplifier:BGA2803 -RF_Amplifier:BGA2815 -RF_Amplifier:BGA2817 -RF_Amplifier:BGA2818 -RF_Amplifier:BGA2850 -RF_Amplifier:BGA2851 -RF_Amplifier:BGA2865 -RF_Amplifier:BGA2866 -RF_Amplifier:BGA2867 -RF_Amplifier:BGA2869 -RF_Amplifier:BGA2870 -RF_Amplifier:BGA2874 -RF_Amplifier:CMX901 -RF_Amplifier:GALI-1 -RF_Amplifier:GALI-19 -RF_Amplifier:GALI-2 -RF_Amplifier:GALI-21 -RF_Amplifier:GALI-24 -RF_Amplifier:GALI-29 -RF_Amplifier:GALI-3 -RF_Amplifier:GALI-33 -RF_Amplifier:GALI-39 -RF_Amplifier:GALI-4 -RF_Amplifier:GALI-49 -RF_Amplifier:GALI-4F -RF_Amplifier:GALI-5 -RF_Amplifier:GALI-51 -RF_Amplifier:GALI-51F -RF_Amplifier:GALI-52 -RF_Amplifier:GALI-55 -RF_Amplifier:GALI-59 -RF_Amplifier:GALI-5F -RF_Amplifier:GALI-6 -RF_Amplifier:GALI-6F -RF_Amplifier:GALI-74 -RF_Amplifier:GALI-84 -RF_Amplifier:GALI-S66 -RF_Amplifier:GVA-123 -RF_Amplifier:GVA-60 -RF_Amplifier:GVA-62 -RF_Amplifier:GVA-63 -RF_Amplifier:GVA-81 -RF_Amplifier:GVA-82 -RF_Amplifier:GVA-83 -RF_Amplifier:GVA-84 -RF_Amplifier:GVA-93 -RF_Amplifier:HMC1099PM5E -RF_Amplifier:HMC8500PM5E -RF_Amplifier:MAX2679 -RF_Amplifier:MAX2679B -RF_Amplifier:MMZ09332BT1 -RF_Amplifier:PGA-102 -RF_Amplifier:PGA-1021 -RF_Amplifier:PGA-103 -RF_Amplifier:PGA-105 -RF_Amplifier:PGA-106-75 -RF_Amplifier:PGA-106R-75 -RF_Amplifier:PGA-122-75 -RF_Amplifier:PGA-32-75 -RF_Amplifier:PHA-1 -RF_Amplifier:PHA-101 -RF_Amplifier:PHA-13HLN -RF_Amplifier:PHA-13LN -RF_Amplifier:PHA-1H -RF_Amplifier:PHA-23HLN -RF_Amplifier:PHA-23LN -RF_Amplifier:QPL9547 -RF_Amplifier:SGL0622Z -RF_Amplifier:SKY65404 -RF_Amplifier:SPF5189Z -RF_Amplifier:TRF37A73 -RF_AM_FM:LA1185 -RF_AM_FM:MCS3142 -RF_AM_FM:SA605D -RF_AM_FM:SA605DK -RF_AM_FM:SA636DK -RF_AM_FM:Si4362 -RF_AM_FM:Si4730-D60-GU -RF_AM_FM:Si4731-D60-GU -RF_AM_FM:Si4734-D60-GU -RF_AM_FM:Si4735-D60-GU -RF_AM_FM:ZETA-433-SO -RF_AM_FM:ZETA-868-SO -RF_AM_FM:ZETA-915-SO -RF_Bluetooth:BL652 -RF_Bluetooth:BM78SPPS5MC2 -RF_Bluetooth:BM78SPPS5NC2 -RF_Bluetooth:BTM112 -RF_Bluetooth:BTM222 -RF_Bluetooth:MOD-nRF8001 -RF_Bluetooth:Microchip_BM83 -RF_Bluetooth:RFD77101 -RF_Bluetooth:RN42 -RF_Bluetooth:RN42N -RF_Bluetooth:RN4871 -RF_Bluetooth:SPBTLE-RF -RF_Bluetooth:SPBTLE-RF0 -RF_Bluetooth:nRF8001 -RF_Filter:B3715 -RF_Filter:BFCN-1445 -RF_Filter:BFCN-1525 -RF_Filter:BFCN-152W-75 -RF_Filter:BFCN-1560 -RF_Filter:BFCN-1575 -RF_Filter:BFCN-1690 -RF_Filter:BFCN-1840 -RF_Filter:BFCN-1855 -RF_Filter:BFCN-1860 -RF_Filter:BFCN-1900 -RF_Filter:BFCN-1945 -RF_Filter:BFCN-2275 -RF_Filter:BFCN-2360 -RF_Filter:BFCN-2435 -RF_Filter:BFCN-2450 -RF_Filter:BFCN-2500 -RF_Filter:BFCN-2555 -RF_Filter:BFCN-2700 -RF_Filter:BFCN-2840 -RF_Filter:BFCN-2850 -RF_Filter:BFCN-2900 -RF_Filter:BFCN-2910 -RF_Filter:BFCN-2975 -RF_Filter:BFCN-3010 -RF_Filter:BFCN-3085 -RF_Filter:BFCN-3085A -RF_Filter:BFCN-3115 -RF_Filter:BFCN-3600 -RF_Filter:BFCN-3700 -RF_Filter:BFCN-4100 -RF_Filter:BFCN-4440 -RF_Filter:BFCN-4800 -RF_Filter:BFCN-5100 -RF_Filter:BFCN-5200 -RF_Filter:BFCN-5540 -RF_Filter:BFCN-5750 -RF_Filter:BFCN-7200 -RF_Filter:BFCN-7331 -RF_Filter:BFCN-7350 -RF_Filter:BFCN-7500 -RF_Filter:BFCN-7700 -RF_Filter:BFCN-7900 -RF_Filter:BFCN-8000 -RF_Filter:BFCN-8350 -RF_Filter:BFCN-8450 -RF_Filter:BFCN-8650 -RF_Filter:BPF-A355 -RF_Filter:HFCN-1000 -RF_Filter:HFCN-1080 -RF_Filter:HFCN-1100 -RF_Filter:HFCN-1150 -RF_Filter:HFCN-1200 -RF_Filter:HFCN-1200D -RF_Filter:HFCN-1300 -RF_Filter:HFCN-1300D -RF_Filter:HFCN-1320 -RF_Filter:HFCN-1320D -RF_Filter:HFCN-1322 -RF_Filter:HFCN-1500 -RF_Filter:HFCN-1500D -RF_Filter:HFCN-1600 -RF_Filter:HFCN-1600D -RF_Filter:HFCN-1760 -RF_Filter:HFCN-1810 -RF_Filter:HFCN-1810D -RF_Filter:HFCN-1910 -RF_Filter:HFCN-1910D -RF_Filter:HFCN-2000 -RF_Filter:HFCN-2100 -RF_Filter:HFCN-2100D -RF_Filter:HFCN-2275 -RF_Filter:HFCN-2700 -RF_Filter:HFCN-2700A -RF_Filter:HFCN-2700AD -RF_Filter:HFCN-3100 -RF_Filter:HFCN-3100D -RF_Filter:HFCN-3500 -RF_Filter:HFCN-3500D -RF_Filter:HFCN-3800 -RF_Filter:HFCN-3800D -RF_Filter:HFCN-440 -RF_Filter:HFCN-4400 -RF_Filter:HFCN-4400D -RF_Filter:HFCN-4600 -RF_Filter:HFCN-5050 -RF_Filter:HFCN-5500 -RF_Filter:HFCN-5500D -RF_Filter:HFCN-6010 -RF_Filter:HFCN-650 -RF_Filter:HFCN-650D -RF_Filter:HFCN-672 -RF_Filter:HFCN-7150 -RF_Filter:HFCN-740 -RF_Filter:HFCN-740D -RF_Filter:HFCN-7971 -RF_Filter:HFCN-8400 -RF_Filter:HFCN-8400D -RF_Filter:HFCN-880 -RF_Filter:HFCN-880D -RF_Filter:HFCN-9700 -RF_Filter:LFCN-1000 -RF_Filter:LFCN-1000D -RF_Filter:LFCN-105 -RF_Filter:LFCN-113 -RF_Filter:LFCN-120 -RF_Filter:LFCN-1200 -RF_Filter:LFCN-1200D -RF_Filter:LFCN-123 -RF_Filter:LFCN-1282 -RF_Filter:LFCN-1325 -RF_Filter:LFCN-1400 -RF_Filter:LFCN-1400D -RF_Filter:LFCN-1450 -RF_Filter:LFCN-1500 -RF_Filter:LFCN-1500D -RF_Filter:LFCN-1525 -RF_Filter:LFCN-1525D -RF_Filter:LFCN-1575 -RF_Filter:LFCN-1575D -RF_Filter:LFCN-160 -RF_Filter:LFCN-1700 -RF_Filter:LFCN-1700D -RF_Filter:LFCN-180 -RF_Filter:LFCN-1800 -RF_Filter:LFCN-1800D -RF_Filter:LFCN-190 -RF_Filter:LFCN-2000 -RF_Filter:LFCN-2000D -RF_Filter:LFCN-225 -RF_Filter:LFCN-2250 -RF_Filter:LFCN-2250D -RF_Filter:LFCN-225D -RF_Filter:LFCN-2290 -RF_Filter:LFCN-2400 -RF_Filter:LFCN-2400D -RF_Filter:LFCN-2500 -RF_Filter:LFCN-2500D -RF_Filter:LFCN-2600 -RF_Filter:LFCN-2600D -RF_Filter:LFCN-2750 -RF_Filter:LFCN-2750D -RF_Filter:LFCN-2850 -RF_Filter:LFCN-2850D -RF_Filter:LFCN-3000 -RF_Filter:LFCN-3000D -RF_Filter:LFCN-320 -RF_Filter:LFCN-320D -RF_Filter:LFCN-3400 -RF_Filter:LFCN-3400D -RF_Filter:LFCN-3800 -RF_Filter:LFCN-3800D -RF_Filter:LFCN-400 -RF_Filter:LFCN-400D -RF_Filter:LFCN-4400 -RF_Filter:LFCN-4400D -RF_Filter:LFCN-490 -RF_Filter:LFCN-490D -RF_Filter:LFCN-5000 -RF_Filter:LFCN-5000D -RF_Filter:LFCN-530 -RF_Filter:LFCN-530D -RF_Filter:LFCN-5500 -RF_Filter:LFCN-5500D -RF_Filter:LFCN-575 -RF_Filter:LFCN-575D -RF_Filter:LFCN-5850 -RF_Filter:LFCN-5850D -RF_Filter:LFCN-6000 -RF_Filter:LFCN-6000D -RF_Filter:LFCN-630 -RF_Filter:LFCN-630D -RF_Filter:LFCN-6400 -RF_Filter:LFCN-6400D -RF_Filter:LFCN-6700 -RF_Filter:LFCN-6700D -RF_Filter:LFCN-7200 -RF_Filter:LFCN-7200D -RF_Filter:LFCN-722 -RF_Filter:LFCN-80 -RF_Filter:LFCN-800 -RF_Filter:LFCN-800D -RF_Filter:LFCN-8400 -RF_Filter:LFCN-8440 -RF_Filter:LFCN-900 -RF_Filter:LFCN-900D -RF_Filter:LFCN-9170 -RF_Filter:LFCN-95 -RF_Filter:LPF-B0R3 -RF_Filter:RBP-280 -RF_Filter:RBPF-246 -RF_Filter:RLP-30 -RF_Filter:SCHF-31 -RF_Filter:STA0232A -RF_Filter:STA1090EC -RF_Filter:SXBP-100 -RF_Filter:SXBP-140 -RF_Filter:SXBP-202 -RF_Filter:SXBP-27R5 -RF_Filter:TA0232A -RF_Filter:TA0970B -RF_GPS:L70-R -RF_GPS:L80-R -RF_GPS:LEA-M8F -RF_GPS:LEA-M8S -RF_GPS:LEA-M8T -RF_GPS:MAX-8C -RF_GPS:MAX-8Q -RF_GPS:MAX-M10S -RF_GPS:MAX-M8C -RF_GPS:MAX-M8Q -RF_GPS:MAX-M8W -RF_GPS:NEO-8Q -RF_GPS:NEO-M8M -RF_GPS:NEO-M8N -RF_GPS:NEO-M8P -RF_GPS:NEO-M8Q -RF_GPS:NEO-M8T -RF_GPS:NEO-M9N -RF_GPS:RXM-GPS-FM -RF_GPS:RXM-GPS-RM -RF_GPS:SAM-M8Q -RF_GPS:SIM28ML -RF_GPS:ZED-F9P -RF_GPS:ZOE-M8G -RF_GPS:ZOE-M8Q -RF_GSM:BC66 -RF_GSM:BC95 -RF_GSM:BG95-M1 -RF_GSM:BG95-M2 -RF_GSM:BG95-M3 -RF_GSM:BG95-M4 -RF_GSM:BG95-M5 -RF_GSM:BG95-M6 -RF_GSM:BG95-M8 -RF_GSM:BG95-MF -RF_GSM:LENA-R8001 -RF_GSM:M95 -RF_GSM:SARA-U201 -RF_GSM:SARA-U260 -RF_GSM:SARA-U270 -RF_GSM:SARA-U280 -RF_GSM:SE150A4 -RF_GSM:SIM7020C -RF_GSM:SIM7020E -RF_GSM:SIM800C -RF_GSM:SIM900 -RF_GSM:UL865 -RF_Mixer:AD831AP -RF_Mixer:ADE-6 -RF_Mixer:ADEX-10 -RF_Mixer:ADL5801 -RF_Mixer:ADL5802 -RF_Mixer:HMC213A -RF_Mixer:HMC213B -RF_Mixer:LT5560 -RF_Module:AST50147-xx -RF_Module:ATSAMR21G18-MR210UA_NoRFPads -RF_Module:AX-SIP-SFEU -RF_Module:AX-SIP-SFEU-API -RF_Module:Ai-Thinker-Ra-01 -RF_Module:Ai-Thinker-Ra-02 -RF_Module:CMWX1ZZABZ-078 -RF_Module:CMWX1ZZABZ-091 -RF_Module:D52MxxM8 -RF_Module:DCTR-52DA -RF_Module:DCTR-52DAT -RF_Module:DWM1000 -RF_Module:DWM1001 -RF_Module:DWM3000 -RF_Module:E18-MS1-PCB -RF_Module:E73-2G4M04S-52810 -RF_Module:E73-2G4M04S-52832 -RF_Module:ESP-07 -RF_Module:ESP-12E -RF_Module:ESP-12F -RF_Module:ESP-WROOM-02 -RF_Module:ESP32-C3-DevKitM-1 -RF_Module:ESP32-C3-WROOM-02 -RF_Module:ESP32-C3-WROOM-02U -RF_Module:ESP32-C6-MINI-1 -RF_Module:ESP32-S2-WROVER -RF_Module:ESP32-S2-WROVER-I -RF_Module:ESP32-S3-MINI-1 -RF_Module:ESP32-S3-MINI-1U -RF_Module:ESP32-S3-WROOM-1 -RF_Module:ESP32-S3-WROOM-2 -RF_Module:ESP32-WROOM-32 -RF_Module:ESP32-WROOM-32D -RF_Module:ESP32-WROOM-32E -RF_Module:ESP32-WROOM-32E-R2 -RF_Module:ESP32-WROOM-32U -RF_Module:ESP32-WROOM-32UE -RF_Module:ESP32-WROOM-32UE-R2 -RF_Module:HT-CT62 -RF_Module:Jadak_Thingmagic_M6e-Nano -RF_Module:MDBT42Q-512K -RF_Module:MDBT50Q-1MV2 -RF_Module:MDBT50Q-512K -RF_Module:MDBT50Q-P1MV2 -RF_Module:MDBT50Q-P512K -RF_Module:MDBT50Q-U1MV2 -RF_Module:MDBT50Q-U512K -RF_Module:MM002 -RF_Module:Particle_P1 -RF_Module:RAK4200 -RF_Module:RAK811-HF-AS923 -RF_Module:RAK811-HF-AU915 -RF_Module:RAK811-HF-EU868 -RF_Module:RAK811-HF-IN865 -RF_Module:RAK811-HF-KR920 -RF_Module:RAK811-HF-US915 -RF_Module:RAK811-LF-CN470 -RF_Module:RAK811-LF-EU433 -RF_Module:RFM69HCW -RF_Module:RFM69HW -RF_Module:RFM69W -RF_Module:RFM95W-868S2 -RF_Module:RFM95W-915S2 -RF_Module:RFM96W-315S2 -RF_Module:RFM96W-433S2 -RF_Module:RFM97W-868S2 -RF_Module:RFM97W-915S2 -RF_Module:RFM98W-315S2 -RF_Module:RFM98W-433S2 -RF_Module:STM32WB5MMG -RF_Module:TD1205 -RF_Module:TD1208 -RF_Module:TR-52DA -RF_Module:TR-52DAT -RF_Module:TR-72DA -RF_Module:TR-72DAT -RF_Module:WEMOS_C3_mini -RF_Module:WEMOS_D1_mini -RF_Module:iM880A -RF_Module:iM880B -RF_NFC:PN5321A3HN_C1xx -RF_NFC:ST25DV04K-IER8C3 -RF_NFC:ST25DV04K-JFR6D3 -RF_NFC:ST25DV16K-IER8C3 -RF_NFC:ST25DV16K-JFR6D3 -RF_NFC:ST25DV64K-IER8C3 -RF_NFC:ST25DV64K-JFR6D3 -RF_NFC:ST25R3911B-AQF -RF_NFC:ST25R3911B-AQW -RF_RFID:HTRC11001T -RF_RFID:PN5120A0HN1 -RF_Switch:ADG901BCPZ -RF_Switch:ADG901BRMZ -RF_Switch:ADG902BRMZ -RF_Switch:ADG918BCPZ -RF_Switch:ADG918BRM -RF_Switch:ADG919BCPZ -RF_Switch:ADG919BRMZ -RF_Switch:AS179-92LF -RF_Switch:BGS12WN6E6327 -RF_Switch:HMC7992 -RF_Switch:HMC849A -RF_Switch:KSW-2-46 -RF_Switch:KSWA-2-46 -RF_Switch:KSWHA-1-20 -RF_Switch:MASW-007221 -RF_Switch:MASWSS0115 -RF_Switch:MASWSS0136 -RF_Switch:MASWSS0143 -RF_Switch:MASWSS0151 -RF_Switch:MASWSS0166 -RF_Switch:MASWSS0176 -RF_Switch:MASWSS0178 -RF_Switch:MASWSS0179 -RF_Switch:MASWSS0192 -RF_Switch:MSW-2-20 -RF_Switch:MSW2-50 -RF_Switch:MSWA-2-20 -RF_Switch:MSWA2-50 -RF_Switch:SKY13380-350LF -RF_Switch:SKY13575-639LF -RF_WiFi:HF-A11-SMT -RF_WiFi:USR-C322 -RF_ZigBee:CC2520 -RF_ZigBee:MC13192 -RF_ZigBee:MW-R-DP-W -RF_ZigBee:MW-R-WX -RF_ZigBee:TWE-L-DP-W -RF_ZigBee:TWE-L-WX -RF_ZigBee:XBee_SMT Security:ATAES132A-SH Security:ATECC508A-MAHDA Security:ATECC508A-SSHDA @@ -19553,7 +20319,6 @@ Sensor:BME280 Sensor:BME680 Sensor:CHT11 Sensor:DHT11 -Sensor:INA260 Sensor:LTC2990 Sensor:MAX30102 Sensor:Nuclear-Radiation_Detector @@ -19583,8 +20348,6 @@ Sensor_Current:A1367xKTTN-1 Sensor_Current:A1367xKTTN-10 Sensor_Current:A1367xKTTN-2 Sensor_Current:A1367xKTTN-5 -Sensor_Current:A1369xUA-10 -Sensor_Current:A1369xUA-24 Sensor_Current:ACS706xLC-05C Sensor_Current:ACS709xLFTR-10BB Sensor_Current:ACS709xLFTR-20BB @@ -19677,7 +20440,6 @@ Sensor_Current:ACS730xLCTR-40AU Sensor_Current:ACS730xLCTR-50AB Sensor_Current:ACS730xLCTR-80AU Sensor_Current:ACS732xLATR-40AB -Sensor_Current:ACS73369xUAA-010B5 Sensor_Current:ACS733xLATR-20AB Sensor_Current:ACS733xLATR-40AB Sensor_Current:ACS733xLATR-40AU @@ -19843,9 +20605,12 @@ Sensor_Energy:INA219BxD Sensor_Energy:INA219BxDCN Sensor_Energy:INA226 Sensor_Energy:INA228 +Sensor_Energy:INA229 Sensor_Energy:INA233 +Sensor_Energy:INA234AxYBJ Sensor_Energy:INA237 Sensor_Energy:INA238 +Sensor_Energy:INA260 Sensor_Energy:LTC4151xMS Sensor_Energy:MCP39F521 Sensor_Energy:PAC1931x-xJ6CX @@ -19895,6 +20660,7 @@ Sensor_Gas:SCD40-D-R2 Sensor_Gas:SCD41-D-R2 Sensor_Gas:TGS-5141 Sensor_Humidity:ENS210 +Sensor_Humidity:GXHTC3 Sensor_Humidity:HDC1080 Sensor_Humidity:HDC2080 Sensor_Humidity:SHT30-DIS @@ -19908,24 +20674,27 @@ Sensor_Humidity:SHTC1 Sensor_Humidity:SHTC3 Sensor_Humidity:Si7020-A20 Sensor_Humidity:Si7021-A20 -Sensor_Magnetic:A1101ELHL -Sensor_Magnetic:A1101LLHL -Sensor_Magnetic:A1102ELHL -Sensor_Magnetic:A1102LLHL -Sensor_Magnetic:A1103ELHL -Sensor_Magnetic:A1103LLHL -Sensor_Magnetic:A1104LLHL -Sensor_Magnetic:A1106LLHL -Sensor_Magnetic:A1301EUA-T -Sensor_Magnetic:A1301KLHLT-T -Sensor_Magnetic:A1301KUA-T -Sensor_Magnetic:A1302ELHLT-T -Sensor_Magnetic:A1302KLHLT-T -Sensor_Magnetic:A1302KUA-T -Sensor_Magnetic:A3214ELHLT-T +Sensor_Magnetic:A1101xLH +Sensor_Magnetic:A1101xUA +Sensor_Magnetic:A1102xLH +Sensor_Magnetic:A1103xLH +Sensor_Magnetic:A1104xLH +Sensor_Magnetic:A1106xLH +Sensor_Magnetic:A1301xLH +Sensor_Magnetic:A1301xUA +Sensor_Magnetic:A1302xLH +Sensor_Magnetic:A1302xUA +Sensor_Magnetic:A1308xLHxx-1 +Sensor_Magnetic:A1308xLHxx-2 +Sensor_Magnetic:A1309xLHxx-9 +Sensor_Magnetic:A1369xUA-10 +Sensor_Magnetic:A1369xUA-24 +Sensor_Magnetic:A3214xLH +Sensor_Magnetic:ACS73369xUAA-010B5 Sensor_Magnetic:AH1806-P Sensor_Magnetic:AH1806-W Sensor_Magnetic:AH1806-Z +Sensor_Magnetic:AH452UA Sensor_Magnetic:AK7452 Sensor_Magnetic:AS5045B Sensor_Magnetic:AS5047D @@ -19933,6 +20702,8 @@ Sensor_Magnetic:AS5048A Sensor_Magnetic:AS5048B Sensor_Magnetic:AS5050A Sensor_Magnetic:AS5055A +Sensor_Magnetic:AS5510-DSO +Sensor_Magnetic:AS5510-DWL Sensor_Magnetic:BM1422AGMV Sensor_Magnetic:BMM150 Sensor_Magnetic:DRV5033AJxDBZ @@ -19952,6 +20723,7 @@ Sensor_Magnetic:IST8310 Sensor_Magnetic:LIS2MDL Sensor_Magnetic:LIS3MDL Sensor_Magnetic:MA730 +Sensor_Magnetic:MLX90395xLW Sensor_Magnetic:MMC5633NJL Sensor_Magnetic:MMC5883MA Sensor_Magnetic:MT6701CT @@ -19979,6 +20751,7 @@ Sensor_Magnetic:TMAG5170-Q1 Sensor_Motion:ADXL343 Sensor_Motion:ADXL363 Sensor_Motion:BMF055 +Sensor_Motion:BMI088 Sensor_Motion:BMI160 Sensor_Motion:BNO055 Sensor_Motion:ICM-20602 @@ -20044,6 +20817,8 @@ Sensor_Optical:BPX61 Sensor_Optical:BPX65 Sensor_Optical:BPY62 Sensor_Optical:C12880MA +Sensor_Optical:D_SiPM_OnSemi_MicroFJ-300xx +Sensor_Optical:D_SiPM_OnSemi_MicroFJ-60035 Sensor_Optical:Flir_LEPTON Sensor_Optical:ISL29035 Sensor_Optical:KPS-3227 @@ -20087,7 +20862,11 @@ Sensor_Pressure:40PC015G Sensor_Pressure:40PC100G Sensor_Pressure:40PC150G Sensor_Pressure:40PC250G +Sensor_Pressure:ABPxxxxxxxxx0 +Sensor_Pressure:ABPxxxxxxxxxA +Sensor_Pressure:ABPxxxxxxxxxS Sensor_Pressure:BMP280 +Sensor_Pressure:ILPS28QSW Sensor_Pressure:LPS22DF Sensor_Pressure:LPS22HB Sensor_Pressure:LPS22HH @@ -20103,6 +20882,7 @@ Sensor_Pressure:MS5607-02BA Sensor_Pressure:MS5611-01BA Sensor_Pressure:MS5837-xxBA Sensor_Pressure:WSEN-PADS_2511020213301 +Sensor_Pressure:XGZP6859D Sensor_Pressure:XGZP6897D Sensor_Pressure:XGZP6899D Sensor_Proximity:AD7150BRMZ @@ -20112,6 +20892,7 @@ Sensor_Proximity:BPR-105 Sensor_Proximity:BPR-105F Sensor_Proximity:BPR-205 Sensor_Proximity:CNY70 +Sensor_Proximity:FDC1004DGS Sensor_Proximity:GP2S700HCP Sensor_Proximity:ITR1201SR10AR Sensor_Proximity:ITR8307 @@ -20247,6 +21028,7 @@ Sensor_Temperature:TMP116xxDRV Sensor_Temperature:TMP117xxDRV Sensor_Temperature:TMP117xxYBG Sensor_Temperature:TMP119AIYBGR +Sensor_Temperature:TMP1826DGK Sensor_Temperature:TMP20AIDCK Sensor_Temperature:TMP20AIDRL Sensor_Temperature:TMP36xS @@ -20312,6 +21094,7 @@ Simulation_SPICE:PMOS Simulation_SPICE:PMOS_Substrate Simulation_SPICE:PNP Simulation_SPICE:PNP_Substrate +Simulation_SPICE:Potentiometer Simulation_SPICE:SWITCH Simulation_SPICE:TLINE Simulation_SPICE:VAM @@ -20399,7 +21182,8 @@ Switch:SW_SPDT_XKB_DMx-xxxx-1 Switch:SW_SPST Switch:SW_SPST_LED Switch:SW_SPST_Lamp -Switch:SW_SPST_Temperature +Switch:SW_SPST_Temperature_NC +Switch:SW_SPST_Temperature_NO Switch:SW_Slide_DPDT Switch:SW_Wuerth_450301014042 Timer:8253 @@ -20516,6 +21300,13 @@ Timer_RTC:MCP7940N-xMS Timer_RTC:MCP7940N-xP Timer_RTC:MCP7940N-xSN Timer_RTC:MCP7940N-xST +Timer_RTC:MCP79510-xMS +Timer_RTC:MCP79511-xMS +Timer_RTC:MCP79512-xMS +Timer_RTC:MCP79520-xMS +Timer_RTC:MCP79521-xMS +Timer_RTC:MCP79522-xMS +Timer_RTC:PCA2131 Timer_RTC:PCF85063ATL Timer_RTC:PCF8523T Timer_RTC:PCF8523TK @@ -20660,6 +21451,8 @@ Transistor_Array:SN75469 Transistor_Array:TBD62783A Transistor_Array:TBD62785AFWG Transistor_Array:TBD62785APG +Transistor_Array:TPL7407LAD +Transistor_Array:TPL7407LAPW Transistor_Array:ULN2002 Transistor_Array:ULN2002A Transistor_Array:ULN2003 @@ -20734,6 +21527,7 @@ Transistor_BJT:BC818 Transistor_BJT:BC818W Transistor_BJT:BC846 Transistor_BJT:BC846BDW1 +Transistor_BJT:BC846BLP4 Transistor_BJT:BC846BPDW1 Transistor_BJT:BC846BPN Transistor_BJT:BC846BS @@ -21019,6 +21813,7 @@ Transistor_BJT:Q_NPN_Darlington_ECBC Transistor_BJT:Q_NPN_EBC Transistor_BJT:Q_NPN_ECB Transistor_BJT:Q_NPN_ECBC +Transistor_BJT:Q_PNP_ACAB Transistor_BJT:Q_PNP_BCE Transistor_BJT:Q_PNP_BCEC Transistor_BJT:Q_PNP_BEC @@ -21062,12 +21857,14 @@ Transistor_BJT:TIP42A Transistor_BJT:TIP42B Transistor_BJT:TIP42C Transistor_BJT:UMH3N +Transistor_FET:12N03M Transistor_FET:2N3819 Transistor_FET:2N7000 Transistor_FET:2N7002 Transistor_FET:2N7002E Transistor_FET:2N7002H Transistor_FET:2N7002K +Transistor_FET:2SK3019 Transistor_FET:3SK263 Transistor_FET:AO3400A Transistor_FET:AO3401A @@ -21195,6 +21992,9 @@ Transistor_FET:BUK9M7R2-40EX Transistor_FET:BUK9M85-60EX Transistor_FET:BUK9M9R1-40EX Transistor_FET:BUZ11 +Transistor_FET:BUZ31 +Transistor_FET:BUZ31H3046 +Transistor_FET:BUZ31L Transistor_FET:C2M0025120D Transistor_FET:C2M0040120D Transistor_FET:C2M0045170D @@ -21287,6 +22087,7 @@ Transistor_FET:CSD19532Q5B Transistor_FET:CSD19533Q5A Transistor_FET:CSD19534Q5A Transistor_FET:CSD19537Q3 +Transistor_FET:CSD19538Q2 Transistor_FET:CSD25302Q2 Transistor_FET:CSD25402Q3A Transistor_FET:CSD25480F3 @@ -21330,6 +22131,7 @@ Transistor_FET:DMN61D8LQ Transistor_FET:DMN67D7L Transistor_FET:DMN67D8L Transistor_FET:DMP3013SFV +Transistor_FET:DMP3099L Transistor_FET:DMP6050SSD Transistor_FET:DMT6008LFG Transistor_FET:EPC2035 @@ -21405,6 +22207,7 @@ Transistor_FET:IPP060N06N Transistor_FET:IPT012N08N5 Transistor_FET:IPT015N10N5 Transistor_FET:IPT020N10N3 +Transistor_FET:IPT60R055CM8 Transistor_FET:IRF3205 Transistor_FET:IRF40DM229 Transistor_FET:IRF4905 @@ -21463,8 +22266,10 @@ Transistor_FET:IRF6893M Transistor_FET:IRF6894M Transistor_FET:IRF6898M Transistor_FET:IRF7171M -Transistor_FET:IRF7309IPBF +Transistor_FET:IRF7309PBF +Transistor_FET:IRF7311PBF Transistor_FET:IRF7324 +Transistor_FET:IRF7341PBF Transistor_FET:IRF7343PBF Transistor_FET:IRF740 Transistor_FET:IRF7403 @@ -21490,6 +22295,7 @@ Transistor_FET:IRF8304M Transistor_FET:IRF8306M Transistor_FET:IRF8308M Transistor_FET:IRF8327S +Transistor_FET:IRF840A Transistor_FET:IRF8721PBF-1 Transistor_FET:IRF9383M Transistor_FET:IRF9540N @@ -21531,6 +22337,7 @@ Transistor_FET:MMBFJ111 Transistor_FET:MMBFJ112 Transistor_FET:MMBFJ113 Transistor_FET:NDT3055L +Transistor_FET:NTMFS016N06CT1G Transistor_FET:NTR2101P Transistor_FET:PGA26E07BA Transistor_FET:PGA26E19BA @@ -21540,6 +22347,7 @@ Transistor_FET:PSMN5R2-60YL Transistor_FET:QM6006D Transistor_FET:QM6015D Transistor_FET:Q_Dual_NMOS_G1S2G2D2S1D1 +Transistor_FET:Q_Dual_NMOS_PMOS_G1S2G2D2S1D1 Transistor_FET:Q_Dual_NMOS_S1G1D2S2G2D1 Transistor_FET:Q_Dual_NMOS_S1G1S2G2D2D1 Transistor_FET:Q_Dual_NMOS_S1G1S2G2D2D2D1D1 @@ -21551,8 +22359,10 @@ Transistor_FET:Q_NMOS_DSG Transistor_FET:Q_NMOS_GDS Transistor_FET:Q_NMOS_GDSD Transistor_FET:Q_NMOS_GSD +Transistor_FET:Q_NMOS_GSSD Transistor_FET:Q_NMOS_SDGD Transistor_FET:Q_NMOS_SGD +Transistor_FET:Q_NMOS_SSSGD_AvalancheRated Transistor_FET:Q_PMOS_DGS Transistor_FET:Q_PMOS_DSG Transistor_FET:Q_PMOS_GDS @@ -21566,12 +22376,15 @@ Transistor_FET:RS9N50D Transistor_FET:RSQ030N08HZG Transistor_FET:SCTL35N65G2V Transistor_FET:SGT65R65AL +Transistor_FET:SP2002KNC +Transistor_FET:SQJ409EP Transistor_FET:SQJQ100E Transistor_FET:SQJQ100EL Transistor_FET:SQJQ112E Transistor_FET:SQJQ114EL Transistor_FET:SQJQ116EL Transistor_FET:SQJQ130EL +Transistor_FET:SQJQ131EL Transistor_FET:SQJQ140E Transistor_FET:SQJQ142E Transistor_FET:SQJQ144AE @@ -21591,6 +22404,7 @@ Transistor_FET:STB15N80K5 Transistor_FET:STB33N65M2 Transistor_FET:STB40N60M2 Transistor_FET:STD7NK40Z +Transistor_FET:STE40NC60 Transistor_FET:STS2DNE60 Transistor_FET:SUD08P06-155L Transistor_FET:SUD09P10-195 @@ -21612,6 +22426,9 @@ Transistor_FET:Si7141DP Transistor_FET:Si7336ADP Transistor_FET:Si7450DP Transistor_FET:Si7617DN +Transistor_FET:Si9634DY +Transistor_FET:Si9945BDY +Transistor_FET:Si9948AEY Transistor_FET:SiA449DJ Transistor_FET:SiA453EDJ Transistor_FET:SiA462DJ @@ -21626,7 +22443,6 @@ Transistor_FET:TP0610T Transistor_FET:TSM2301ACX Transistor_FET:TSM2302CX Transistor_FET:VN10LF -Transistor_FET:VNP10N07 Transistor_FET:VP0610L Transistor_FET:VP0610T Transistor_FET:ZVN3306F @@ -21649,15 +22465,22 @@ Transistor_FET:ZXMN3B14F Transistor_FET:ZXMN3F30FH Transistor_FET:ZXMN6A07F Transistor_FET:ZXMP4A16G +Transistor_FET_Other:BSP75N +Transistor_FET_Other:BSP76 +Transistor_FET_Other:BTS4140N Transistor_FET_Other:DN2540N3-G Transistor_FET_Other:DN2540N5-G Transistor_FET_Other:DN2540N8-G +Transistor_FET_Other:NCV8402xST Transistor_FET_Other:Q_NMOS_Depletion_DGS Transistor_FET_Other:Q_NMOS_Depletion_DSG Transistor_FET_Other:Q_NMOS_Depletion_GDS Transistor_FET_Other:Q_NMOS_Depletion_GSD Transistor_FET_Other:Q_NMOS_Depletion_SDG Transistor_FET_Other:Q_NMOS_Depletion_SGD +Transistor_FET_Other:VNB35N07xx-E +Transistor_FET_Other:VNP10N07 +Transistor_FET_Other:VNP35N07xx-E Transistor_IGBT:IRG4PF50W Transistor_IGBT:STGP7NC60HD Transistor_Power_Module:A2C25S12M3 @@ -21674,6 +22497,10 @@ Transistor_Power_Module:FP25R12W2T4P Transistor_Power_Module:FP35R12W2T4 Transistor_Power_Module:FP35R12W2T4P Transistor_Power_Module:FP50R06W2E3 +Transistor_Power_Module:FS25R12W1T7 +Transistor_Power_Module:FS25R12W1T7B11 +Transistor_Power_Module:FS25R12W1T7PB11 +Transistor_Power_Module:FS30R06W1E3 Transistor_Power_Module:FS75R07N2E4 Transistor_Power_Module:MG12100W-XN2MM Transistor_Power_Module:MG1215H-XBN2MM @@ -21779,8 +22606,9 @@ Video:ADV7390BCPZ Video:ADV7391BCPZ Video:AV9173 Video:CX7930 +Video:CXA1145P Video:CXD3400N -Video:HD63484 +Video:HD63484P Video:HD63484_PLCC Video:ICX415AQ Video:ISL59885 @@ -21805,3 +22633,104 @@ Video:TDA9513 Video:TEA2014 Video:TEA5115 Video:TFP410PAP +power:+10V +power:+12C +power:+12L +power:+12LF +power:+12P +power:+12V +power:+12VA +power:+15V +power:+1V0 +power:+1V1 +power:+1V2 +power:+1V35 +power:+1V5 +power:+1V8 +power:+24V +power:+28V +power:+2V5 +power:+2V8 +power:+3.3V +power:+3.3VA +power:+3.3VADC +power:+3.3VDAC +power:+3.3VP +power:+36V +power:+3V0 +power:+3V3 +power:+3V8 +power:+48V +power:+4V +power:+5C +power:+5F +power:+5P +power:+5V +power:+5VA +power:+5VD +power:+5VL +power:+5VP +power:+6V +power:+7.5V +power:+8V +power:+9V +power:+9VA +power:+BATT +power:+VDC +power:+VSW +power:-10V +power:-12V +power:-12VA +power:-15V +power:-24V +power:-2V5 +power:-36V +power:-3V3 +power:-48V +power:-5V +power:-5VA +power:-6V +power:-8V +power:-9V +power:-9VA +power:-BATT +power:-VDC +power:-VSW +power:AC +power:Earth +power:Earth_Clean +power:Earth_Protective +power:GND +power:GND1 +power:GND2 +power:GND3 +power:GNDA +power:GNDD +power:GNDPWR +power:GNDREF +power:GNDS +power:HT +power:LINE +power:NEUT +power:PRI_HI +power:PRI_LO +power:PRI_MID +power:PWR_FLAG +power:VAA +power:VAC +power:VBUS +power:VCC +power:VCCQ +power:VCOM +power:VD +power:VDC +power:VDD +power:VDDA +power:VDDF +power:VEE +power:VMEM +power:VPP +power:VS +power:VSS +power:VSSA +power:Vdrive diff --git a/rector.php b/rector.php index 936b447e..5a77a882 100644 --- a/rector.php +++ b/rector.php @@ -18,7 +18,7 @@ use Rector\Symfony\Set\SymfonySetList; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; return RectorConfig::configure() - ->withComposerBased(phpunit: true) + ->withComposerBased(phpunit: true, symfony: true) ->withSymfonyContainerPhp(__DIR__ . '/tests/symfony-container.php') ->withSymfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml') @@ -36,8 +36,6 @@ return RectorConfig::configure() PHPUnitSetList::PHPUNIT_90, PHPUnitSetList::PHPUNIT_110, PHPUnitSetList::PHPUNIT_CODE_QUALITY, - - ]) ->withRules([ @@ -59,6 +57,9 @@ return RectorConfig::configure() PreferPHPUnitThisCallRector::class, //Do not replace 'GET' with class constant, LiteralGetToRequestClassConstantRector::class, + + //Do not move help text of commands to the command class, as we want to keep the help text in the command definition for better readability + \Rector\Symfony\Symfony73\Rector\Class_\CommandHelpToAttributeRector::class ]) //Do not apply rules to Symfony own files @@ -67,6 +68,7 @@ return RectorConfig::configure() __DIR__ . '/src/Kernel.php', __DIR__ . '/config/preload.php', __DIR__ . '/config/bundles.php', + __DIR__ . '/config/reference.php' ]) ; diff --git a/src/ApiResource/LabelGenerationRequest.php b/src/ApiResource/LabelGenerationRequest.php new file mode 100644 index 00000000..9b4462a0 --- /dev/null +++ b/src/ApiResource/LabelGenerationRequest.php @@ -0,0 +1,84 @@ +. + */ + +namespace App\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\RequestBody; +use ApiPlatform\OpenApi\Model\Response; +use App\Entity\LabelSystem\LabelSupportedElement; +use App\State\LabelGenerationProcessor; +use App\Validator\Constraints\Misc\ValidRange; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * API Resource for generating PDF labels for parts, part lots, or storage locations. + * This endpoint allows generating labels using saved label profiles. + */ +#[ApiResource( + uriTemplate: '/labels/generate', + description: 'Generate PDF labels for parts, part lots, or storage locations using label profiles.', + operations: [ + new Post( + inputFormats: ['json' => ['application/json']], + outputFormats: [], + openapi: new Operation( + responses: [ + "200" => new Response(description: "PDF file containing the generated labels"), + ], + summary: 'Generate PDF labels', + description: 'Generate PDF labels for one or more elements using a label profile. Returns a PDF file.', + requestBody: new RequestBody( + description: 'Label generation request', + required: true, + ), + ), + ) + ], + processor: LabelGenerationProcessor::class, +)] +class LabelGenerationRequest +{ + /** + * @var int The ID of the label profile to use for generation + */ + #[Assert\NotBlank(message: 'Profile ID is required')] + #[Assert\Positive(message: 'Profile ID must be a positive integer')] + public int $profileId = 0; + + /** + * @var string Comma-separated list of element IDs or ranges (e.g., "1,2,5-10,15") + */ + #[Assert\NotBlank(message: 'Element IDs are required')] + #[ValidRange()] + #[ApiProperty(example: "1,2,5-10,15")] + public string $elementIds = ''; + + /** + * @var LabelSupportedElement|null Optional: Override the element type. If not provided, uses profile's default. + */ + public ?LabelSupportedElement $elementType = null; +} diff --git a/src/Command/BackupCommand.php b/src/Command/BackupCommand.php index 085c552a..c4fb3777 100644 --- a/src/Command/BackupCommand.php +++ b/src/Command/BackupCommand.php @@ -201,6 +201,10 @@ class BackupCommand extends Command $config_dir = $this->project_dir.'/config'; $zip->addFile($config_dir.'/parameters.yaml', 'config/parameters.yaml'); $zip->addFile($config_dir.'/banner.md', 'config/banner.md'); + + //Add kicad custom footprints and symbols files + $zip->addFile($this->project_dir . '/public/kicad/footprints_custom.txt', 'public/kicad/footprints_custom.txt'); + $zip->addFile($this->project_dir . '/public/kicad/symbols_custom.txt', 'public/kicad/symbols_custom.txt'); } protected function backupAttachments(ZipFile $zip, SymfonyStyle $io): void diff --git a/src/Command/LoadFixturesCommand.php b/src/Command/LoadFixturesCommand.php index d01d19c3..98052ba6 100644 --- a/src/Command/LoadFixturesCommand.php +++ b/src/Command/LoadFixturesCommand.php @@ -56,13 +56,16 @@ class LoadFixturesCommand extends Command } $factory = new ResetAutoIncrementPurgerFactory(); - $purger = $factory->createForEntityManager(null, $this->entityManager); + + //Use truncate purging to fix compatibility with postgresql + $purger = $factory->createForEntityManager(null, $this->entityManager, purgeWithTruncate: true); $purger->purge(); //Afterwards run the load fixtures command as normal, but with the --append option $new_input = new ArrayInput([ 'command' => 'doctrine:fixtures:load', + '--purge-with-truncate' => true, '--append' => true, ]); @@ -70,4 +73,4 @@ class LoadFixturesCommand extends Command return $returnCode ?? Command::FAILURE; } -} \ No newline at end of file +} diff --git a/src/Command/MaintenanceModeCommand.php b/src/Command/MaintenanceModeCommand.php new file mode 100644 index 00000000..47b1eaef --- /dev/null +++ b/src/Command/MaintenanceModeCommand.php @@ -0,0 +1,141 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Command; + +use App\Services\System\UpdateExecutor; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand('partdb:maintenance-mode', 'Enable/disable maintenance mode and set a message')] +class MaintenanceModeCommand extends Command +{ + public function __construct( + private readonly UpdateExecutor $updateExecutor + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDefinition([ + new InputOption('enable', null, InputOption::VALUE_NONE, 'Enable maintenance mode'), + new InputOption('disable', null, InputOption::VALUE_NONE, 'Disable maintenance mode'), + new InputOption('status', null, InputOption::VALUE_NONE, 'Show current maintenance mode status'), + new InputOption('message', null, InputOption::VALUE_REQUIRED, 'Optional maintenance message (explicit option)'), + new InputArgument('message_arg', InputArgument::OPTIONAL, 'Optional maintenance message as a positional argument (preferred when writing message directly)') + ]); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $enable = (bool)$input->getOption('enable'); + $disable = (bool)$input->getOption('disable'); + $status = (bool)$input->getOption('status'); + + // Accept message either via --message option or as positional argument + $optionMessage = $input->getOption('message'); + $argumentMessage = $input->getArgument('message_arg'); + + // Prefer explicit --message option, otherwise use positional argument if provided + $message = null; + if (is_string($optionMessage) && $optionMessage !== '') { + $message = $optionMessage; + } elseif (is_string($argumentMessage) && $argumentMessage !== '') { + $message = $argumentMessage; + } + + // If no action provided, show help + if (!$enable && !$disable && !$status) { + $io->text('Maintenance mode command. See usage below:'); + $this->printHelp($io); + return Command::SUCCESS; + } + + if ($enable && $disable) { + $io->error('Conflicting options: specify either --enable or --disable, not both.'); + return Command::FAILURE; + } + + try { + if ($status) { + if ($this->updateExecutor->isMaintenanceMode()) { + $info = $this->updateExecutor->getMaintenanceInfo(); + $reason = $info['reason'] ?? 'Unknown reason'; + $enabledAt = $info['enabled_at'] ?? 'Unknown time'; + + $io->success(sprintf('Maintenance mode is ENABLED (since %s).', $enabledAt)); + $io->text(sprintf('Reason: %s', $reason)); + } else { + $io->success('Maintenance mode is DISABLED.'); + } + + // If only status requested, exit + if (!$enable && !$disable) { + return Command::SUCCESS; + } + } + + if ($enable) { + // Use provided message or fallback to a default English message + $reason = is_string($message) + ? $message + : 'The system is temporarily unavailable due to maintenance.'; + + $this->updateExecutor->enableMaintenanceMode($reason); + + $io->success(sprintf('Maintenance mode enabled. Reason: %s', $reason)); + } + + if ($disable) { + $this->updateExecutor->disableMaintenanceMode(); + $io->success('Maintenance mode disabled.'); + } + + return Command::SUCCESS; + } catch (\Throwable $e) { + $io->error(sprintf('Unexpected error: %s', $e->getMessage())); + return Command::FAILURE; + } + } + + private function printHelp(SymfonyStyle $io): void + { + $io->writeln(''); + $io->writeln('Usage:'); + $io->writeln(' php bin/console partdb:maintenance_mode --enable [--message="Maintenance message"]'); + $io->writeln(' php bin/console partdb:maintenance_mode --enable "Maintenance message"'); + $io->writeln(' php bin/console partdb:maintenance_mode --disable'); + $io->writeln(' php bin/console partdb:maintenance_mode --status'); + $io->writeln(''); + } + +} diff --git a/src/Command/Migrations/DBPlatformConvertCommand.php b/src/Command/Migrations/DBPlatformConvertCommand.php new file mode 100644 index 00000000..d1215da4 --- /dev/null +++ b/src/Command/Migrations/DBPlatformConvertCommand.php @@ -0,0 +1,266 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Command\Migrations; + +use App\Entity\UserSystem\User; +use App\Services\ImportExportSystem\PartKeeprImporter\PKImportHelper; +use Doctrine\Bundle\DoctrineBundle\ConnectionFactory; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager; +use Doctrine\Migrations\Configuration\Migration\ExistingConfiguration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Migrations\DependencyFactory; +use Doctrine\ORM\Id\AssignedGenerator; +use Doctrine\ORM\Mapping\ClassMetadata; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +#[AsCommand('partdb:migrations:convert-db-platform', 'Convert the database to a different platform')] +class DBPlatformConvertCommand extends Command +{ + + public function __construct( + private readonly EntityManagerInterface $targetEM, + private readonly PKImportHelper $importHelper, + private readonly DependencyFactory $dependencyFactory, + #[Autowire('%kernel.project_dir%')] + private readonly string $kernelProjectDir, + ) + { + parent::__construct(); + } + + public function configure(): void + { + $this + ->setHelp('This command allows you to migrate the database from one database platform to another (e.g. from MySQL to PostgreSQL).') + ->addArgument('url', InputArgument::REQUIRED, 'The database connection URL of the source database to migrate from'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $sourceEM = $this->getSourceEm($input->getArgument('url')); + + //Check that both databases are not using the same driver + if ($sourceEM->getConnection()->getDatabasePlatform()::class === $this->targetEM->getConnection()->getDatabasePlatform()::class) { + $io->warning('Source and target database are using the same database platform / driver. This command is only intended to migrate between different database platforms (e.g. from MySQL to PostgreSQL).'); + if (!$io->confirm('Do you want to continue anyway?', false)) { + $io->info('Aborting migration process.'); + return Command::SUCCESS; + } + } + + + $this->ensureVersionUpToDate($sourceEM); + + $io->note('This command is still in development. If you encounter any problems, please report them to the issue tracker on GitHub.'); + $io->warning(sprintf('This command will delete all existing data in the target database "%s". Make sure that you have no important data in the database before you continue!', + $this->targetEM->getConnection()->getDatabase() ?? 'unknown' + )); + + //$users = $sourceEM->getRepository(User::class)->findAll(); + //dump($users); + + $io->ask('Please type "DELETE ALL DATA" to continue.', '', function ($answer) { + if (strtoupper($answer) !== 'DELETE ALL DATA') { + throw new \RuntimeException('You did not type "DELETE ALL DATA"!'); + } + return $answer; + }); + + + // Example migration logic (to be replaced with actual migration code) + $io->info('Starting database migration...'); + + //Disable all event listeners on target EM to avoid unwanted side effects + $eventManager = $this->targetEM->getEventManager(); + foreach ($eventManager->getAllListeners() as $event => $listeners) { + foreach ($listeners as $listener) { + $eventManager->removeEventListener($event, $listener); + } + } + + $io->info('Clear target database...'); + $this->importHelper->purgeDatabaseForImport($this->targetEM, ['internal', 'migration_versions']); + + $metadata = $this->targetEM->getMetadataFactory()->getAllMetadata(); + + $io->info('Modifying entity metadata for migration...'); + //First we modify each entity metadata to have an persist cascade on all relations + foreach ($metadata as $metadatum) { + $entityClass = $metadatum->getName(); + $io->writeln('Modifying cascade and ID settings for entity: ' . $entityClass, OutputInterface::VERBOSITY_VERBOSE); + + foreach ($metadatum->getAssociationNames() as $fieldName) { + $mapping = $metadatum->getAssociationMapping($fieldName); + $mapping->cascade = array_unique(array_merge($mapping->cascade, ['persist'])); + $mapping->fetch = ClassMetadata::FETCH_EAGER; //Avoid lazy loading issues during migration + + $metadatum->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE); + $metadatum->setIdGenerator(new AssignedGenerator()); + } + } + + + $io->progressStart(count($metadata)); + + //First we migrate users to avoid foreign key constraint issues + $io->info('Migrating users first to avoid foreign key constraint issues...'); + $this->fixUsers($sourceEM); + + //Afterward we migrate all entities + foreach ($metadata as $metadatum) { + //skip all superclasses + if ($metadatum->isMappedSuperclass) { + continue; + } + + $entityClass = $metadatum->getName(); + + $io->note('Migrating entity: ' . $entityClass); + + $repo = $sourceEM->getRepository($entityClass); + $items = $repo->findAll(); + foreach ($items as $index => $item) { + $this->targetEM->persist($item); + } + $this->targetEM->flush(); + } + + $io->progressFinish(); + + + //Fix sequences / auto increment values on target database + $io->info('Fixing sequences / auto increment values on target database...'); + $this->fixAutoIncrements($this->targetEM); + + $io->success('Database migration completed successfully.'); + + if ($io->isVerbose()) { + $io->info('Process took peak memory: ' . round(memory_get_peak_usage(true) / 1024 / 1024, 2) . ' MB'); + } + + return Command::SUCCESS; + } + + /** + * Construct a source EntityManager based on the given connection URL + * @param string $url + * @return EntityManagerInterface + */ + private function getSourceEm(string $url): EntityManagerInterface + { + //Replace any %kernel.project_dir% placeholders + $url = str_replace('%kernel.project_dir%', $this->kernelProjectDir, $url); + + $connectionFactory = new ConnectionFactory(); + $connection = $connectionFactory->createConnection(['url' => $url]); + return new EntityManager($connection, $this->targetEM->getConfiguration()); + } + + private function ensureVersionUpToDate(EntityManagerInterface $sourceEM): void + { + //Ensure that target database is up to date + $migrationStatusCalculator = $this->dependencyFactory->getMigrationStatusCalculator(); + $newMigrations = $migrationStatusCalculator->getNewMigrations(); + if (count($newMigrations->getItems()) > 0) { + throw new \RuntimeException("Target database is not up to date. Please run all migrations (with doctrine:migrations:migrate) before starting the migration process."); + } + + $sourceDependencyLoader = DependencyFactory::fromEntityManager(new ExistingConfiguration($this->dependencyFactory->getConfiguration()), new ExistingEntityManager($sourceEM)); + $sourceMigrationStatusCalculator = $sourceDependencyLoader->getMigrationStatusCalculator(); + $sourceNewMigrations = $sourceMigrationStatusCalculator->getNewMigrations(); + if (count($sourceNewMigrations->getItems()) > 0) { + throw new \RuntimeException("Source database is not up to date. Please run all migrations (with doctrine:migrations:migrate) on the source database before starting the migration process."); + } + } + + private function fixUsers(EntityManagerInterface $sourceEM): void + { + //To avoid a problem with (Column 'settings' cannot be null) in MySQL we need to migrate the user entities first + //and fix the settings and backupCodes fields + + $reflClass = new \ReflectionClass(User::class); + foreach ($sourceEM->getRepository(User::class)->findAll() as $user) { + foreach (['settings', 'backupCodes'] as $field) { + $property = $reflClass->getProperty($field); + if (!$property->isInitialized($user) || $property->getValue($user) === null) { + $property->setValue($user, []); + } + } + $this->targetEM->persist($user); + } + } + + private function fixAutoIncrements(EntityManagerInterface $em): void + { + $connection = $em->getConnection(); + $platform = $connection->getDatabasePlatform(); + + if ($platform instanceof PostgreSQLPlatform) { + $connection->executeStatement( + //See https://github.com/Part-DB/Part-DB-server/issues/1362 + <<datastructureImporter->importPartUnits($data); $io->success('Imported '.$count.' measurement units.'); + //Import the custom states + $io->info('Importing custom states...'); + $count = $this->datastructureImporter->importPartCustomStates($data); + $io->success('Imported '.$count.' custom states.'); + //Import manufacturers $io->info('Importing manufacturers...'); $count = $this->datastructureImporter->importManufacturers($data); diff --git a/src/Command/PopulateKicadCommand.php b/src/Command/PopulateKicadCommand.php new file mode 100644 index 00000000..721e7706 --- /dev/null +++ b/src/Command/PopulateKicadCommand.php @@ -0,0 +1,364 @@ +setHelp('This command populates KiCad footprint paths on Footprint entities and KiCad symbol paths on Category entities based on their names.'); + + $this + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Preview changes without applying them') + ->addOption('footprints', null, InputOption::VALUE_NONE, 'Only update footprint entities') + ->addOption('categories', null, InputOption::VALUE_NONE, 'Only update category entities') + ->addOption('force', null, InputOption::VALUE_NONE, 'Overwrite existing values (by default, only empty values are updated)') + ->addOption('list', null, InputOption::VALUE_NONE, 'List all footprints and categories with their current KiCad values') + ->addOption('mapping-file', null, InputOption::VALUE_REQUIRED, 'Path to a JSON file with custom mappings (merges with built-in defaults)') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $dryRun = $input->getOption('dry-run'); + $footprintsOnly = $input->getOption('footprints'); + $categoriesOnly = $input->getOption('categories'); + $force = $input->getOption('force'); + $list = $input->getOption('list'); + $mappingFile = $input->getOption('mapping-file'); + + // If neither specified, do both + $doFootprints = !$categoriesOnly || $footprintsOnly; + $doCategories = !$footprintsOnly || $categoriesOnly; + + if ($list) { + $this->listCurrentValues($io); + return Command::SUCCESS; + } + + // Load mappings: start with built-in defaults, then merge user-supplied file + ['footprints' => $footprintMappings, 'categories' => $categoryMappings] = $this->getDefaultMappings(); + + if ($mappingFile !== null) { + $customMappings = $this->loadMappingFile($mappingFile, $io); + if ($customMappings === null) { + return Command::FAILURE; + } + if (isset($customMappings['footprints']) && is_array($customMappings['footprints'])) { + // User mappings take priority (overwrite defaults) + $footprintMappings = array_merge($footprintMappings, $customMappings['footprints']); + $io->text(sprintf('Loaded %d custom footprint mappings from %s', count($customMappings['footprints']), $mappingFile)); + } + if (isset($customMappings['categories']) && is_array($customMappings['categories'])) { + $categoryMappings = array_merge($categoryMappings, $customMappings['categories']); + $io->text(sprintf('Loaded %d custom category mappings from %s', count($customMappings['categories']), $mappingFile)); + } + } + + if ($dryRun) { + $io->note('DRY RUN MODE - No changes will be made'); + } + + $totalUpdated = 0; + + if ($doFootprints) { + $updated = $this->updateFootprints($io, $dryRun, $force, $footprintMappings); + $totalUpdated += $updated; + } + + if ($doCategories) { + $updated = $this->updateCategories($io, $dryRun, $force, $categoryMappings); + $totalUpdated += $updated; + } + + if (!$dryRun && $totalUpdated > 0) { + $this->entityManager->flush(); + $io->success(sprintf('Updated %d entities. Run "php bin/console cache:clear" to clear the cache.', $totalUpdated)); + } elseif ($dryRun && $totalUpdated > 0) { + $io->info(sprintf('DRY RUN: Would update %d entities. Run without --dry-run to apply changes.', $totalUpdated)); + } else { + $io->info('No entities needed updating.'); + } + + return Command::SUCCESS; + } + + private function listCurrentValues(SymfonyStyle $io): void + { + $io->section('Current Footprint KiCad Values'); + + $footprintRepo = $this->entityManager->getRepository(Footprint::class); + /** @var Footprint[] $footprints */ + $footprints = $footprintRepo->findAll(); + + $rows = []; + foreach ($footprints as $footprint) { + $kicadValue = $footprint->getEdaInfo()->getKicadFootprint(); + $rows[] = [ + $footprint->getId(), + $footprint->getName(), + $kicadValue ?? '(empty)', + ]; + } + + $io->table(['ID', 'Name', 'KiCad Footprint'], $rows); + + $io->section('Current Category KiCad Values'); + + $categoryRepo = $this->entityManager->getRepository(Category::class); + /** @var Category[] $categories */ + $categories = $categoryRepo->findAll(); + + $rows = []; + foreach ($categories as $category) { + $kicadValue = $category->getEdaInfo()->getKicadSymbol(); + $rows[] = [ + $category->getId(), + $category->getName(), + $kicadValue ?? '(empty)', + ]; + } + + $io->table(['ID', 'Name', 'KiCad Symbol'], $rows); + } + + private function updateFootprints(SymfonyStyle $io, bool $dryRun, bool $force, array $mappings): int + { + $io->section('Updating Footprint Entities'); + + $footprintRepo = $this->entityManager->getRepository(Footprint::class); + /** @var Footprint[] $footprints */ + $footprints = $footprintRepo->findAll(); + + $updated = 0; + $skipped = []; + + foreach ($footprints as $footprint) { + $name = $footprint->getName(); + $currentValue = $footprint->getEdaInfo()->getKicadFootprint(); + + // Skip if already has value and not forcing + if (!$force && $currentValue !== null && $currentValue !== '') { + continue; + } + + // Check for exact match on name first, then try alternative names + $matchedValue = $this->findFootprintMapping($mappings, $name, $footprint->getAlternativeNames()); + + if ($matchedValue !== null) { + $io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $matchedValue)); + + if (!$dryRun) { + $footprint->getEdaInfo()->setKicadFootprint($matchedValue); + } + $updated++; + } else { + // No mapping found + $skipped[] = $name; + } + } + + $io->newLine(); + $io->text(sprintf('Updated: %d footprints', $updated)); + + if (count($skipped) > 0) { + $io->warning(sprintf('No mapping found for %d footprints:', count($skipped))); + foreach ($skipped as $name) { + $io->text(' - ' . $name); + } + } + + return $updated; + } + + private function updateCategories(SymfonyStyle $io, bool $dryRun, bool $force, array $mappings): int + { + $io->section('Updating Category Entities'); + + $categoryRepo = $this->entityManager->getRepository(Category::class); + /** @var Category[] $categories */ + $categories = $categoryRepo->findAll(); + + $updated = 0; + $skipped = []; + + foreach ($categories as $category) { + $name = $category->getName(); + $currentValue = $category->getEdaInfo()->getKicadSymbol(); + + // Skip if already has value and not forcing + if (!$force && $currentValue !== null && $currentValue !== '') { + continue; + } + + // Check for matches using the pattern-based mappings (also check alternative names) + $matchedValue = $this->findCategoryMapping($mappings, $name, $category->getAlternativeNames()); + + if ($matchedValue !== null) { + $io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $matchedValue)); + + if (!$dryRun) { + $category->getEdaInfo()->setKicadSymbol($matchedValue); + } + $updated++; + } else { + $skipped[] = $name; + } + } + + $io->newLine(); + $io->text(sprintf('Updated: %d categories', $updated)); + + if (count($skipped) > 0) { + $io->note(sprintf('No mapping found for %d categories (this is often expected):', count($skipped))); + foreach ($skipped as $name) { + $io->text(' - ' . $name); + } + } + + return $updated; + } + + /** + * Loads a JSON mapping file and returns the parsed data. + * Expected format: {"footprints": {"Name": "KiCad:Path"}, "categories": {"Pattern": "KiCad:Path"}} + * + * @return array|null The parsed mappings, or null on error + */ + private function loadMappingFile(string $path, SymfonyStyle $io): ?array + { + if (!file_exists($path)) { + $io->error(sprintf('Mapping file not found: %s', $path)); + return null; + } + + $content = file_get_contents($path); + if ($content === false) { + $io->error(sprintf('Could not read mapping file: %s', $path)); + return null; + } + + $data = json_decode($content, true); + if (!is_array($data)) { + $io->error(sprintf('Invalid JSON in mapping file: %s', $path)); + return null; + } + + return $data; + } + + private function matchesPattern(string $name, string $pattern): bool + { + // Check for exact match + if ($pattern === $name) { + return true; + } + + // Check for case-insensitive contains + if (stripos($name, $pattern) !== false) { + return true; + } + + return false; + } + + /** + * Finds a footprint mapping by checking the entity name and its alternative names. + * Footprints use exact matching. + * + * @param array $mappings + * @param string $name The primary name of the footprint + * @param string|null $alternativeNames Comma-separated alternative names + * @return string|null The matched KiCad path, or null if no match found + */ + private function findFootprintMapping(array $mappings, string $name, ?string $alternativeNames): ?string + { + // Check primary name + if (isset($mappings[$name])) { + return $mappings[$name]; + } + + // Check alternative names + if ($alternativeNames !== null && $alternativeNames !== '') { + foreach (explode(',', $alternativeNames) as $altName) { + $altName = trim($altName); + if ($altName !== '' && isset($mappings[$altName])) { + return $mappings[$altName]; + } + } + } + + return null; + } + + /** + * Finds a category mapping by checking the entity name and its alternative names. + * Categories use pattern-based matching (case-insensitive contains). + * + * @param array $mappings + * @param string $name The primary name of the category + * @param string|null $alternativeNames Comma-separated alternative names + * @return string|null The matched KiCad symbol path, or null if no match found + */ + private function findCategoryMapping(array $mappings, string $name, ?string $alternativeNames): ?string + { + // Check primary name against all patterns + foreach ($mappings as $pattern => $kicadSymbol) { + if ($this->matchesPattern($name, $pattern)) { + return $kicadSymbol; + } + } + + // Check alternative names against all patterns + if ($alternativeNames !== null && $alternativeNames !== '') { + foreach (explode(',', $alternativeNames) as $altName) { + $altName = trim($altName); + if ($altName === '') { + continue; + } + foreach ($mappings as $pattern => $kicadSymbol) { + if ($this->matchesPattern($altName, $pattern)) { + return $kicadSymbol; + } + } + } + } + + return null; + } + + /** + * Returns the default mappings for footprints and categories. + * @return array{footprints: array, categories: array} + * @throws \JsonException + */ + private function getDefaultMappings(): array + { + $path = $this->projectDir . '/' . self::DEFAULT_MAPPING_FILE; + $content = file_get_contents($path); + + return json_decode($content, true, 512, JSON_THROW_ON_ERROR); + } +} diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php new file mode 100644 index 00000000..ca6c8399 --- /dev/null +++ b/src/Command/UpdateCommand.php @@ -0,0 +1,445 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Command; + +use App\Services\System\UpdateChecker; +use App\Services\System\UpdateExecutor; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand(name: 'partdb:update', description: 'Check for and install Part-DB updates', aliases: ['app:update'])] +class UpdateCommand extends Command +{ + public function __construct(private readonly UpdateChecker $updateChecker, + private readonly UpdateExecutor $updateExecutor) + { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setHelp(<<<'HELP' +The %command.name% command checks for Part-DB updates and can install them. + +Check for updates: + php %command.full_name% --check + +List available versions: + php %command.full_name% --list + +Update to the latest version: + php %command.full_name% + +Update to a specific version: + php %command.full_name% v2.6.0 + +Update without creating a backup (faster but riskier): + php %command.full_name% --no-backup + +Non-interactive update for scripts: + php %command.full_name% --force + +View update logs: + php %command.full_name% --logs +HELP + ) + ->addArgument( + 'version', + InputArgument::OPTIONAL, + 'Target version to update to (e.g., v2.6.0). If not specified, updates to the latest stable version.' + ) + ->addOption( + 'check', + 'c', + InputOption::VALUE_NONE, + 'Only check for updates without installing' + ) + ->addOption( + 'list', + 'l', + InputOption::VALUE_NONE, + 'List all available versions' + ) + ->addOption( + 'no-backup', + null, + InputOption::VALUE_NONE, + 'Skip creating a backup before updating (not recommended)' + ) + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Skip confirmation prompts' + ) + ->addOption( + 'include-prerelease', + null, + InputOption::VALUE_NONE, + 'Include pre-release versions' + ) + ->addOption( + 'logs', + null, + InputOption::VALUE_NONE, + 'Show recent update logs' + ) + ->addOption( + 'refresh', + 'r', + InputOption::VALUE_NONE, + 'Force refresh of cached version information' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + // Handle --logs option + if ($input->getOption('logs')) { + return $this->showLogs($io); + } + + // Handle --refresh option + if ($input->getOption('refresh')) { + $io->text('Refreshing version information...'); + $this->updateChecker->refreshVersionInfo(); + $io->success('Version cache cleared.'); + } + + // Handle --list option + if ($input->getOption('list')) { + return $this->listVersions($io, $input->getOption('include-prerelease')); + } + + // Get update status + $status = $this->updateChecker->getUpdateStatus(); + + // Display current status + $io->title('Part-DB Update Manager'); + + $this->displayStatus($io, $status); + + // Handle --check option + if ($input->getOption('check')) { + return $this->checkOnly($io, $status); + } + + // Validate we can update + $validationResult = $this->validateUpdate($io, $status); + if ($validationResult !== null) { + return $validationResult; + } + + // Determine target version + $targetVersion = $input->getArgument('version'); + $includePrerelease = $input->getOption('include-prerelease'); + + if (!$targetVersion) { + $latest = $this->updateChecker->getLatestVersion($includePrerelease); + if (!$latest) { + $io->error('Could not determine the latest version. Please specify a version manually.'); + return Command::FAILURE; + } + $targetVersion = $latest['tag']; + } + + // Validate target version + if (!$this->updateChecker->isNewerVersionThanCurrent($targetVersion)) { + $io->warning(sprintf( + 'Version %s is not newer than the current version %s.', + $targetVersion, + $status['current_version'] + )); + + if (!$input->getOption('force')) { + if (!$io->confirm('Do you want to proceed anyway?', false)) { + $io->info('Update cancelled.'); + return Command::SUCCESS; + } + } + } + + // Confirm update + if (!$input->getOption('force')) { + $io->section('Update Plan'); + + $io->listing([ + sprintf('Target version: %s', $targetVersion), + $input->getOption('no-backup') + ? 'Backup will be SKIPPED' + : 'A full backup will be created before updating', + 'Maintenance mode will be enabled during update', + 'Database migrations will be run automatically', + 'Cache will be cleared and rebuilt', + ]); + + $io->warning('The update process may take several minutes. Do not interrupt it.'); + + if (!$io->confirm('Do you want to proceed with the update?', false)) { + $io->info('Update cancelled.'); + return Command::SUCCESS; + } + } + + // Execute update + return $this->executeUpdate($io, $targetVersion, !$input->getOption('no-backup')); + } + + private function displayStatus(SymfonyStyle $io, array $status): void + { + $io->definitionList( + ['Current Version' => sprintf('%s', $status['current_version'])], + ['Latest Version' => $status['latest_version'] + ? sprintf('%s', $status['latest_version']) + : 'Unknown'], + ['Installation Type' => $status['installation']['type_name']], + ['Git Branch' => $status['git']['branch'] ?? 'N/A'], + ['Git Commit' => $status['git']['commit'] ?? 'N/A'], + ['Local Changes' => $status['git']['has_local_changes'] + ? 'Yes (update blocked)' + : 'No'], + ['Commits Behind' => $status['git']['commits_behind'] > 0 + ? sprintf('%d', $status['git']['commits_behind']) + : '0'], + ['Update Available' => $status['update_available'] + ? 'Yes' + : 'No'], + ['Can Auto-Update' => $status['can_auto_update'] + ? 'Yes' + : 'No'], + ); + + if (!empty($status['update_blockers'])) { + $io->warning('Update blockers: ' . implode(', ', $status['update_blockers'])); + } + } + + private function checkOnly(SymfonyStyle $io, array $status): int + { + if (!$status['check_enabled']) { + $io->warning('Update checking is disabled in privacy settings.'); + return Command::SUCCESS; + } + + if ($status['update_available']) { + $io->success(sprintf( + 'A new version is available: %s (current: %s)', + $status['latest_version'], + $status['current_version'] + )); + + if ($status['release_url']) { + $io->text(sprintf('Release notes: %s', $status['release_url'], $status['release_url'])); + } + + if ($status['can_auto_update']) { + $io->text(''); + $io->text('Run php bin/console partdb:update to update.'); + } else { + $io->text(''); + $io->text($status['installation']['update_instructions']); + } + + return Command::SUCCESS; + } + + $io->success('You are running the latest version.'); + return Command::SUCCESS; + } + + private function validateUpdate(SymfonyStyle $io, array $status): ?int + { + // Check if update checking is enabled + if (!$status['check_enabled']) { + $io->error('Update checking is disabled in privacy settings. Enable it to use automatic updates.'); + return Command::FAILURE; + } + + // Check installation type + if (!$status['can_auto_update']) { + $io->error('Automatic updates are not supported for this installation type.'); + $io->text($status['installation']['update_instructions']); + return Command::FAILURE; + } + + // Validate preconditions + $validation = $this->updateExecutor->validateUpdatePreconditions(); + if (!$validation['valid']) { + $io->error('Cannot proceed with update:'); + $io->listing($validation['errors']); + return Command::FAILURE; + } + + return null; + } + + private function executeUpdate(SymfonyStyle $io, string $targetVersion, bool $createBackup): int + { + $io->section('Executing Update'); + $io->text(sprintf('Updating to version: %s', $targetVersion)); + $io->text(''); + + $progressCallback = function (array $step) use ($io): void { + $icon = $step['success'] ? '✓' : '✗'; + $duration = $step['duration'] ? sprintf(' (%.1fs)', $step['duration']) : ''; + $io->text(sprintf(' %s %s: %s%s', $icon, $step['step'], $step['message'], $duration)); + }; + + // Use executeUpdateWithProgress to update the progress file for web UI + $result = $this->updateExecutor->executeUpdateWithProgress($targetVersion, $createBackup, $progressCallback); + + $io->text(''); + + if ($result['success']) { + $io->success(sprintf( + 'Successfully updated to %s in %.1f seconds!', + $targetVersion, + $result['duration'] + )); + + $io->text([ + sprintf('Rollback tag: %s', $result['rollback_tag']), + sprintf('Log file: %s', $result['log_file']), + ]); + + $io->note('If you encounter any issues, you can rollback using: git checkout ' . $result['rollback_tag']); + + return Command::SUCCESS; + } + + $io->error('Update failed: ' . $result['error']); + + if ($result['rollback_tag']) { + $io->warning(sprintf('System was rolled back to: %s', $result['rollback_tag'])); + } + + if ($result['log_file']) { + $io->text(sprintf('See log file for details: %s', $result['log_file'])); + } + + return Command::FAILURE; + } + + private function listVersions(SymfonyStyle $io, bool $includePrerelease): int + { + $releases = $this->updateChecker->getAvailableReleases(15); + $currentVersion = $this->updateChecker->getCurrentVersionString(); + + if (empty($releases)) { + $io->warning('Could not fetch available versions. Check your internet connection.'); + return Command::FAILURE; + } + + $io->title('Available Part-DB Versions'); + + $table = new Table($io); + $table->setHeaders(['Tag', 'Version', 'Released', 'Status']); + + foreach ($releases as $release) { + if (!$includePrerelease && $release['prerelease']) { + continue; + } + + $version = $release['version']; + $status = []; + + if (version_compare($version, $currentVersion, '=')) { + $status[] = 'current'; + } elseif (version_compare($version, $currentVersion, '>')) { + $status[] = 'newer'; + } + + if ($release['prerelease']) { + $status[] = 'pre-release'; + } + + $table->addRow([ + $release['tag'], + $version, + (new \DateTime($release['published_at']))->format('Y-m-d'), + implode(' ', $status) ?: '-', + ]); + } + + $table->render(); + + $io->text(''); + $io->text('Use php bin/console partdb:update [tag] to update to a specific version.'); + + return Command::SUCCESS; + } + + private function showLogs(SymfonyStyle $io): int + { + $logs = $this->updateExecutor->getUpdateLogs(); + + if (empty($logs)) { + $io->info('No update logs found.'); + return Command::SUCCESS; + } + + $io->title('Recent Update Logs'); + + $table = new Table($io); + $table->setHeaders(['Date', 'File', 'Size']); + + foreach (array_slice($logs, 0, 10) as $log) { + $table->addRow([ + date('Y-m-d H:i:s', $log['date']), + $log['file'], + $this->formatBytes($log['size']), + ]); + } + + $table->render(); + + $io->text(''); + $io->text('Log files are stored in: var/log/updates/'); + + return Command::SUCCESS; + } + + private function formatBytes(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $unitIndex = 0; + + while ($bytes >= 1024 && $unitIndex < count($units) - 1) { + $bytes /= 1024; + $unitIndex++; + } + + return sprintf('%.1f %s', $bytes, $units[$unitIndex]); + } +} diff --git a/src/Command/VersionCommand.php b/src/Command/VersionCommand.php index d2ce75e1..d09def8f 100644 --- a/src/Command/VersionCommand.php +++ b/src/Command/VersionCommand.php @@ -22,9 +22,9 @@ declare(strict_types=1); */ namespace App\Command; -use Symfony\Component\Console\Attribute\AsCommand; -use App\Services\Misc\GitVersionInfo; +use App\Services\System\GitVersionInfoProvider; use Shivas\VersioningBundle\Service\VersionManagerInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -33,7 +33,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand('partdb:version|app:version', 'Shows the currently installed version of Part-DB.')] class VersionCommand extends Command { - public function __construct(protected VersionManagerInterface $versionManager, protected GitVersionInfo $gitVersionInfo) + public function __construct(protected VersionManagerInterface $versionManager, protected GitVersionInfoProvider $gitVersionInfo) { parent::__construct(); } @@ -48,9 +48,9 @@ class VersionCommand extends Command $message = 'Part-DB version: '. $this->versionManager->getVersion()->toString(); - if ($this->gitVersionInfo->getGitBranchName() !== null) { - $message .= ' Git branch: '. $this->gitVersionInfo->getGitBranchName(); - $message .= ', Git commit: '. $this->gitVersionInfo->getGitCommitHash(); + if ($this->gitVersionInfo->getBranchName() !== null) { + $message .= ' Git branch: '. $this->gitVersionInfo->getBranchName(); + $message .= ', Git commit: '. $this->gitVersionInfo->getCommitHash(); } $io->success($message); diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index edc5917a..b7c25229 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -34,6 +34,7 @@ use App\Entity\Base\PartsContainingRepositoryInterface; use App\Entity\LabelSystem\LabelProcessMode; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; +use App\Entity\UserSystem\User; use App\Exceptions\AttachmentDownloadException; use App\Exceptions\TwigModeException; use App\Form\AdminPages\ImportType; @@ -195,6 +196,10 @@ abstract class BaseAdminController extends AbstractController $this->commentHelper->setMessage($form['log_comment']->getData()); + //In principle, the form should be disabled, if the edit permission is not granted, but for good measure, we also check it here, before saving changes. + if (!$entity instanceof User) { //Users entities does not have a simple edit permission, so we skip the check for them + $this->denyAccessUnlessGranted('edit', $entity); + } $em->persist($entity); $em->flush(); $this->addFlash('success', 'entity.edit_flash'); @@ -232,6 +237,7 @@ abstract class BaseAdminController extends AbstractController 'timeTravel' => $timeTravel_timestamp, 'repo' => $repo, 'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface, + 'showParameters' => !($this instanceof PartCustomStateController), ]); } @@ -365,6 +371,14 @@ abstract class BaseAdminController extends AbstractController } } + //Count how many actual new entities were created (id is null until persisted) + $created_count = 0; + foreach ($results as $result) { + if (null === $result->getID()) { + $created_count++; + } + } + //Persist valid entities to DB foreach ($results as $result) { $em->persist($result); @@ -372,8 +386,14 @@ abstract class BaseAdminController extends AbstractController $em->flush(); if (count($results) > 0) { - $this->addFlash('success', t('entity.mass_creation_flash', ['%COUNT%' => count($results)])); + $this->addFlash('success', t('entity.mass_creation_flash', ['%COUNT%' => $created_count])); } + + if (count($errors)) { + //Recreate mass creation form, so we get the updated parent list and empty lines + $mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]); + } + } return $this->render($this->twig_template, [ @@ -382,6 +402,7 @@ abstract class BaseAdminController extends AbstractController 'import_form' => $import_form, 'mass_creation_form' => $mass_creation_form, 'route_base' => $this->route_base, + 'showParameters' => !($this instanceof PartCustomStateController), ]); } diff --git a/src/Controller/AdminPages/PartCustomStateController.php b/src/Controller/AdminPages/PartCustomStateController.php new file mode 100644 index 00000000..60f63abf --- /dev/null +++ b/src/Controller/AdminPages/PartCustomStateController.php @@ -0,0 +1,83 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller\AdminPages; + +use App\Entity\Attachments\PartCustomStateAttachment; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Entity\Parts\PartCustomState; +use App\Form\AdminPages\PartCustomStateAdminForm; +use App\Services\ImportExportSystem\EntityExporter; +use App\Services\ImportExportSystem\EntityImporter; +use App\Services\Trees\StructuralElementRecursionHelper; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +/** + * @see \App\Tests\Controller\AdminPages\PartCustomStateControllerTest + */ +#[Route(path: '/part_custom_state')] +class PartCustomStateController extends BaseAdminController +{ + protected string $entity_class = PartCustomState::class; + protected string $twig_template = 'admin/part_custom_state_admin.html.twig'; + protected string $form_class = PartCustomStateAdminForm::class; + protected string $route_base = 'part_custom_state'; + protected string $attachment_class = PartCustomStateAttachment::class; + protected ?string $parameter_class = PartCustomStateParameter::class; + + #[Route(path: '/{id}', name: 'part_custom_state_delete', methods: ['DELETE'])] + public function delete(Request $request, PartCustomState $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse + { + return $this->_delete($request, $entity, $recursionHelper); + } + + #[Route(path: '/{id}/edit/{timestamp}', name: 'part_custom_state_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] + public function edit(PartCustomState $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response + { + return $this->_edit($entity, $request, $em, $timestamp); + } + + #[Route(path: '/new', name: 'part_custom_state_new')] + #[Route(path: '/{id}/clone', name: 'part_custom_state_clone')] + #[Route(path: '/')] + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?PartCustomState $entity = null): Response + { + return $this->_new($request, $em, $importer, $entity); + } + + #[Route(path: '/export', name: 'part_custom_state_export_all')] + public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response + { + return $this->_exportAll($em, $exporter, $request); + } + + #[Route(path: '/{id}/export', name: 'part_custom_state_export')] + public function exportEntity(PartCustomState $entity, EntityExporter $exporter, Request $request): Response + { + return $this->_exportEntity($entity, $exporter, $request); + } +} diff --git a/src/Controller/AttachmentFileController.php b/src/Controller/AttachmentFileController.php index 81369e12..01aeab11 100644 --- a/src/Controller/AttachmentFileController.php +++ b/src/Controller/AttachmentFileController.php @@ -30,6 +30,7 @@ use App\Form\Filters\AttachmentFilterType; use App\Services\Attachments\AttachmentManager; use App\Services\Trees\NodesListBuilder; use App\Settings\BehaviorSettings\TableSettings; +use App\Settings\SystemSettings\AttachmentsSettings; use Omines\DataTablesBundle\DataTableFactory; use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -41,31 +42,56 @@ use Symfony\Component\Routing\Attribute\Route; class AttachmentFileController extends AbstractController { + + public function __construct(private readonly AttachmentManager $helper) + { + + } + + #[Route(path: '/attachment/{id}/sandbox', name: 'attachment_html_sandbox')] + public function htmlSandbox(Attachment $attachment, AttachmentsSettings $attachmentsSettings): Response + { + //Check if the sandbox is enabled in the settings, as it can be a security risk if used without proper precautions, so it should be opt-in + if (!$attachmentsSettings->showHTMLAttachments) { + throw $this->createAccessDeniedException('The HTML sandbox for attachments is disabled in the settings, as it can be a security risk if used without proper precautions. Please enable it in the settings if you want to use it.'); + } + + $this->checkPermissions($attachment); + + $file_path = $this->helper->toAbsoluteInternalFilePath($attachment); + + $attachmentContent = file_get_contents($file_path); + + $response = $this->render('attachments/html_sandbox.html.twig', [ + 'attachment' => $attachment, + 'content' => $attachmentContent, + ]); + + //Set an CSP that allows to run inline scripts, styles and images from external ressources, but does not allow any connections or others. + //Also set the sandbox CSP directive with only "allow-script" to run basic scripts + $response->headers->set('Content-Security-Policy', "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline' *; img-src data: *; sandbox allow-scripts allow-downloads allow-modals;"); + + //Forbid to embed the attachment render page in an iframe to prevent clickjacking, as it is not used anywhere else for now + $response->headers->set('X-Frame-Options', 'DENY'); + + return $response; + } + /** * Download the selected attachment. */ #[Route(path: '/attachment/{id}/download', name: 'attachment_download')] - public function download(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse + public function download(Attachment $attachment): BinaryFileResponse { - $this->denyAccessUnlessGranted('read', $attachment); + $this->checkPermissions($attachment); - if ($attachment->isSecure()) { - $this->denyAccessUnlessGranted('show_private', $attachment); - } - - if (!$attachment->hasInternal()) { - throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); - } - - if (!$helper->isInternalFileExisting($attachment)) { - throw $this->createNotFoundException('The file associated with the attachment is not existing!'); - } - - $file_path = $helper->toAbsoluteInternalFilePath($attachment); + $file_path = $this->helper->toAbsoluteInternalFilePath($attachment); $response = new BinaryFileResponse($file_path); + $response = $this->forbidHTMLContentType($response); + //Set header content disposition, so that the file will be downloaded - $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT); + $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $attachment->getFilename()); return $response; } @@ -74,7 +100,35 @@ class AttachmentFileController extends AbstractController * View the attachment. */ #[Route(path: '/attachment/{id}/view', name: 'attachment_view')] - public function view(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse + public function view(Attachment $attachment): BinaryFileResponse + { + $this->checkPermissions($attachment); + + $file_path = $this->helper->toAbsoluteInternalFilePath($attachment); + $response = new BinaryFileResponse($file_path); + + $response = $this->forbidHTMLContentType($response); + + //Set header content disposition, so that the file will be downloaded + $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $attachment->getFilename()); + + return $response; + } + + private function forbidHTMLContentType(BinaryFileResponse $response): BinaryFileResponse + { + $mimeType = $response->getFile()->getMimeType(); + + if ($mimeType === 'text/html') { + $mimeType = 'text/plain'; + } + + $response->headers->set('Content-Type', $mimeType); + + return $response; + } + + private function checkPermissions(Attachment $attachment): void { $this->denyAccessUnlessGranted('read', $attachment); @@ -86,17 +140,9 @@ class AttachmentFileController extends AbstractController throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); } - if (!$helper->isInternalFileExisting($attachment)) { + if (!$this->helper->isInternalFileExisting($attachment)) { throw $this->createNotFoundException('The file associated with the attachment is not existing!'); } - - $file_path = $helper->toAbsoluteInternalFilePath($attachment); - $response = new BinaryFileResponse($file_path); - - //Set header content disposition, so that the file will be downloaded - $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE); - - return $response; } #[Route(path: '/attachment/list', name: 'attachment_list')] diff --git a/src/Controller/BatchEdaController.php b/src/Controller/BatchEdaController.php new file mode 100644 index 00000000..82c4bb48 --- /dev/null +++ b/src/Controller/BatchEdaController.php @@ -0,0 +1,117 @@ + + */ + private function getSharedEdaValues(array $parts): array + { + $fields = [ + 'reference_prefix' => static fn (Part $p) => $p->getEdaInfo()->getReferencePrefix(), + 'value' => static fn (Part $p) => $p->getEdaInfo()->getValue(), + 'kicad_symbol' => static fn (Part $p) => $p->getEdaInfo()->getKicadSymbol(), + 'kicad_footprint' => static fn (Part $p) => $p->getEdaInfo()->getKicadFootprint(), + 'visibility' => static fn (Part $p) => $p->getEdaInfo()->getVisibility(), + 'exclude_from_bom' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromBom(), + 'exclude_from_board' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromBoard(), + 'exclude_from_sim' => static fn (Part $p) => $p->getEdaInfo()->getExcludeFromSim(), + ]; + + $data = []; + foreach ($fields as $key => $getter) { + $values = array_map($getter, $parts); + $unique = array_unique($values, SORT_REGULAR); + if (count($unique) === 1) { + $data[$key] = $unique[array_key_first($unique)]; + } + } + + return $data; + } + + #[Route('/tools/batch_eda_edit', name: 'batch_eda_edit')] + public function batchEdaEdit(Request $request): Response + { + $this->denyAccessUnlessGranted('@parts.edit'); + + $ids = $request->query->getString('ids', ''); + $redirectUrl = $request->query->getString('_redirect', ''); + + //Parse part IDs and load parts + $idArray = array_filter(array_map(intval(...), explode(',', $ids)), static fn (int $id): bool => $id > 0); + $parts = $this->entityManager->getRepository(Part::class)->findBy(['id' => $idArray]); + + if ($parts === []) { + $this->addFlash('error', 'batch_eda.no_parts_selected'); + + return $redirectUrl !== '' ? $this->redirect($redirectUrl) : $this->redirectToRoute('parts_show_all'); + } + + //Pre-populate form with shared values (when all parts have the same value) + $initialData = $this->getSharedEdaValues($parts); + $form = $this->createForm(BatchEdaType::class, $initialData); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + foreach ($parts as $part) { + $this->denyAccessUnlessGranted('edit', $part); + $edaInfo = $part->getEdaInfo(); + + if ($form->get('apply_reference_prefix')->getData()) { + $edaInfo->setReferencePrefix($form->get('reference_prefix')->getData() ?: null); + } + if ($form->get('apply_value')->getData()) { + $edaInfo->setValue($form->get('value')->getData() ?: null); + } + if ($form->get('apply_kicad_symbol')->getData()) { + $edaInfo->setKicadSymbol($form->get('kicad_symbol')->getData() ?: null); + } + if ($form->get('apply_kicad_footprint')->getData()) { + $edaInfo->setKicadFootprint($form->get('kicad_footprint')->getData() ?: null); + } + if ($form->get('apply_visibility')->getData()) { + $edaInfo->setVisibility($form->get('visibility')->getData()); + } + if ($form->get('apply_exclude_from_bom')->getData()) { + $edaInfo->setExcludeFromBom($form->get('exclude_from_bom')->getData()); + } + if ($form->get('apply_exclude_from_board')->getData()) { + $edaInfo->setExcludeFromBoard($form->get('exclude_from_board')->getData()); + } + if ($form->get('apply_exclude_from_sim')->getData()) { + $edaInfo->setExcludeFromSim($form->get('exclude_from_sim')->getData()); + } + } + + $this->entityManager->flush(); + $this->addFlash('success', 'batch_eda.success'); + + return $redirectUrl !== '' ? $this->redirect($redirectUrl) : $this->redirectToRoute('parts_show_all'); + } + + return $this->render('parts/batch_eda_edit.html.twig', [ + 'form' => $form->createView(), + 'parts' => $parts, + 'redirect_url' => $redirectUrl, + ]); + } +} diff --git a/src/Controller/BrowserPluginController.php b/src/Controller/BrowserPluginController.php new file mode 100644 index 00000000..1bb95787 --- /dev/null +++ b/src/Controller/BrowserPluginController.php @@ -0,0 +1,139 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller; + +use App\Entity\UserSystem\User; +use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Services\InfoProviderSystem\SubmittedPageStorage; +use App\Services\InfoProviderSystem\DTOs\BrowserSubmittedPage; +use App\Settings\InfoProviderSystem\BrowserPluginSettings; +use App\Settings\SystemSettings\CustomizationSettings; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * Provides the endpoint used by browser extensions to submit the current page's HTML to Part-DB, + * so that info providers can use it instead of fetching the URL themselves. + */ +#[Route('/tools/info_providers')] +class BrowserPluginController extends AbstractController +{ + public function __construct( + private readonly SubmittedPageStorage $browserHtmlStorage, + private readonly ProviderRegistry $providerRegistry, + private readonly CustomizationSettings $customizationSettings, + private readonly BrowserPluginSettings $browserPluginSettings, + ) { + } + + private const URL_PROVIDER_KEYS = ['generic_web', 'ai_web']; + + /** + * Returns instance info for the browser extension: logged-in username, instance name, and active URL providers. + * + * Response: { "username": "admin", "instance_name": "Part-DB", "url_providers": [{"id": "generic_web", "label": "Generic Web URL"}] } + */ + #[Route('/browser_info', name: 'browser_plugin_info', methods: ['GET'])] + public function getInfo(): JsonResponse + { + $this->denyAccessUnlessGranted('@info_providers.create_parts'); + $this->throwIfDisabled(); + + $activeProviders = $this->providerRegistry->getActiveProviders(); + + $urlProviders = []; + foreach (self::URL_PROVIDER_KEYS as $key) { + if (isset($activeProviders[$key])) { + $urlProviders[] = [ + 'id' => $key, + 'label' => $activeProviders[$key]->getProviderInfo()['name'], + ]; + } + } + + $user = $this->getUser(); + if ($user instanceof User) { + $username = $user->getFullName(true); + } else { + $username = $user ? $user->getUserIdentifier() : "unknown"; + } + + return new JsonResponse([ + 'username' => $username, + 'instance_name' => $this->customizationSettings->instanceName, + 'url_providers' => $urlProviders, + ]); + } + + /** + * Accepts a JSON POST body with the HTML of the current page from a browser extension. + * Stores the HTML in the session via BrowserHtmlSessionStorage and returns a redirect URL + * pointing to the standard part-creation flow with use_browser_html=1. + * + * Expected JSON body: { "html": "", "url": "https://example.com/product", "provider": "generic_web" } + * The "provider" field is optional and defaults to "generic_web". Use "ai_web" for the AI extractor. + * Response: { "redirect_url": "https://partdb.example.com/en/part/from_info_provider/generic_web/https%3A%2F%2F.../create?use_browser_html=1&no_cache=1" } + */ + #[Route('/browser_html', name: 'browser_plugin_submit_html', methods: ['POST'])] + public function submitHtml(Request $request, + #[MapRequestPayload] + BrowserSubmittedPage $page + ): JsonResponse + { + $this->denyAccessUnlessGranted('@info_providers.create_parts'); + $this->throwIfDisabled(); + + $payload = $request->getPayload(); + + $provider = $payload->get('provider', null); + + // The maprequestpayload already validates the URL and HTML content: + $token = $this->browserHtmlStorage->store($page); + + if ($provider !== null) { + $redirectUrl = $this->generateUrl('info_providers_create_part', [ + 'providerKey' => $provider, + 'providerId' => $page->url, + 'submitted_page_token' => $token, + ], UrlGeneratorInterface::ABSOLUTE_URL); + } + + return new JsonResponse([ + 'redirect_url' => $redirectUrl ?? null, + ]); + } + + public function throwIfDisabled(): void + { + if (!$this->browserPluginSettings->enabled) { + throw HttpException::fromStatusCode(451, "Browser plugin feature is disabled by the administrator, ask him to enable it in system settings."); + } + } +} diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php index 2d3dd7f6..a8622a28 100644 --- a/src/Controller/BulkInfoProviderImportController.php +++ b/src/Controller/BulkInfoProviderImportController.php @@ -29,11 +29,14 @@ use App\Entity\Parts\Part; use App\Entity\Parts\Supplier; use App\Entity\UserSystem\User; use App\Form\InfoProviderSystem\GlobalFieldMappingType; +use App\Services\EntityMergers\Mergers\PartMerger; use App\Services\InfoProviderSystem\BulkInfoProviderService; use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO; use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO; use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO; +use App\Services\InfoProviderSystem\PartInfoRetriever; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\ORMInvalidArgumentException; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -66,6 +69,10 @@ class BulkInfoProviderImportController extends AbstractController { $dtos = []; foreach ($fieldMappings as $mapping) { + // Skip entries where field is null/empty (e.g. user added a row but didn't select a field) + if (empty($mapping['field'])) { + continue; + } $dtos[] = new BulkSearchFieldMappingDTO(field: $mapping['field'], providers: $mapping['providers'], priority: $mapping['priority'] ?? 1); } return $dtos; @@ -276,8 +283,8 @@ class BulkInfoProviderImportController extends AbstractController $updatedJobs = true; } - // Mark jobs with no results for deletion (failed searches) - if ($job->getResultCount() === 0 && $job->isInProgress()) { + // Mark jobs with no results for deletion (failed searches or stale pending) + if ($job->getResultCount() === 0 && ($job->isInProgress() || $job->isPending())) { $jobsToDelete[] = $job; } } @@ -297,9 +304,23 @@ class BulkInfoProviderImportController extends AbstractController } } + // Refetch after cleanup and split into active vs finished + $allJobs = $this->entityManager->getRepository(BulkInfoProviderImportJob::class) + ->findBy([], ['createdAt' => 'DESC']); + + $activeJobs = []; + $finishedJobs = []; + foreach ($allJobs as $job) { + if ($job->isCompleted() || $job->isFailed() || $job->isStopped()) { + $finishedJobs[] = $job; + } else { + $activeJobs[] = $job; + } + } + return $this->render('info_providers/bulk_import/manage.html.twig', [ - 'jobs' => $this->entityManager->getRepository(BulkInfoProviderImportJob::class) - ->findBy([], ['createdAt' => 'DESC']) // Refetch after cleanup + 'active_jobs' => $activeJobs, + 'finished_jobs' => $finishedJobs, ]); } @@ -470,22 +491,13 @@ class BulkInfoProviderImportController extends AbstractController $fieldMappingDtos = $job->getFieldMappings(); $prefetchDetails = $job->isPrefetchDetails(); - try { - $searchResultsDto = $this->bulkService->performBulkSearch([$part], $fieldMappingDtos, $prefetchDetails); - } catch (\Exception $searchException) { - // Handle "no search results found" as a normal case, not an error - if (str_contains($searchException->getMessage(), 'No search results found')) { - $searchResultsDto = null; - } else { - throw $searchException; - } - } + $searchResultsDto = $this->bulkService->performBulkSearch([$part], $fieldMappingDtos, $prefetchDetails); // Update the job's search results for this specific part efficiently $this->updatePartSearchResults($job, $searchResultsDto[0] ?? null); // Prefetch details if requested - if ($prefetchDetails && $searchResultsDto !== null) { + if ($prefetchDetails) { $this->bulkService->prefetchDetailsForResults($searchResultsDto); } @@ -515,6 +527,191 @@ class BulkInfoProviderImportController extends AbstractController } } + #[Route('/job/{jobId}/part/{partId}/quick-apply', name: 'bulk_info_provider_quick_apply', methods: ['POST'])] + public function quickApply( + int $jobId, + int $partId, + Request $request, + PartInfoRetriever $infoRetriever, + PartMerger $partMerger + ): JsonResponse { + $job = $this->validateJobAccess($jobId); + if (!$job) { + return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]); + } + + /** @var Part $part */ + $part = $this->entityManager->getRepository(Part::class)->find($partId); + if (!$part) { + return $this->createErrorResponse('Part not found', 404, ['part_id' => $partId]); + } + + $this->denyAccessUnlessGranted('edit', $part); + + // Get provider key/id from request body, or fall back to top search result + $body = $request->toArray(); + $providerKey = $body['providerKey'] ?? null; + $providerId = $body['providerId'] ?? null; + + if (!$providerKey || !$providerId) { + $searchResults = $job->getSearchResults($this->entityManager); + foreach ($searchResults->partResults as $partResult) { + if ($partResult->part->getId() === $partId) { + $sorted = $partResult->getResultsSortedByPriority(); + if (!empty($sorted)) { + $providerKey = $sorted[0]->searchResult->provider_key; + $providerId = $sorted[0]->searchResult->provider_id; + } + break; + } + } + } + + if (!$providerKey || !$providerId) { + return $this->createErrorResponse('No search result available for this part', 400, ['part_id' => $partId]); + } + + try { + $dto = $infoRetriever->getDetails($providerKey, $providerId); + $providerPart = $infoRetriever->dtoToPart($dto); + $partMerger->merge($part, $providerPart); + + //Persist part manufacturer and supplier if they are new, to avoid issues with detached entities during merge + //Do not footprints here, as it might pollute the database with unwanted formatting footprints from the provider, + $this->entityManager->persist($part->getManufacturer()); + foreach ($part->getOrderdetails() as $orderdetail) { + $this->entityManager->persist($orderdetail->getSupplier()); + } + + try { + $this->entityManager->flush(); + } catch (ORMInvalidArgumentException $exception) { + if (str_contains($exception->getMessage(), 'not configured to cascade persist operations')) { + throw new \RuntimeException('Failed to persist merged part, as it would create new datastructures! Review the provider data by yourself.'); + } + + throw $exception; // Re-throw if it's a different ORM error + } + + $job->markPartAsCompleted($partId); + if ($job->isAllPartsCompleted() && !$job->isCompleted()) { + $job->markAsCompleted(); + } + + $this->entityManager->flush(); + + + return $this->json([ + 'success' => true, + 'message' => sprintf('Applied provider data to "%s"', $part->getName()), + 'part_id' => $partId, + 'provider_key' => $providerKey, + 'provider_id' => $providerId, + 'progress' => $job->getProgressPercentage(), + 'completed_count' => $job->getCompletedPartsCount(), + 'total_count' => $job->getPartCount(), + 'job_completed' => $job->isCompleted(), + ]); + } catch (\Exception $e) { + $this->logger->error($e); + + return $this->createErrorResponse( + 'Quick apply failed: ' . $e->getMessage(), + 500, + ['job_id' => $jobId, 'part_id' => $partId, 'exception' => $e->getMessage()] + ); + } + } + + #[Route('/job/{jobId}/quick-apply-all', name: 'bulk_info_provider_quick_apply_all', methods: ['POST'])] + public function quickApplyAll( + int $jobId, + PartInfoRetriever $infoRetriever, + PartMerger $partMerger + ): JsonResponse { + set_time_limit(600); + + $job = $this->validateJobAccess($jobId); + if (!$job) { + return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]); + } + + $searchResults = $job->getSearchResults($this->entityManager); + $applied = 0; + $failed = 0; + $noResults = 0; + $errors = []; + + foreach ($job->getJobParts() as $jobPart) { + if ($jobPart->isCompleted() || $jobPart->isSkipped()) { + continue; + } + + $part = $jobPart->getPart(); + + if (!$this->isGranted('edit', $part)) { + $errors[] = sprintf('No edit permission for "%s"', $part->getName()); + $failed++; + continue; + } + + // Find top search result for this part + $providerKey = null; + $providerId = null; + foreach ($searchResults->partResults as $partResult) { + if ($partResult->part->getId() === $part->getId()) { + $sorted = $partResult->getResultsSortedByPriority(); + if (!empty($sorted)) { + $providerKey = $sorted[0]->searchResult->provider_key; + $providerId = $sorted[0]->searchResult->provider_id; + } + break; + } + } + + if (!$providerKey || !$providerId) { + $noResults++; + continue; + } + + try { + $dto = $infoRetriever->getDetails($providerKey, $providerId); + $providerPart = $infoRetriever->dtoToPart($dto); + $partMerger->merge($part, $providerPart); + $this->entityManager->flush(); + + $job->markPartAsCompleted($part->getId()); + $applied++; + } catch (\Exception $e) { + $this->logger->error('Quick apply failed for part', [ + 'part_id' => $part->getId(), + 'part_name' => $part->getName(), + 'error' => $e->getMessage(), + ]); + $errors[] = sprintf('Failed for "%s": %s', $part->getName(), $e->getMessage()); + $failed++; + } + } + + if ($job->isAllPartsCompleted() && !$job->isCompleted()) { + $job->markAsCompleted(); + } + $this->entityManager->flush(); + + return $this->json([ + 'success' => true, + 'applied' => $applied, + 'failed' => $failed, + 'no_results' => $noResults, + 'errors' => $errors, + 'message' => sprintf('Applied to %d parts, %d failed, %d had no results', $applied, $failed, $noResults), + 'progress' => $job->getProgressPercentage(), + 'completed_count' => $job->getCompletedPartsCount(), + 'total_count' => $job->getPartCount(), + 'job_completed' => $job->isCompleted(), + ]); + } + #[Route('/job/{jobId}/research-all', name: 'bulk_info_provider_research_all', methods: ['POST'])] public function researchAllParts(int $jobId): JsonResponse { diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php index 076e790b..6f863a3c 100644 --- a/src/Controller/HomepageController.php +++ b/src/Controller/HomepageController.php @@ -24,9 +24,9 @@ namespace App\Controller; use App\DataTables\LogDataTable; use App\Entity\Parts\Part; -use App\Services\Misc\GitVersionInfo; use App\Services\System\BannerHelper; -use App\Services\System\UpdateAvailableManager; +use App\Services\System\GitVersionInfoProvider; +use App\Services\System\UpdateAvailableFacade; use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -43,8 +43,8 @@ class HomepageController extends AbstractController #[Route(path: '/', name: 'homepage')] - public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager, - UpdateAvailableManager $updateAvailableManager): Response + public function homepage(Request $request, GitVersionInfoProvider $versionInfo, EntityManagerInterface $entityManager, + UpdateAvailableFacade $updateAvailableManager): Response { $this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS'); @@ -77,8 +77,8 @@ class HomepageController extends AbstractController return $this->render('homepage.html.twig', [ 'banner' => $this->bannerHelper->getBanner(), - 'git_branch' => $versionInfo->getGitBranchName(), - 'git_commit' => $versionInfo->getGitCommitHash(), + 'git_branch' => $versionInfo->getBranchName(), + 'git_commit' => $versionInfo->getCommitHash(), 'show_first_steps' => $show_first_steps, 'datatable' => $table, 'new_version_available' => $updateAvailableManager->isUpdateAvailable(), diff --git a/src/Controller/InfoProviderController.php b/src/Controller/InfoProviderController.php index b79c307c..28c281d0 100644 --- a/src/Controller/InfoProviderController.php +++ b/src/Controller/InfoProviderController.php @@ -26,10 +26,15 @@ namespace App\Controller; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; use App\Exceptions\OAuthReconnectRequiredException; +use App\Form\InfoProviderSystem\FromURLFormType; use App\Form\InfoProviderSystem\PartSearchType; +use App\Services\InfoProviderSystem\SubmittedPageStorage; use App\Services\InfoProviderSystem\ExistingPartFinder; +use App\Services\InfoProviderSystem\CreateFromUrlHelper; use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Services\InfoProviderSystem\Providers\GenericWebProvider; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Settings\AppSettings; use App\Settings\InfoProviderSystem\InfoProviderGeneralSettings; use Doctrine\ORM\EntityManagerInterface; @@ -39,11 +44,15 @@ use Psr\Log\LoggerInterface; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; + use function Symfony\Component\Translation\t; #[Route('/tools/info_providers')] @@ -54,7 +63,8 @@ class InfoProviderController extends AbstractController private readonly PartInfoRetriever $infoRetriever, private readonly ExistingPartFinder $existingPartFinder, private readonly SettingsManagerInterface $settingsManager, - private readonly SettingsFormFactoryInterface $settingsFormFactory + private readonly SettingsFormFactoryInterface $settingsFormFactory, + private readonly SubmittedPageStorage $browserHtmlStorage, ) { @@ -167,10 +177,15 @@ class InfoProviderController extends AbstractController $keyword = $form->get('keyword')->getData(); $providers = $form->get('providers')->getData(); + $no_cache_search = $form->get('no_cache_search')->getData(); + $no_cache_details = $form->get('no_cache_details')->getData(); + $dtos = []; try { - $dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers); + $dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers, options: [ + InfoProviderInterface::OPTION_NO_CACHE => $no_cache_search + ]); } catch (ClientException $e) { $this->addFlash('error', t('info_providers.search.error.client_exception')); $this->addFlash('error',$e->getMessage()); @@ -178,6 +193,13 @@ class InfoProviderController extends AbstractController $exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]); } catch (OAuthReconnectRequiredException $e) { $this->addFlash('error', t('info_providers.search.error.oauth_reconnect', ['%provider%' => $e->getProviderName()])); + } catch (TransportException $e) { + $this->addFlash('error', t('info_providers.search.error.transport_exception')); + $exceptionLogger->error('Transport error during info provider search: ' . $e->getMessage(), ['exception' => $e]); + } catch (\RuntimeException $e) { + $this->addFlash('error', t('info_providers.search.error.general_exception', ['%type%' => (new \ReflectionClass($e))->getShortName()])); + //Log the exception + $exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]); } @@ -195,7 +217,73 @@ class InfoProviderController extends AbstractController return $this->render('info_providers/search/part_search.html.twig', [ 'form' => $form, 'results' => $results, - 'update_target' => $update_target + 'update_target' => $update_target, + 'no_cache_details' => $no_cache_details ?? false, ]); } + + #[Route('/from_url', name: 'info_providers_from_url')] + public function fromURL(Request $request, CreateFromUrlHelper $fromUrlHelper): Response + { + $this->denyAccessUnlessGranted('@info_providers.create_parts'); + + if (!$fromUrlHelper->canCreateFromUrl()) { + $this->addFlash('error', "Generic Web Provider is not active. Please enable it in the provider settings."); + return $this->redirectToRoute('info_providers_list'); + } + + $form = $this->createForm(FromURLFormType::class); + $form->handleRequest($request); + + $partDetail = null; + if ($form->isSubmitted() && $form->isValid()) { + //Try to retrieve the part detail from the given URL + $url = $form->get('url')->getData(); + + $method = $form->get('method')->getData(); + $no_cache = $form->get('no_cache')->getData(); + $skip_delegation = $form->get('skip_delegation')->getData(); + + $submittedPageToken = $request->request->get('submitted_page_token', null); + if ($submittedPageToken !== null && $submittedPageToken !== '') { + $url = $this->browserHtmlStorage->retrieve($submittedPageToken)->url; + } + + + try { + //It's okay if we use the cached results here, as its just for convenience + $searchResult = $this->infoRetriever->searchByKeyword( + keyword: $url, + providers: [$method], + options: [ + InfoProviderInterface::OPTION_SKIP_DELEGATION => $skip_delegation, + InfoProviderInterface::OPTION_SUBMITTED_PAGE_TOKEN => $submittedPageToken, + ] + ); + + if (count($searchResult) === 0) { + $this->addFlash('warning', t('info_providers.from_url.no_part_found')); + } else { + $searchResult = $searchResult[0]; + //Redirect to the part creation page with the found part detail + return $this->redirectToRoute('info_providers_create_part', [ + 'providerKey' => $searchResult->provider_key, + 'providerId' => $searchResult->provider_id, + 'no_cache' => $no_cache ? 1 : null, + 'skip_delegation' => $skip_delegation ? 1 : null, + 'submitted_page_token' => $submittedPageToken ?: null, + ]); + } + } catch (ExceptionInterface $e) { + $this->addFlash('error', t('info_providers.search.error.general_exception', ['%type%' => (new \ReflectionClass($e))->getShortName()])); + } + } + + return $this->render('info_providers/from_url/from_url.html.twig', [ + 'form' => $form, + 'partDetail' => $partDetail, + 'recentBrowserPages' => $this->browserHtmlStorage->getRecentPages(), + ]); + + } } diff --git a/src/Controller/KiCadApiController.php b/src/Controller/KiCadApiController.php index c28e87a6..76727877 100644 --- a/src/Controller/KiCadApiController.php +++ b/src/Controller/KiCadApiController.php @@ -27,6 +27,8 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Services\EDA\KiCadHelper; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -55,15 +57,16 @@ class KiCadApiController extends AbstractController } #[Route('/categories.json', name: 'kicad_api_categories')] - public function categories(): Response + public function categories(Request $request): Response { $this->denyAccessUnlessGranted('@categories.read'); - return $this->json($this->kiCADHelper->getCategories()); + $data = $this->kiCADHelper->getCategories(); + return $this->createCacheableJsonResponse($request, $data, 300); } #[Route('/parts/category/{category}.json', name: 'kicad_api_category')] - public function categoryParts(?Category $category): Response + public function categoryParts(Request $request, ?Category $category): Response { if ($category !== null) { $this->denyAccessUnlessGranted('read', $category); @@ -72,14 +75,31 @@ class KiCadApiController extends AbstractController } $this->denyAccessUnlessGranted('@parts.read'); - return $this->json($this->kiCADHelper->getCategoryParts($category)); + $minimal = $request->query->getBoolean('minimal', false); + $data = $this->kiCADHelper->getCategoryParts($category, $minimal); + return $this->createCacheableJsonResponse($request, $data, 300); } #[Route('/parts/{part}.json', name: 'kicad_api_part')] - public function partDetails(Part $part): Response + public function partDetails(Request $request, Part $part): Response { $this->denyAccessUnlessGranted('read', $part); - return $this->json($this->kiCADHelper->getKiCADPart($part)); + $data = $this->kiCADHelper->getKiCADPart($part); + return $this->createCacheableJsonResponse($request, $data, 60); + } + + /** + * Creates a JSON response with HTTP cache headers (ETag and Cache-Control). + * Returns 304 Not Modified if the client's ETag matches. + */ + private function createCacheableJsonResponse(Request $request, array $data, int $maxAge): Response + { + $response = new JsonResponse($data); + $response->setEtag(md5(json_encode($data))); + $response->headers->set('Cache-Control', 'private, max-age=' . $maxAge); + $response->isNotModified($request); + + return $response; } } \ No newline at end of file diff --git a/src/Controller/KicadListEditorController.php b/src/Controller/KicadListEditorController.php new file mode 100644 index 00000000..85ca0a28 --- /dev/null +++ b/src/Controller/KicadListEditorController.php @@ -0,0 +1,88 @@ +. + */ + +declare(strict_types=1); + +namespace App\Controller; + +use App\Form\Settings\KicadListEditorType; +use App\Settings\MiscSettings\KiCadEDASettings; +use App\Services\EDA\KicadListFileManager; +use Jbtronics\SettingsBundle\Exception\SettingsNotValidException; +use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface; +use RuntimeException; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +use function Symfony\Component\Translation\t; + +final class KicadListEditorController extends AbstractController +{ + public function __construct( + private readonly SettingsManagerInterface $settingsManager, + ) { + } + + #[Route('/settings/misc/kicad-lists', name: 'settings_kicad_lists')] + public function __invoke(Request $request, KicadListFileManager $fileManager): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->denyAccessUnlessGranted('@config.change_system_settings'); + + /** @var KiCadEDASettings $settings */ + $settings = $this->settingsManager->createTemporaryCopy(KiCadEDASettings::class); + $form = $this->createForm(KicadListEditorType::class, [ + 'useCustomList' => $settings->useCustomList, + 'customFootprints' => $fileManager->getCustomFootprintsContent(), + 'customSymbols' => $fileManager->getCustomSymbolsContent(), + ], [ + 'default_footprints' => $fileManager->getFootprintsContent(), + 'default_symbols' => $fileManager->getSymbolsContent(), + ]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + + try { + $fileManager->saveCustom($data['customFootprints'], $data['customSymbols']); + $settings->useCustomList = (bool) $data['useCustomList']; + $this->settingsManager->mergeTemporaryCopy($settings); + $this->settingsManager->save($settings); + $this->addFlash('success', t('settings.flash.saved')); + + return $this->redirectToRoute('settings_kicad_lists'); + } catch (RuntimeException|SettingsNotValidException $exception) { + $this->addFlash('error', $exception->getMessage()); + } + } + + if ($form->isSubmitted() && !$form->isValid()) { + $this->addFlash('error', t('settings.flash.invalid')); + } + + return $this->render('settings/kicad_list_editor.html.twig', [ + 'form' => $form, + ]); + } +} diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 8f4cafed..56cde1e3 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Controller; +use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob; use App\DataTables\LogDataTable; use App\Entity\Attachments\AttachmentUpload; use App\Entity\Parts\Category; @@ -35,10 +36,12 @@ use App\Entity\PriceInformations\Orderdetail; use App\Entity\ProjectSystem\Project; use App\Exceptions\AttachmentDownloadException; use App\Form\Part\PartBaseType; +use App\Form\Part\PartLotType; use App\Services\Attachments\AttachmentSubmitHandler; use App\Services\Attachments\PartPreviewGenerator; use App\Services\EntityMergers\Mergers\PartMerger; use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\LogSystem\EventCommentHelper; use App\Services\LogSystem\HistoryHelper; use App\Services\LogSystem\TimeTravel; @@ -47,18 +50,21 @@ use App\Services\Parts\PartLotWithdrawAddHelper; use App\Services\Parts\PricedetailHelper; use App\Services\ProjectSystem\ProjectBuildPartHelper; use App\Settings\BehaviorSettings\PartInfoSettings; +use App\Settings\MiscSettings\IpnSuggestSettings; use DateTime; use Doctrine\ORM\EntityManagerInterface; use Exception; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; use Symfony\Contracts\Translation\TranslatorInterface; use function Symfony\Component\Translation\t; @@ -74,6 +80,7 @@ final class PartController extends AbstractController private readonly EntityManagerInterface $em, private readonly EventCommentHelper $commentHelper, private readonly PartInfoSettings $partInfoSettings, + private readonly IpnSuggestSettings $ipnSuggestSettings, ) { } @@ -122,6 +129,17 @@ final class PartController extends AbstractController $table = null; } + // Build the add-lot form for the INFO page modal (only when not in time-travel mode) + $addLotForm = null; + if ($timeTravel_timestamp === null && $this->isGranted('edit', $part)) { + $newLot = new PartLot(); + $newLot->setPart($part); + $addLotForm = $this->createForm(PartLotType::class, $newLot, [ + 'measurement_unit' => $part->getPartUnit(), + 'action' => $this->generateUrl('part_lot_add', ['id' => $part->getID()]), + ]); + } + return $this->render( 'parts/info/show_part_info.html.twig', [ @@ -133,10 +151,40 @@ final class PartController extends AbstractController 'description_params' => $this->partInfoSettings->extractParamsFromDescription ? $parameterExtractor->extractParameters($part->getDescription()) : [], 'comment_params' => $this->partInfoSettings->extractParamsFromNotes ? $parameterExtractor->extractParameters($part->getComment()) : [], 'withdraw_add_helper' => $withdrawAddHelper, + 'highlightLotId' => $request->query->getInt('highlightLot', 0), + 'add_lot_form' => $addLotForm, ] ); } + #[Route(path: '/{id}/add_lot', name: 'part_lot_add', methods: ['POST'])] + public function addLot(Part $part, Request $request, EntityManagerInterface $em): Response + { + $this->denyAccessUnlessGranted('edit', $part); + + $newLot = new PartLot(); + $newLot->setPart($part); + + $form = $this->createForm(PartLotType::class, $newLot, [ + 'measurement_unit' => $part->getPartUnit(), + ]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $em->persist($newLot); + $em->flush(); + $this->addFlash('success', 'part.edited_flash'); + return $this->redirectToRoute('part_info', [ + 'id' => $part->getID(), + 'highlightLot' => $newLot->getID(), + ]); + } + + $this->addFlash('error', 'part.created_flash.invalid'); + return $this->redirectToRoute('part_info', ['id' => $part->getID()]); + } + #[Route(path: '/{id}/edit', name: 'part_edit')] public function edit(Part $part, Request $request): Response { @@ -146,7 +194,7 @@ final class PartController extends AbstractController $jobId = $request->query->get('jobId'); $bulkJob = null; if ($jobId) { - $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId); + $bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId); // Verify user owns this job if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) { $bulkJob = null; @@ -167,7 +215,7 @@ final class PartController extends AbstractController throw $this->createAccessDeniedException('Invalid CSRF token'); } - $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId); + $bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId); if (!$bulkJob || $bulkJob->getCreatedBy() !== $this->getUser()) { throw $this->createNotFoundException('Bulk import job not found'); } @@ -298,13 +346,39 @@ final class PartController extends AbstractController { $this->denyAccessUnlessGranted('@info_providers.create_parts'); - $dto = $infoRetriever->getDetails($providerKey, $providerId); + //Force info providers to not use cache, when retrieving part details for creating a new part, because otherwise we might end up with outdated information + $no_cache = $request->query->getBoolean('no_cache', false); + $skip_delegation = $request->query->getBoolean('skip_delegation', false); + $submitted_page_token = $request->query->getString('submitted_page_token'); + + $dto = $infoRetriever->getDetails($providerKey, $providerId, [ + InfoProviderInterface::OPTION_NO_CACHE => $no_cache, + InfoProviderInterface::OPTION_SKIP_DELEGATION => $skip_delegation, + InfoProviderInterface::OPTION_SUBMITTED_PAGE_TOKEN => $submitted_page_token, + ]); $new_part = $infoRetriever->dtoToPart($dto); if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) { $this->addFlash('warning', t("part.create_from_info_provider.no_category_yet")); } + $lotAmount = $request->query->get('lotAmount'); + $lotName = $request->query->get('lotName'); + $lotUserBarcode = $request->query->get('lotUserBarcode'); + + if ($lotAmount !== null || $lotName !== null || $lotUserBarcode !== null) { + $partLot = new PartLot(); + $partLot->setAmount($lotAmount !== null ? (float)$lotAmount : 0); + $partLot->setDescription($lotName !== null ? (string)$lotName : ''); + $partLot->setUserBarcode($lotUserBarcode !== null ? (string)$lotUserBarcode : ''); + + $new_part->addPartLot($partLot); + + $this->addFlash('notice', t('part.create_from_info_provider.lot_filled_from_barcode')); + + } + + return $this->renderPartForm('new', $request, $new_part, [ 'info_provider_dto' => $dto, ]); @@ -340,10 +414,13 @@ final class PartController extends AbstractController $this->denyAccessUnlessGranted('edit', $part); $this->denyAccessUnlessGranted('@info_providers.create_parts'); + //Force info providers to not use cache, when retrieving part details for creating a new part, because otherwise we might end up with outdated information + $no_cache = $request->query->getBoolean('no_cache', false); + //Save the old name of the target part for the template $old_name = $part->getName(); - $dto = $infoRetriever->getDetails($providerKey, $providerId); + $dto = $infoRetriever->getDetails($providerKey, $providerId, [InfoProviderInterface::OPTION_NO_CACHE => $no_cache]); $provider_part = $infoRetriever->dtoToPart($dto); $part = $partMerger->merge($part, $provider_part); @@ -354,7 +431,7 @@ final class PartController extends AbstractController $jobId = $request->query->get('jobId'); $bulkJob = null; if ($jobId) { - $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId); + $bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId); // Verify user owns this job if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) { $bulkJob = null; @@ -465,10 +542,13 @@ final class PartController extends AbstractController $template = 'parts/edit/update_from_ip.html.twig'; } + $partRepository = $this->em->getRepository(Part::class); + return $this->render( $template, [ 'part' => $new_part, + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, $data->getDescription(), $this->ipnSuggestSettings->suggestPartDigits), 'form' => $form, 'merge_old_name' => $merge_infos['tname_before'] ?? null, 'merge_other' => $merge_infos['other_part'] ?? null, @@ -478,6 +558,53 @@ final class PartController extends AbstractController ); } + #[Route(path: '/{id}/stocktake', name: 'part_stocktake', methods: ['POST'])] + #[IsCsrfTokenValid(new Expression("'part_stocktake-' ~ args['part'].getid()"), '_token')] + public function stocktakeHandler(Part $part, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper, + Request $request, + ): Response + { + $partLot = $em->find(PartLot::class, $request->request->get('lot_id')); + + //Check that the user is allowed to stocktake the partlot + $this->denyAccessUnlessGranted('stocktake', $partLot); + + if (!$partLot instanceof PartLot) { + throw new \RuntimeException('Part lot not found!'); + } + //Ensure that the partlot belongs to the part + if ($partLot->getPart() !== $part) { + throw new \RuntimeException("The origin partlot does not belong to the part!"); + } + + $actualAmount = (float) $request->request->get('actual_amount'); + $comment = $request->request->get('comment'); + + $timestamp = null; + $timestamp_str = $request->request->getString('timestamp', ''); + //Try to parse the timestamp + if ($timestamp_str !== '') { + $timestamp = new DateTime($timestamp_str); + } + + $withdrawAddHelper->stocktake($partLot, $actualAmount, $comment, $timestamp); + + //Ensure that the timestamp is not in the future + if ($timestamp !== null && $timestamp > new DateTime("+20min")) { + throw new \LogicException("The timestamp must not be in the future!"); + } + + //Save the changes to the DB + $em->flush(); + $this->addFlash('success', 'part.withdraw.success'); + + //If a redirect was passed, then redirect there + if ($request->request->get('_redirect')) { + return $this->redirect($request->request->get('_redirect')); + } + //Otherwise just redirect to the part page + return $this->redirectToRoute('part_info', ['id' => $part->getID()]); + } #[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])] public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response diff --git a/src/Controller/PartListsController.php b/src/Controller/PartListsController.php index 808b0c5d..2210fc18 100644 --- a/src/Controller/PartListsController.php +++ b/src/Controller/PartListsController.php @@ -319,6 +319,7 @@ class PartListsController extends AbstractController //As an unchecked checkbox is not set in the query, the default value for all bools have to be false (which is the default argument value)! $filter->setName($request->query->getBoolean('name')); + $filter->setDbId($request->query->getBoolean('dbid')); $filter->setCategory($request->query->getBoolean('category')); $filter->setDescription($request->query->getBoolean('description')); $filter->setMpn($request->query->getBoolean('mpn')); diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 2a6d19ee..531deb3f 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -69,10 +69,13 @@ class ProjectController extends AbstractController return $table->getResponse(); } + $number_of_builds = max(1, $request->query->getInt('n', 1)); + return $this->render('projects/info/info.html.twig', [ 'buildHelper' => $buildHelper, 'datatable' => $table, 'project' => $project, + 'number_of_builds' => $number_of_builds, ]); } @@ -240,7 +243,8 @@ class ProjectController extends AbstractController } // Detect fields and get suggestions - $detected_fields = $BOMImporter->detectFields($file_content); + $detected_delimiter = $BOMImporter->detectDelimiter($file_content); + $detected_fields = $BOMImporter->detectFields($file_content, $detected_delimiter); $suggested_mapping = $BOMImporter->getSuggestedFieldMapping($detected_fields); // Create mapping of original field names to sanitized field names for template @@ -257,7 +261,7 @@ class ProjectController extends AbstractController $builder->add('delimiter', ChoiceType::class, [ 'label' => 'project.bom_import.delimiter', 'required' => true, - 'data' => ',', + 'data' => $detected_delimiter, 'choices' => [ 'project.bom_import.delimiter.comma' => ',', 'project.bom_import.delimiter.semicolon' => ';', diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index aebadd89..65eccf27 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -41,11 +41,16 @@ declare(strict_types=1); namespace App\Controller; +use App\Exceptions\InfoProviderNotActiveException; use App\Form\LabelSystem\ScanDialogType; -use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector; +use App\Services\InfoProviderSystem\Providers\LCSCProvider; +use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultHandler; use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper; +use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultInterface; use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType; use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult; +use App\Services\LabelSystem\BarcodeScanner\LCSCBarcodeScanResult; +use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult; use Doctrine\ORM\EntityNotFoundException; use InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -53,6 +58,13 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; +use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\ProviderRegistry; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use App\Entity\Parts\Part; +use \App\Entity\Parts\StorageLocation; +use Symfony\UX\Turbo\TurboBundle; /** * @see \App\Tests\Controller\ScanControllerTest @@ -60,9 +72,10 @@ use Symfony\Component\Routing\Attribute\Route; #[Route(path: '/scan')] class ScanController extends AbstractController { - public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeScanHelper $barcodeNormalizer) - { - } + public function __construct( + protected BarcodeScanResultHandler $resultHandler, + protected BarcodeScanHelper $barcodeNormalizer, + ) {} #[Route(path: '', name: 'scan_dialog')] public function dialog(Request $request, #[MapQueryParameter] ?string $input = null): Response @@ -72,35 +85,86 @@ class ScanController extends AbstractController $form = $this->createForm(ScanDialogType::class); $form->handleRequest($request); + // If JS is working, scanning uses /scan/lookup and this action just renders the page. + // This fallback only runs if user submits the form manually or uses ?input=... if ($input === null && $form->isSubmitted() && $form->isValid()) { $input = $form['input']->getData(); - $mode = $form['mode']->getData(); } - $infoModeData = null; - if ($input !== null) { + if ($input !== null && $input !== '') { + $mode = $form->isSubmitted() ? $form['mode']->getData() : null; + $infoMode = $form->isSubmitted() && $form['info_mode']->getData(); + try { - $scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null); - //Perform a redirect if the info mode is not enabled - if (!$form['info_mode']->getData()) { - try { - return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); - } catch (EntityNotFoundException) { - $this->addFlash('success', 'scan.qr_not_found'); + $scan = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null); + + // If not in info mode, mimic “normal scan” behavior: redirect if possible. + if (!$infoMode) { + + // Try to get an Info URL if possible + $url = $this->resultHandler->getInfoURL($scan); + if ($url !== null) { + return $this->redirect($url); + } + + //Try to get an creation URL if possible (only for vendor codes) + $createUrl = $this->buildCreateUrlForScanResult($scan); + if ($createUrl !== null) { + return $this->redirect($createUrl); + } + + //// Otherwise: show “not found” (not “format unknown”) + $this->addFlash('warning', 'scan.qr_not_found'); + } else { // Info mode + // Info mode fallback: render page with prefilled result + $decoded = $scan->getDecodedForInfoMode(); + + //Try to resolve to an entity, to enhance info mode with entity-specific data + $dbEntity = $this->resultHandler->resolveEntity($scan); + $resolvedPart = $this->resultHandler->resolvePart($scan); + $openUrl = $this->resultHandler->getInfoURL($scan); + + //If no entity is found, try to create an URL for creating a new part (only for vendor codes) + $createUrl = null; + if ($dbEntity === null) { + $createUrl = $this->buildCreateUrlForScanResult($scan); + } + + if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) { + $request->setRequestFormat(TurboBundle::STREAM_FORMAT); + return $this->renderBlock('label_system/scanner/scanner.html.twig', 'scan_results', [ + 'decoded' => $decoded, + 'entity' => $dbEntity, + 'part' => $resolvedPart, + 'openUrl' => $openUrl, + 'createUrl' => $createUrl, + ]); } - } else { //Otherwise retrieve infoModeData - $infoModeData = $scan_result->getDecodedForInfoMode(); } - } catch (InvalidArgumentException) { - $this->addFlash('error', 'scan.format_unknown'); + } catch (\Throwable $e) { + // Keep fallback user-friendly; avoid 500 + $this->addFlash('warning', 'scan.format_unknown'); } } + //When we reach here, only the flash messages are relevant, so if it's a Turbo request, only send the flash message fragment, so the client can show it without a full page reload + if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) { + $request->setRequestFormat(TurboBundle::STREAM_FORMAT); + //Only send our flash message, so the client can show it without a full page reload + return $this->renderBlock('_turbo_control.html.twig', 'flashes'); + } + return $this->render('label_system/scanner/scanner.html.twig', [ 'form' => $form, - 'infoModeData' => $infoModeData, + + //Info mode + 'decoded' => $decoded ?? null, + 'entity' => $dbEntity ?? null, + 'part' => $resolvedPart ?? null, + 'openUrl' => $openUrl ?? null, + 'createUrl' => $createUrl ?? null, ]); } @@ -125,11 +189,30 @@ class ScanController extends AbstractController source_type: BarcodeSourceType::INTERNAL ); - return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); + return $this->redirect($this->resultHandler->getInfoURL($scan_result) ?? throw new EntityNotFoundException("Not found")); } catch (EntityNotFoundException) { $this->addFlash('success', 'scan.qr_not_found'); return $this->redirectToRoute('homepage'); } } + + /** + * Builds a URL for creating a new part based on the barcode data, handles exceptions and shows user-friendly error messages if the provider is not active or if there is an error during URL generation. + * @param BarcodeScanResultInterface $scanResult + * @return string|null + */ + private function buildCreateUrlForScanResult(BarcodeScanResultInterface $scanResult): ?string + { + try { + return $this->resultHandler->getCreationURL($scanResult); + } catch (InfoProviderNotActiveException $e) { + $this->addFlash('error', $e->getMessage()); + } catch (\Throwable) { + // Don’t break scanning UX if provider lookup fails + $this->addFlash('error', 'An error occurred while looking up the provider for this barcode. Please try again later.'); + } + + return null; + } } diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 4e2077c4..93336bf9 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -25,6 +25,7 @@ namespace App\Controller; use App\Entity\UserSystem\User; use App\Events\SecurityEvent; use App\Events\SecurityEvents; +use App\Form\Security\LoginFormType; use App\Services\UserSystem\PasswordResetManager; use Doctrine\ORM\EntityManagerInterface; use Gregwar\CaptchaBundle\Type\CaptchaType; @@ -61,7 +62,12 @@ class SecurityController extends AbstractController // last username entered by the user $lastUsername = $authenticationUtils->getLastUsername(); + $form = $this->createForm(LoginFormType::class, [ + '_username' => $lastUsername, + ]); + return $this->render('security/login.html.twig', [ + 'form' => $form, 'last_username' => $lastUsername, 'error' => $error, ]); @@ -147,10 +153,7 @@ class SecurityController extends AbstractController 'label' => 'user.settings.pw_confirm.label', ], 'invalid_message' => 'password_must_match', - 'constraints' => [new Length([ - 'min' => 6, - 'max' => 128, - ])], + 'constraints' => [new Length(min: 6, max: 128)], ]); $builder->add('submit', SubmitType::class, [ diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php index 3479cf84..c4a52bc7 100644 --- a/src/Controller/SettingsController.php +++ b/src/Controller/SettingsController.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Controller; +use Symfony\Component\Form\Extension\Core\Type\ResetType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use App\Settings\AppSettings; use Jbtronics\SettingsBundle\Form\SettingsFormFactoryInterface; @@ -44,12 +45,15 @@ class SettingsController extends AbstractController public function systemSettings(Request $request, TagAwareCacheInterface $cache): Response { $this->denyAccessUnlessGranted('@config.change_system_settings'); + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); //Create a clone of the settings object $settings = $this->settingsManager->createTemporaryCopy(AppSettings::class); //Create a form builder for the settings object - $builder = $this->settingsFormFactory->createSettingsFormBuilder($settings); + $builder = $this->settingsFormFactory->createSettingsFormBuilder($settings, formOptions: [ + 'warn_on_unsaved_changes' => true, + ]); //Add a submit button to the form $builder->add('submit', SubmitType::class, ['label' => 'save']); @@ -64,7 +68,7 @@ class SettingsController extends AbstractController $this->settingsManager->save($settings); //It might be possible, that the tree settings have changed, so clear the cache - $cache->invalidateTags(['tree_treeview', 'sidebar_tree_update']); + $cache->invalidateTags(['tree_tools', 'tree_treeview', 'sidebar_tree_update', 'synonyms']); $this->addFlash('success', t('settings.flash.saved')); } diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index d78aff62..76dffb4d 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -27,8 +27,8 @@ use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\BuiltinAttachmentsFinder; use App\Services\Doctrine\DBInfoHelper; use App\Services\Doctrine\NatsortDebugHelper; -use App\Services\Misc\GitVersionInfo; -use App\Services\System\UpdateAvailableManager; +use App\Services\System\GitVersionInfoProvider; +use App\Services\System\UpdateAvailableFacade; use App\Settings\AppSettings; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -47,16 +47,16 @@ class ToolsController extends AbstractController } #[Route(path: '/server_infos', name: 'tools_server_infos')] - public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper, - AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager, + public function systemInfos(GitVersionInfoProvider $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper, + AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableFacade $updateAvailableManager, AppSettings $settings): Response { $this->denyAccessUnlessGranted('@system.server_infos'); return $this->render('tools/server_infos/server_infos.html.twig', [ //Part-DB section - 'git_branch' => $versionInfo->getGitBranchName(), - 'git_commit' => $versionInfo->getGitCommitHash(), + 'git_branch' => $versionInfo->getBranchName(), + 'git_commit' => $versionInfo->getCommitHash(), 'default_locale' => $settings->system->localization->locale, 'default_timezone' => $settings->system->localization->timezone, 'default_currency' => $settings->system->localization->baseCurrency, diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 89eac7ff..f7e15b6d 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,37 +22,43 @@ declare(strict_types=1); namespace App\Controller; -use App\Entity\Parameters\AbstractParameter; -use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; +use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; -use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; use App\Entity\Parameters\MeasurementUnitParameter; use App\Entity\Parameters\PartParameter; +use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Currency; use App\Repository\ParameterRepository; +use App\Services\AI\AIPlatformRegistry; +use App\Services\AI\AIPlatforms; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\BuiltinAttachmentsFinder; use App\Services\Attachments\PartPreviewGenerator; use App\Services\Tools\TagFinder; +use App\Settings\MiscSettings\IpnSuggestSettings; use Doctrine\ORM\EntityManagerInterface; +use Symfony\AI\Platform\Capability; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Asset\Packages; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; /** * In this controller the endpoints for the typeaheads are collected. @@ -60,14 +66,20 @@ use Symfony\Component\Serializer\Serializer; #[Route(path: '/typeahead')] class TypeaheadController extends AbstractController { - public function __construct(protected AttachmentURLGenerator $urlGenerator, protected Packages $assets) - { + public function __construct( + protected AttachmentURLGenerator $urlGenerator, + protected Packages $assets, + protected IpnSuggestSettings $ipnSuggestSettings, + ) { } #[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')] public function builtInResources(Request $request, BuiltinAttachmentsFinder $finder): JsonResponse { - $query = $request->get('query'); + //Ensure that the user can access Part-DB at all + $this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS'); + + $query = $request->query->getString('query'); $array = $finder->find($query); $result = []; @@ -114,9 +126,12 @@ class TypeaheadController extends AbstractController } #[Route(path: '/parts/search/{query}', name: 'typeahead_parts')] - public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator, - AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse - { + public function parts( + EntityManagerInterface $entityManager, + PartPreviewGenerator $previewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { $this->denyAccessUnlessGranted('@parts.read'); $repo = $entityManager->getRepository(Part::class); @@ -127,7 +142,7 @@ class TypeaheadController extends AbstractController foreach ($parts as $part) { //Determine the picture to show: $preview_attachment = $previewGenerator->getTablePreviewAttachment($part); - if($preview_attachment instanceof Attachment) { + if ($preview_attachment instanceof Attachment) { $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); } else { $preview_url = ''; @@ -141,7 +156,7 @@ class TypeaheadController extends AbstractController 'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), 'image' => $preview_url, - ]; + ]; } return new JsonResponse($data); @@ -183,4 +198,65 @@ class TypeaheadController extends AbstractController return new JsonResponse($data, Response::HTTP_OK, [], true); } + + #[Route(path: '/parts/ipn-suggestions', name: 'ipn_suggestions', methods: ['GET'])] + public function ipnSuggestions( + Request $request, + EntityManagerInterface $entityManager + ): JsonResponse { + $partId = $request->query->get('partId'); + if ($partId === '0' || $partId === 'undefined' || $partId === 'null') { + $partId = null; + } + $categoryId = $request->query->getInt('categoryId'); + $description = base64_decode($request->query->getString('description'), true); + + /** @var Part $part */ + $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); + /** @var Category|null $category */ + $category = $entityManager->getRepository(Category::class)->find($categoryId); + + //Ensure the user has access to both the part and the category + $this->denyAccessUnlessGranted('read', $part); + if ($category !== null) { + $this->denyAccessUnlessGranted('read', $category); + } + + $clonedPart = clone $part; + $clonedPart->setCategory($category); + + + $partRepository = $entityManager->getRepository(Part::class); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, + $this->ipnSuggestSettings->suggestPartDigits); + + return new JsonResponse($ipnSuggestions); + } + + #[Route(path: '/ai/{platform}/models', name: 'typeahead_ai_models', requirements: ['platform' => '.+'])] + public function aiModels( + AIPlatforms $platform, + Request $request, + AIPlatformRegistry $platformRegistry, + CacheInterface $cache, + ): JsonResponse { + $this->denyAccessUnlessGranted('@config.change_system_settings'); + + $capability_filter = $request->query->getEnum('capability', Capability::class); + + $models = $cache->get('ai_models_'.$platform->value.'_'.($capability_filter->value ?? 'all'), + function (ItemInterface $item) use ($platformRegistry, $platform, $capability_filter) { + $item->expiresAfter(3600); //Cache for 1 hour + if ($capability_filter === null) { + return $platformRegistry->getPlatform($platform)->getModelCatalog()->getModels(); + } + + //Otherwise filter the models by the capability + return array_filter($platformRegistry->getPlatform($platform)->getModelCatalog()->getModels(), + static fn(array $model) => in_array($capability_filter, $model['capabilities'], true) + ); + }); + + return new JsonResponse($models); + } } diff --git a/src/Controller/UpdateManagerController.php b/src/Controller/UpdateManagerController.php new file mode 100644 index 00000000..4901da48 --- /dev/null +++ b/src/Controller/UpdateManagerController.php @@ -0,0 +1,605 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Controller; + +use App\Entity\UserSystem\User; +use App\Services\System\BackupManager; +use App\Services\System\InstallationTypeDetector; +use App\Services\System\UpdateChecker; +use App\Services\System\UpdateExecutor; +use App\Services\System\WatchtowerClient; +use Shivas\VersioningBundle\Service\VersionManagerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Routing\Attribute\Route; + +/** + * Controller for the Update Manager web interface. + * + * This provides a read-only view of update status and instructions. + * Actual updates should be performed via the CLI command for safety. + */ +#[Route('/system/update-manager')] +class UpdateManagerController extends AbstractController +{ + public function __construct( + private readonly UpdateChecker $updateChecker, + private readonly UpdateExecutor $updateExecutor, + private readonly VersionManagerInterface $versionManager, + private readonly BackupManager $backupManager, + private readonly InstallationTypeDetector $installationTypeDetector, + private readonly UserPasswordHasherInterface $passwordHasher, + private readonly WatchtowerClient $watchtowerClient, + #[Autowire(env: 'bool:DISABLE_WEB_UPDATES')] + private readonly bool $webUpdatesDisabled = false, + #[Autowire(env: 'bool:DISABLE_BACKUP_RESTORE')] + private readonly bool $backupRestoreDisabled = false, + #[Autowire(env: 'bool:DISABLE_BACKUP_DOWNLOAD')] + private readonly bool $backupDownloadDisabled = false, + ) { + } + + /** + * Check if web updates are disabled and throw exception if so. + */ + private function denyIfWebUpdatesDisabled(): void + { + if ($this->webUpdatesDisabled) { + throw new AccessDeniedHttpException('Web-based updates are disabled by server configuration. Please use the CLI command instead.'); + } + } + + /** + * Check if backup restore is disabled and throw exception if so. + */ + private function denyIfBackupRestoreDisabled(): void + { + if ($this->backupRestoreDisabled) { + throw new AccessDeniedHttpException('Backup restore is disabled by server configuration.'); + } + } + + /** + * Check if backup download is disabled and throw exception if so. + */ + private function denyIfBackupDownloadDisabled(): void + { + if ($this->backupDownloadDisabled) { + throw new AccessDeniedHttpException('Backup download is disabled by server configuration.'); + } + } + + /** + * Main update manager page. + */ + #[Route('', name: 'admin_update_manager', methods: ['GET'])] + public function index(): Response + { + $this->denyAccessUnlessGranted('@system.show_updates'); + + $status = $this->updateChecker->getUpdateStatus(); + $availableUpdates = $this->updateChecker->getAvailableUpdates(); + $validation = $this->updateExecutor->validateUpdatePreconditions(); + + return $this->render('admin/update_manager/index.html.twig', [ + 'status' => $status, + 'available_updates' => $availableUpdates, + 'all_releases' => $this->updateChecker->getAvailableReleases(10), + 'validation' => $validation, + 'is_locked' => $this->updateExecutor->isLocked(), + 'lock_info' => $this->updateExecutor->getLockInfo(), + 'is_maintenance' => $this->updateExecutor->isMaintenanceMode(), + 'maintenance_info' => $this->updateExecutor->getMaintenanceInfo(), + 'update_logs' => $this->updateExecutor->getUpdateLogs(), + 'backups' => $this->backupManager->getBackups(), + 'web_updates_disabled' => $this->webUpdatesDisabled, + 'backup_restore_disabled' => $this->backupRestoreDisabled, + 'backup_download_disabled' => $this->backupDownloadDisabled, + 'is_docker' => $this->installationTypeDetector->isDocker(), + ]); + } + + /** + * AJAX endpoint to check update status. + */ + #[Route('/status', name: 'admin_update_manager_status', methods: ['GET'])] + public function status(): JsonResponse + { + $this->denyAccessUnlessGranted('@system.show_updates'); + + return $this->json([ + 'status' => $this->updateChecker->getUpdateStatus(), + 'is_locked' => $this->updateExecutor->isLocked(), + 'is_maintenance' => $this->updateExecutor->isMaintenanceMode(), + 'lock_info' => $this->updateExecutor->getLockInfo(), + ]); + } + + /** + * AJAX endpoint to refresh version information. + */ + #[Route('/refresh', name: 'admin_update_manager_refresh', methods: ['POST'])] + public function refresh(Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('@system.show_updates'); + + // Validate CSRF token + if (!$this->isCsrfTokenValid('update_manager_refresh', $request->request->get('_token'))) { + return $this->json(['error' => 'Invalid CSRF token'], Response::HTTP_FORBIDDEN); + } + + $this->updateChecker->refreshVersionInfo(); + + return $this->json([ + 'success' => true, + 'status' => $this->updateChecker->getUpdateStatus(), + ]); + } + + /** + * View release notes for a specific version. + */ + #[Route('/release/{tag}', name: 'admin_update_manager_release', methods: ['GET'])] + public function releaseNotes(string $tag): Response + { + $this->denyAccessUnlessGranted('@system.show_updates'); + + $releases = $this->updateChecker->getAvailableReleases(20); + $release = null; + + foreach ($releases as $r) { + if ($r['tag'] === $tag) { + $release = $r; + break; + } + } + + if (!$release) { + throw $this->createNotFoundException('Release not found'); + } + + return $this->render('admin/update_manager/release_notes.html.twig', [ + 'release' => $release, + 'current_version' => $this->updateChecker->getCurrentVersionString(), + ]); + } + + /** + * View an update log file. + */ + #[Route('/log/{filename}', name: 'admin_update_manager_log', methods: ['GET'])] + public function viewLog(string $filename): Response + { + $this->denyAccessUnlessGranted('@system.manage_updates'); + + // Security: Only allow viewing files from the update logs directory + $logs = $this->updateExecutor->getUpdateLogs(); + $logPath = null; + + foreach ($logs as $log) { + if ($log['file'] === $filename) { + $logPath = $log['path']; + break; + } + } + + if (!$logPath || !file_exists($logPath)) { + throw $this->createNotFoundException('Log file not found'); + } + + $content = file_get_contents($logPath); + + return $this->render('admin/update_manager/log_viewer.html.twig', [ + 'filename' => $filename, + 'content' => $content, + ]); + } + + /** + * Start an update process. + */ + #[Route('/start', name: 'admin_update_manager_start', methods: ['POST'])] + public function startUpdate(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->denyAccessUnlessGranted('@system.manage_updates'); + $this->denyIfWebUpdatesDisabled(); + + // Validate CSRF token + if (!$this->isCsrfTokenValid('update_manager_start', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token'); + return $this->redirectToRoute('admin_update_manager'); + } + + // Check if update is already running + if ($this->updateExecutor->isLocked() || $this->updateExecutor->isUpdateRunning()) { + $this->addFlash('error', 'An update is already in progress.'); + return $this->redirectToRoute('admin_update_manager'); + } + + $targetVersion = $request->request->get('version'); + $createBackup = $request->request->getBoolean('backup', true); + + if (!$targetVersion) { + // Get latest version if not specified + $latest = $this->updateChecker->getLatestVersion(); + if (!$latest) { + $this->addFlash('error', 'Could not determine target version.'); + return $this->redirectToRoute('admin_update_manager'); + } + $targetVersion = $latest['tag']; + } + + // Validate preconditions + $validation = $this->updateExecutor->validateUpdatePreconditions(); + if (!$validation['valid']) { + $this->addFlash('error', implode(' ', $validation['errors'])); + return $this->redirectToRoute('admin_update_manager'); + } + + // Start the background update + $pid = $this->updateExecutor->startBackgroundUpdate($targetVersion, $createBackup); + + if (!$pid) { + $this->addFlash('error', 'Failed to start update process.'); + return $this->redirectToRoute('admin_update_manager'); + } + + // Redirect to progress page + return $this->redirectToRoute('admin_update_manager_progress'); + } + + /** + * Update progress page. + */ + #[Route('/progress', name: 'admin_update_manager_progress', methods: ['GET'])] + public function progress(): Response + { + $this->denyAccessUnlessGranted('@system.manage_updates'); + + $progress = $this->updateExecutor->getProgress(); + $currentVersion = $this->versionManager->getVersion()->toString(); + + // Determine if this is a downgrade + $isDowngrade = false; + if ($progress && isset($progress['target_version'])) { + $targetVersion = ltrim($progress['target_version'], 'v'); + $isDowngrade = version_compare($targetVersion, $currentVersion, '<'); + } + + return $this->render('admin/update_manager/progress.html.twig', [ + 'progress' => $progress, + 'is_locked' => $this->updateExecutor->isLocked(), + 'is_maintenance' => $this->updateExecutor->isMaintenanceMode(), + 'is_downgrade' => $isDowngrade, + 'current_version' => $currentVersion, + ]); + } + + /** + * AJAX endpoint to get update progress. + */ + #[Route('/progress/status', name: 'admin_update_manager_progress_status', methods: ['GET'])] + public function progressStatus(): JsonResponse + { + $this->denyAccessUnlessGranted('@system.show_updates'); + + $progress = $this->updateExecutor->getProgress(); + + return $this->json([ + 'progress' => $progress, + 'is_locked' => $this->updateExecutor->isLocked(), + 'is_maintenance' => $this->updateExecutor->isMaintenanceMode(), + ]); + } + + /** + * Get backup details for restore confirmation. + */ + #[Route('/backup/{filename}', name: 'admin_update_manager_backup_details', methods: ['GET'])] + public function backupDetails(string $filename): JsonResponse + { + $this->denyAccessUnlessGranted('@system.manage_updates'); + + $details = $this->backupManager->getBackupDetails($filename); + + if (!$details) { + return $this->json(['error' => 'Backup not found'], 404); + } + + return $this->json($details); + } + + /** + * Create a manual backup. + */ + #[Route('/backup', name: 'admin_update_manager_backup', methods: ['POST'])] + public function createBackup(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->denyAccessUnlessGranted('@system.manage_updates'); + + if (!$this->isCsrfTokenValid('update_manager_backup', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('admin_update_manager'); + } + + if ($this->updateExecutor->isLocked()) { + $this->addFlash('error', 'Cannot create backup while an update is in progress.'); + return $this->redirectToRoute('admin_update_manager'); + } + + try { + $this->backupManager->createBackup(null, 'manual'); + $this->addFlash('success', 'update_manager.backup.created'); + } catch (\Exception $e) { + $this->addFlash('error', 'Backup failed: ' . $e->getMessage()); + } + + return $this->redirectToRoute('admin_update_manager'); + } + + /** + * Delete a backup file. + */ + #[Route('/backup/delete', name: 'admin_update_manager_backup_delete', methods: ['POST'])] + public function deleteBackup(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->denyAccessUnlessGranted('@system.manage_updates'); + + if (!$this->isCsrfTokenValid('update_manager_delete', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('admin_update_manager'); + } + + $filename = $request->request->get('filename'); + if ($filename && $this->backupManager->deleteBackup($filename)) { + $this->addFlash('success', 'update_manager.backup.deleted'); + } else { + $this->addFlash('error', 'update_manager.backup.delete_error'); + } + + return $this->redirectToRoute('admin_update_manager'); + } + + /** + * Delete an update log file. + */ + #[Route('/log/delete', name: 'admin_update_manager_log_delete', methods: ['POST'])] + public function deleteLog(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->denyAccessUnlessGranted('@system.manage_updates'); + + if (!$this->isCsrfTokenValid('update_manager_delete', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('admin_update_manager'); + } + + $filename = $request->request->get('filename'); + if ($filename && $this->updateExecutor->deleteLog($filename)) { + $this->addFlash('success', 'update_manager.log.deleted'); + } else { + $this->addFlash('error', 'update_manager.log.delete_error'); + } + + return $this->redirectToRoute('admin_update_manager'); + } + + /** + * Download a backup file. + * Requires password confirmation as backups contain sensitive data (password hashes, secrets, etc.). + */ + #[Route('/backup/download', name: 'admin_update_manager_backup_download', methods: ['POST'])] + public function downloadBackup(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->denyAccessUnlessGranted('@system.manage_updates'); + $this->denyIfBackupDownloadDisabled(); + + if (!$this->isCsrfTokenValid('update_manager_download', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('admin_update_manager'); + } + + // Verify password + $password = $request->request->get('password', ''); + $user = $this->getUser(); + if (!$user instanceof User || !$this->passwordHasher->isPasswordValid($user, $password)) { + $this->addFlash('error', 'update_manager.backup.download.invalid_password'); + return $this->redirectToRoute('admin_update_manager'); + } + + $filename = $request->request->get('filename', ''); + $details = $this->backupManager->getBackupDetails($filename); + if (!$details) { + throw $this->createNotFoundException('Backup not found'); + } + + $response = new BinaryFileResponse($details['path']); + $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $details['file']); + + return $response; + } + + /** + * Restore from a backup. + */ + #[Route('/restore', name: 'admin_update_manager_restore', methods: ['POST'])] + public function restore(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->denyAccessUnlessGranted('@system.manage_updates'); + $this->denyIfBackupRestoreDisabled(); + + // Validate CSRF token + if (!$this->isCsrfTokenValid('update_manager_restore', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('admin_update_manager'); + } + + // Check if already locked + if ($this->updateExecutor->isLocked()) { + $this->addFlash('error', 'An update or restore is already in progress.'); + return $this->redirectToRoute('admin_update_manager'); + } + + $filename = $request->request->get('filename'); + $restoreDatabase = $request->request->getBoolean('restore_database', true); + $restoreConfig = $request->request->getBoolean('restore_config', false); + $restoreAttachments = $request->request->getBoolean('restore_attachments', false); + + if (!$filename) { + $this->addFlash('error', 'No backup file specified.'); + return $this->redirectToRoute('admin_update_manager'); + } + + // Verify the backup exists + $backupDetails = $this->backupManager->getBackupDetails($filename); + if (!$backupDetails) { + $this->addFlash('error', 'Backup file not found.'); + return $this->redirectToRoute('admin_update_manager'); + } + + // Execute restore (this is a synchronous operation for now - could be made async later) + $result = $this->updateExecutor->restoreBackup( + $filename, + $restoreDatabase, + $restoreConfig, + $restoreAttachments + ); + + if ($result['success']) { + $this->addFlash('success', 'Backup restored successfully.'); + } else { + $this->addFlash('error', 'Restore failed: ' . ($result['error'] ?? 'Unknown error')); + } + + return $this->redirectToRoute('admin_update_manager'); + } + + /** + * Start a Docker update via Watchtower. + */ + #[Route('/start-docker', name: 'admin_update_manager_start_docker', methods: ['POST'])] + public function startDockerUpdate(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->denyAccessUnlessGranted('@system.manage_updates'); + $this->denyIfWebUpdatesDisabled(); + + // Validate CSRF token + if (!$this->isCsrfTokenValid('update_manager_start_docker', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token'); + return $this->redirectToRoute('admin_update_manager'); + } + + // Check if Watchtower is configured and available + if (!$this->watchtowerClient->isConfigured()) { + $this->addFlash('error', 'Watchtower is not configured. Please set WATCHTOWER_API_URL and WATCHTOWER_API_TOKEN.'); + return $this->redirectToRoute('admin_update_manager'); + } + + if (!$this->watchtowerClient->isAvailable()) { + $this->addFlash('error', 'Watchtower is not reachable. Please check that the Watchtower container is running and accessible.'); + return $this->redirectToRoute('admin_update_manager'); + } + + // Create backup if requested + $createBackup = $request->request->getBoolean('backup', true); + if ($createBackup) { + try { + $this->backupManager->createBackup(); + } catch (\Throwable $e) { + $this->addFlash('error', 'Failed to create backup before update: ' . $e->getMessage()); + return $this->redirectToRoute('admin_update_manager'); + } + } + + // Trigger Watchtower update + $success = $this->watchtowerClient->triggerUpdate(); + + if (!$success) { + $this->addFlash('error', 'Failed to trigger Watchtower update. Check the logs for details.'); + return $this->redirectToRoute('admin_update_manager'); + } + + $currentVersion = $this->versionManager->getVersion()->toString(); + + // Redirect to Docker progress page + return $this->redirectToRoute('admin_update_manager_docker_progress', [ + 'previous_version' => $currentVersion, + ]); + } + + /** + * Docker update progress page. + * This page contains client-side JavaScript that polls until the container restarts. + */ + #[Route('/progress/docker', name: 'admin_update_manager_docker_progress', methods: ['GET'])] + public function dockerProgress(Request $request): Response + { + $this->denyAccessUnlessGranted('@system.manage_updates'); + + $previousVersion = $request->query->get('previous_version', 'unknown'); + + return $this->render('admin/update_manager/docker_progress.html.twig', [ + 'previous_version' => $previousVersion, + ]); + } + + /** + * Lightweight health check endpoint used by Docker update progress page. + * Returns current version so the client-side JS can detect when the container restarts with a new version. + * + * Intentionally unauthenticated: after a Docker container restart, the user's session may not survive + * (depends on session storage backend). The version string is non-sensitive public information. + * This endpoint is also whitelisted in MaintenanceModeSubscriber. + */ + #[Route('/health', name: 'admin_update_manager_health', methods: ['GET'])] + public function healthCheck(): JsonResponse + { + //Only show version if user is logged in and has permission + + $response = [ + 'status' => 'ok', + ]; + + if ($this->isGranted('@system.show_updates')) { + $response['version'] = $this->versionManager->getVersion()->toString(); + } else { + $response['version'] = "not authorized"; + } + + return $this->json($response); + } +} diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php index 4e56015a..3f54e99c 100644 --- a/src/Controller/UserSettingsController.php +++ b/src/Controller/UserSettingsController.php @@ -295,10 +295,7 @@ class UserSettingsController extends AbstractController 'autocomplete' => 'new-password', ], ], - 'constraints' => [new Length([ - 'min' => 6, - 'max' => 128, - ])], + 'constraints' => [new Length(min: 6, max: 128)], ]) ->add('submit', SubmitType::class, [ 'label' => 'save', diff --git a/src/DataFixtures/DataStructureFixtures.php b/src/DataFixtures/DataStructureFixtures.php index fc713d4d..9c685338 100644 --- a/src/DataFixtures/DataStructureFixtures.php +++ b/src/DataFixtures/DataStructureFixtures.php @@ -24,6 +24,7 @@ namespace App\DataFixtures; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -50,7 +51,7 @@ class DataStructureFixtures extends Fixture implements DependentFixtureInterface { //Reset autoincrement $types = [AttachmentType::class, Project::class, Category::class, Footprint::class, Manufacturer::class, - MeasurementUnit::class, StorageLocation::class, Supplier::class,]; + MeasurementUnit::class, StorageLocation::class, Supplier::class, PartCustomState::class]; foreach ($types as $type) { $this->createNodesForClass($type, $manager); diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index 7e6db49c..464aee26 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -41,6 +41,7 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; @@ -67,6 +68,7 @@ class PartFilter implements FilterInterface public readonly BooleanConstraint $favorite; public readonly BooleanConstraint $needsReview; public readonly NumberConstraint $mass; + public readonly TextConstraint $gtin; public readonly DateTimeConstraint $lastModified; public readonly DateTimeConstraint $addedDate; public readonly EntityConstraint $category; @@ -88,6 +90,7 @@ class PartFilter implements FilterInterface public readonly EntityConstraint $lotOwner; public readonly EntityConstraint $measurementUnit; + public readonly EntityConstraint $partCustomState; public readonly TextConstraint $manufacturer_product_url; public readonly TextConstraint $manufacturer_product_number; public readonly IntConstraint $attachmentsCount; @@ -130,7 +133,9 @@ class PartFilter implements FilterInterface $this->favorite = new BooleanConstraint('part.favorite'); $this->needsReview = new BooleanConstraint('part.needs_review'); $this->measurementUnit = new EntityConstraint($nodesListBuilder, MeasurementUnit::class, 'part.partUnit'); + $this->partCustomState = new EntityConstraint($nodesListBuilder, PartCustomState::class, 'part.partCustomState'); $this->mass = new NumberConstraint('part.mass'); + $this->gtin = new TextConstraint('part.gtin'); $this->dbId = new IntConstraint('part.id'); $this->ipn = new TextConstraint('part.ipn'); $this->addedDate = new DateTimeConstraint('part.addedDate'); diff --git a/src/DataTables/Filters/PartSearchFilter.php b/src/DataTables/Filters/PartSearchFilter.php index aa8c20f4..9f6734e5 100644 --- a/src/DataTables/Filters/PartSearchFilter.php +++ b/src/DataTables/Filters/PartSearchFilter.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\DataTables\Filters; use App\DataTables\Filters\Constraints\AbstractConstraint; use Doctrine\ORM\QueryBuilder; +use Doctrine\DBAL\ParameterType; class PartSearchFilter implements FilterInterface { @@ -33,6 +34,9 @@ class PartSearchFilter implements FilterInterface /** @var bool Use name field for searching */ protected bool $name = true; + /** @var bool Use id field for searching */ + protected bool $dbId = false; + /** @var bool Use category name for searching */ protected bool $category = true; @@ -120,33 +124,51 @@ class PartSearchFilter implements FilterInterface public function apply(QueryBuilder $queryBuilder): void { $fields_to_search = $this->getFieldsToSearch(); + $is_numeric = preg_match('/^\d+$/', $this->keyword) === 1; + + // Add exact ID match only when the keyword is numeric + $search_dbId = $is_numeric && (bool)$this->dbId; //If we have nothing to search for, do nothing - if ($fields_to_search === [] || $this->keyword === '') { + if (($fields_to_search === [] && !$search_dbId) || $this->keyword === '') { return; } - //Convert the fields to search to a list of expressions - $expressions = array_map(function (string $field): string { + $expressions = []; + + if($fields_to_search !== []) { + //Convert the fields to search to a list of expressions + $expressions = array_map(function (string $field): string { + if ($this->regex) { + return sprintf("REGEXP(%s, :search_query) = TRUE", $field); + } + + return sprintf("ILIKE(%s, :search_query) = TRUE", $field); + }, $fields_to_search); + + //For regex, we pass the query as is, for like we add % to the start and end as wildcards if ($this->regex) { - return sprintf("REGEXP(%s, :search_query) = TRUE", $field); + $queryBuilder->setParameter('search_query', $this->keyword); + } else { + //Escape % and _ characters in the keyword + $this->keyword = str_replace(['%', '_'], ['\%', '\_'], $this->keyword); + $queryBuilder->setParameter('search_query', '%' . $this->keyword . '%'); } + } - return sprintf("ILIKE(%s, :search_query) = TRUE", $field); - }, $fields_to_search); + //Use equal expression to just search for exact numeric matches + if ($search_dbId) { + $expressions[] = $queryBuilder->expr()->eq('part.id', ':id_exact'); + $queryBuilder->setParameter('id_exact', (int) $this->keyword, + ParameterType::INTEGER); + } - //Add Or concatenation of the expressions to our query - $queryBuilder->andWhere( - $queryBuilder->expr()->orX(...$expressions) - ); - - //For regex, we pass the query as is, for like we add % to the start and end as wildcards - if ($this->regex) { - $queryBuilder->setParameter('search_query', $this->keyword); - } else { - //Escape % and _ characters in the keyword - $this->keyword = str_replace(['%', '_'], ['\%', '\_'], $this->keyword); - $queryBuilder->setParameter('search_query', '%' . $this->keyword . '%'); + //Guard condition + if (!empty($expressions)) { + //Add Or concatenation of the expressions to our query + $queryBuilder->andWhere( + $queryBuilder->expr()->orX(...$expressions) + ); } } @@ -183,6 +205,17 @@ class PartSearchFilter implements FilterInterface return $this; } + public function isDbId(): bool + { + return $this->dbId; + } + + public function setDbId(bool $dbId): PartSearchFilter + { + $this->dbId = $dbId; + return $this; + } + public function isCategory(): bool { return $this->category; diff --git a/src/DataTables/Helpers/PartDataTableHelper.php b/src/DataTables/Helpers/PartDataTableHelper.php index c33c3a82..54094ff1 100644 --- a/src/DataTables/Helpers/PartDataTableHelper.php +++ b/src/DataTables/Helpers/PartDataTableHelper.php @@ -115,6 +115,61 @@ class PartDataTableHelper return implode('
', $tmp); } + /** + * Renders an EDA/KiCad completeness indicator for the given part. + * Shows icons for symbol, footprint, and value status. + */ + public function renderEdaStatus(Part $context): string + { + $edaInfo = $context->getEdaInfo(); + $category = $context->getCategory(); + $footprint = $context->getFootprint(); + + // Determine effective values (direct or inherited) + $hasSymbol = $edaInfo->getKicadSymbol() !== null || $category?->getEdaInfo()->getKicadSymbol() !== null; + $hasFootprint = $edaInfo->getKicadFootprint() !== null || $footprint?->getEdaInfo()->getKicadFootprint() !== null; + $hasReference = $edaInfo->getReferencePrefix() !== null || $category?->getEdaInfo()->getReferencePrefix() !== null; + + $symbolInherited = $edaInfo->getKicadSymbol() === null && $category?->getEdaInfo()->getKicadSymbol() !== null; + $footprintInherited = $edaInfo->getKicadFootprint() === null && $footprint?->getEdaInfo()->getKicadFootprint() !== null; + + $icons = []; + + // Symbol status + if ($hasSymbol) { + $title = $this->translator->trans('eda.status.symbol_set'); + $class = $symbolInherited ? 'text-info' : 'text-success'; + $icons[] = sprintf('', $class, $title); + } + + // Footprint status + if ($hasFootprint) { + $title = $this->translator->trans('eda.status.footprint_set'); + $class = $footprintInherited ? 'text-info' : 'text-success'; + $icons[] = sprintf('', $class, $title); + } + + // Reference prefix status + if ($hasReference) { + $icons[] = sprintf('', + $this->translator->trans('eda.status.reference_set')); + } + + if (empty($icons)) { + return ''; + } + + // Overall status: all 3 = green check, partial = yellow + $allSet = $hasSymbol && $hasFootprint && $hasReference; + $statusIcon = $allSet + ? sprintf('', $this->translator->trans('eda.status.complete')) + : sprintf('', $this->translator->trans('eda.status.partial')); + + // Wrap in link to EDA settings tab (data-turbo=false to ensure hash is read on page load) + $editUrl = $this->entityURLGenerator->editURL($context) . '#eda'; + return sprintf('
%s', $editUrl, $statusIcon); + } + public function renderAmount(Part $context): string { $amount = $context->getAmountSum(); diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index f6604279..2c37767b 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -208,6 +208,7 @@ class LogDataTable implements DataTableTypeInterface $dataTable->add('extra', LogEntryExtraColumn::class, [ 'label' => 'log.extra', + 'orderable' => false, //Sorting the JSON column makes no sense: MySQL/Sqlite does it via the string representation, PostgreSQL errors out ]); $dataTable->add('timeTravel', IconLinkColumn::class, [ diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index de7313b7..b3ff841b 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -38,6 +38,7 @@ use App\DataTables\Filters\PartFilter; use App\DataTables\Filters\PartSearchFilter; use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; +use App\Doctrine\Functions\SiValueSort; use App\Doctrine\Helpers\FieldHelper; use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; @@ -47,6 +48,7 @@ use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; use App\Settings\BehaviorSettings\TableSettings; use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Column\TextColumn; @@ -58,7 +60,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; final class PartsDataTable implements DataTableTypeInterface { - const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]; + public const LENGTH_MENU = [[10, 25, 50, 100, 250, 500, -1], [10, 25, 50, 100, 250, 500, "All"]]; public function __construct( private readonly EntityURLGenerator $urlGenerator, @@ -88,6 +90,10 @@ final class PartsDataTable implements DataTableTypeInterface $this->configureOptions($resolver); $options = $resolver->resolve($options); + /************************************************************************************************************* + * When adding columns here, add them also to PartTableColumns enum, to make them configurable in the settings! + *************************************************************************************************************/ + $this->csh //Color the table rows depending on the review and favorite status ->add('row_color', RowClassColumn::class, [ @@ -113,6 +119,18 @@ final class PartsDataTable implements DataTableTypeInterface 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderName($context), 'orderField' => 'NATSORT(part.name)' ]) + ->add('si_value', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.si_value'), + 'render' => function ($value, Part $context): string { + $siValue = SiValueSort::sqliteSiValue($context->getName()); + if ($siValue !== null) { + //Output it as scientific number with a big E + return htmlspecialchars(sprintf('%G', $siValue)); + } + return ''; + }, + 'orderField' => 'SI_VALUE_SORT(part.name)', + ]) ->add('id', TextColumn::class, [ 'label' => $this->translator->trans('part.table.id'), ]) @@ -183,6 +201,19 @@ final class PartsDataTable implements DataTableTypeInterface return $tmp; } ]) + ->add('partCustomState', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.partCustomState'), + 'orderField' => 'NATSORT(_partCustomState.name)', + 'render' => function($value, Part $context): string { + $partCustomState = $context->getPartCustomState(); + + if ($partCustomState === null) { + return ''; + } + + return htmlspecialchars($partCustomState->getName()); + } + ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), ]) @@ -214,11 +245,30 @@ final class PartsDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('part.table.mass'), 'unit' => 'g' ]) + ->add('gtin', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.gtin'), + 'orderField' => 'NATSORT(part.gtin)' + ]) ->add('tags', TagsColumn::class, [ 'label' => $this->translator->trans('part.table.tags'), ]) ->add('attachments', PartAttachmentsColumn::class, [ 'label' => $this->translator->trans('part.table.attachments'), + ]) + ->add('eda_reference', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.eda_reference'), + 'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getReferencePrefix() ?? ''), + 'orderField' => 'NATSORT(part.eda_info.reference_prefix)' + ]) + ->add('eda_value', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.eda_value'), + 'render' => static fn($value, Part $context) => htmlspecialchars($context->getEdaInfo()->getValue() ?? ''), + 'orderField' => 'NATSORT(part.eda_info.value)' + ]) + ->add('eda_status', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.eda_status'), + 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderEdaStatus($context), + 'className' => 'text-center', ]); //Add a column to list the projects where the part is used, when the user has the permission to see the projects @@ -318,12 +368,14 @@ final class PartsDataTable implements DataTableTypeInterface ->addSelect('footprint') ->addSelect('manufacturer') ->addSelect('partUnit') + ->addSelect('partCustomState') ->addSelect('master_picture_attachment') ->addSelect('footprint_attachment') ->addSelect('partLots') ->addSelect('orderdetails') ->addSelect('attachments') ->addSelect('storelocations') + ->addSelect('projectBomEntries') ->from(Part::class, 'part') ->leftJoin('part.category', 'category') ->leftJoin('part.master_picture_attachment', 'master_picture_attachment') @@ -336,7 +388,9 @@ final class PartsDataTable implements DataTableTypeInterface ->leftJoin('orderdetails.supplier', 'suppliers') ->leftJoin('part.attachments', 'attachments') ->leftJoin('part.partUnit', 'partUnit') + ->leftJoin('part.partCustomState', 'partCustomState') ->leftJoin('part.parameters', 'parameters') + ->leftJoin('part.project_bom_entries', 'projectBomEntries') ->where('part.id IN (:ids)') ->setParameter('ids', $ids) @@ -353,7 +407,13 @@ final class PartsDataTable implements DataTableTypeInterface ->addGroupBy('suppliers') ->addGroupBy('attachments') ->addGroupBy('partUnit') - ->addGroupBy('parameters'); + ->addGroupBy('partCustomState') + ->addGroupBy('parameters') + ->addGroupBy('projectBomEntries') + + ->setHint(Query::HINT_READ_ONLY, true) + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, false) + ; //Get the results in the same order as the IDs were passed FieldHelper::addOrderByFieldParam($builder, 'part.id', 'ids'); @@ -424,6 +484,10 @@ final class PartsDataTable implements DataTableTypeInterface $builder->leftJoin('part.partUnit', '_partUnit'); $builder->addGroupBy('_partUnit'); } + if (str_contains($dql, '_partCustomState')) { + $builder->leftJoin('part.partCustomState', '_partCustomState'); + $builder->addGroupBy('_partCustomState'); + } if (str_contains($dql, '_parameters')) { $builder->leftJoin('part.parameters', '_parameters'); //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 @@ -442,6 +506,19 @@ final class PartsDataTable implements DataTableTypeInterface //$builder->addGroupBy('_bulkImportJob'); } + //When sorting by SI value, add NATSORT as a secondary sort so that parts without + //an SI-prefixed value fall back to natural string ordering seamlessly. + $orderByParts = $builder->getDQLPart('orderBy'); + foreach ($orderByParts as $orderBy) { + foreach ($orderBy->getParts() as $part) { + if (str_contains($part, 'SI_VALUE_SORT')) { + $direction = str_contains($part, 'DESC') ? 'DESC' : 'ASC'; + $builder->addOrderBy('NATSORT(part.name)', $direction); + break 2; + } + } + } + return $builder; } diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index fcb06984..b5beeca0 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -1,8 +1,5 @@ . */ + +declare(strict_types=1); + namespace App\DataTables; +use App\DataTables\Adapters\TwoStepORMAdapter; use App\DataTables\Column\EntityColumn; +use App\DataTables\Column\EnumColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; use App\DataTables\Helpers\PartDataTableHelper; -use App\Entity\Attachments\Attachment; +use App\Doctrine\Helpers\FieldHelper; +use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; +use App\Services\Formatters\MoneyFormatter; +use App\Services\ProjectSystem\ProjectBuildHelper; +use Brick\Math\BigDecimal; +use Brick\Math\RoundingMode; +use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; -use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; @@ -41,8 +50,14 @@ use Symfony\Contracts\Translation\TranslatorInterface; class ProjectBomEntriesDataTable implements DataTableTypeInterface { - public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) - { + public function __construct( + protected EntityURLGenerator $entityURLGenerator, + protected TranslatorInterface $translator, + protected AmountFormatter $amountFormatter, + protected PartDataTableHelper $partDataTableHelper, + protected ProjectBuildHelper $projectBuildHelper, + protected MoneyFormatter $moneyFormatter, + ) { } @@ -58,7 +73,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface return ''; } return $this->partDataTableHelper->renderPicture($context->getPart()); - }, + } ]) ->add('id', TextColumn::class, [ @@ -79,7 +94,14 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit())); }, ]) - + ->add('partId', TextColumn::class, [ + 'label' => $this->translator->trans('project.bom.part_id'), + 'visible' => true, + 'orderField' => 'part.id', + 'render' => function ($value, ProjectBOMEntry $context) { + return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : ''; + }, + ]) ->add('name', TextColumn::class, [ 'label' => $this->translator->trans('part.table.name'), 'orderField' => 'NATSORT(part.name)', @@ -122,18 +144,32 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface ->add('category', EntityColumn::class, [ 'label' => $this->translator->trans('part.table.category'), 'property' => 'part.category', - 'orderField' => 'NATSORT(category.name)', + 'orderField' => 'NATSORT(category.name)' ]) ->add('footprint', EntityColumn::class, [ 'property' => 'part.footprint', 'label' => $this->translator->trans('part.table.footprint'), - 'orderField' => 'NATSORT(footprint.name)', + 'orderField' => 'NATSORT(footprint.name)' ]) ->add('manufacturer', EntityColumn::class, [ 'property' => 'part.manufacturer', 'label' => $this->translator->trans('part.table.manufacturer'), - 'orderField' => 'NATSORT(manufacturer.name)', + 'orderField' => 'NATSORT(manufacturer.name)' + ]) + + ->add('manufacturing_status', EnumColumn::class, [ + 'label' => $this->translator->trans('part.table.manufacturingStatus'), + 'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(), + 'orderField' => 'part.manufacturing_status', + 'class' => ManufacturingStatus::class, + 'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string { + if ($status === null) { + return ''; + } + + return $this->translator->trans($status->toTranslationKey()); + }, ]) ->add('mountnames', TextColumn::class, [ @@ -159,8 +195,10 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface return ''; } ]) - ->add('storageLocations', TextColumn::class, [ - 'label' => 'part.table.storeLocations', + ->add('storelocation', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.storeLocations'), + //We need to use a aggregate function to get the first store location, as we have a one-to-many relation + 'orderField' => 'NATSORT(MIN(_storelocations.name))', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { if ($context->getPart() !== null) { @@ -170,6 +208,28 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface return ''; } ]) + ->add('price', TextColumn::class, [ + 'label' => 'project.bom.price', + 'visible' => false, + 'render' => function ($value, ProjectBOMEntry $context) { + $price = $this->projectBuildHelper->getEntryUnitPrice($context); + return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true); + }, + ]) + ->add('ext_price', TextColumn::class, [ + 'label' => 'project.bom.ext_price', + 'visible' => false, + 'render' => function ($value, ProjectBOMEntry $context) { + $price = $this->projectBuildHelper->getEntryUnitPrice($context); + return $this->moneyFormatter->format( + $price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity())) + ->toScale(2, RoundingMode::Up)->toFloat(), + null, + 2, + true + ); + }, + ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), @@ -183,11 +243,13 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface $dataTable->addOrderBy('name', DataTable::SORT_ASCENDING); - $dataTable->createAdapter(ORMAdapter::class, [ - 'entity' => Attachment::class, - 'query' => function (QueryBuilder $builder) use ($options): void { - $this->getQuery($builder, $options); + $dataTable->createAdapter(TwoStepORMAdapter::class, [ + 'entity' => ProjectBOMEntry::class, + 'hydrate' => AbstractQuery::HYDRATE_OBJECT, + 'filter_query' => function (QueryBuilder $builder) use ($options): void { + $this->getFilterQuery($builder, $options); }, + 'detail_query' => $this->getDetailQuery(...), 'criteria' => [ function (QueryBuilder $builder) use ($options): void { $this->buildCriteria($builder, $options); @@ -197,20 +259,71 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface ]); } - private function getQuery(QueryBuilder $builder, array $options): void + private function getFilterQuery(QueryBuilder $builder, array $options): void { - $builder->select('bom_entry') - ->addSelect('part') + $builder + ->select('bom_entry.id') ->from(ProjectBOMEntry::class, 'bom_entry') ->leftJoin('bom_entry.part', 'part') ->leftJoin('part.category', 'category') + ->leftJoin('part.partLots', '_partLots') + ->leftJoin('_partLots.storage_location', '_storelocations') ->leftJoin('part.footprint', 'footprint') ->leftJoin('part.manufacturer', 'manufacturer') + ->leftJoin('part.partCustomState', 'partCustomState') ->where('bom_entry.project = :project') ->setParameter('project', $options['project']) + ->addGroupBy('bom_entry') + ->addGroupBy('part') + ->addGroupBy('category') + ->addGroupBy('footprint') + ->addGroupBy('manufacturer') + ->addGroupBy('partCustomState') ; } + private function getDetailQuery(QueryBuilder $builder, array $filter_results): void + { + $ids = array_map(static fn (array $row) => $row['id'], $filter_results); + if ($ids === []) { + $ids = [-1]; + } + + $builder + ->select('bom_entry') + ->addSelect('part') + ->addSelect('category') + ->addSelect('partLots') + ->addSelect('storelocations') + ->addSelect('footprint') + ->addSelect('manufacturer') + ->addSelect('partCustomState') + ->from(ProjectBOMEntry::class, 'bom_entry') + ->leftJoin('bom_entry.part', 'part') + ->leftJoin('part.category', 'category') + ->leftJoin('part.partLots', 'partLots') + ->leftJoin('partLots.storage_location', 'storelocations') + ->leftJoin('part.footprint', 'footprint') + ->leftJoin('part.manufacturer', 'manufacturer') + ->leftJoin('part.partCustomState', 'partCustomState') + ->where('bom_entry.id IN (:ids)') + ->setParameter('ids', $ids) + ->addGroupBy('bom_entry') + ->addGroupBy('part') + ->addGroupBy('partLots') + ->addGroupBy('category') + ->addGroupBy('storelocations') + ->addGroupBy('footprint') + ->addGroupBy('manufacturer') + ->addGroupBy('partCustomState') + + ->setHint(Query::HINT_READ_ONLY, true) + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, false) + ; + + FieldHelper::addOrderByFieldParam($builder, 'bom_entry.id', 'ids'); + } + private function buildCriteria(QueryBuilder $builder, array $options): void { diff --git a/src/Doctrine/Functions/SiValueSort.php b/src/Doctrine/Functions/SiValueSort.php new file mode 100644 index 00000000..c4d16444 --- /dev/null +++ b/src/Doctrine/Functions/SiValueSort.php @@ -0,0 +1,196 @@ +. + */ + +declare(strict_types=1); + +namespace App\Doctrine\Functions; + +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; +use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\AST\Node; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; + +/** + * Custom DQL function that extracts the first numeric value with an optional SI prefix + * from a string and returns the scaled numeric value for sorting. + * + * Usage: SI_VALUE_SORT(part.name) + * + * This enables sorting parts by their physical value. For example, capacitors + * named "100nF", "1uF", "10pF" will be sorted by actual value: 10pF < 100nF < 1uF. + * + * Supported SI prefixes: p (pico, 1e-12), n (nano, 1e-9), u/µ (micro, 1e-6), + * m (milli, 1e-3), k/K (kilo, 1e3), M (mega, 1e6), G (giga, 1e9), T (tera, 1e12). + * + * Only matches numbers at the very beginning of the string (ignoring leading whitespace). + * Names like "Crystal 20MHz" will NOT match since the number is not at the start. + * Names without a recognizable numeric+prefix pattern return NULL and sort last. + */ +class SiValueSort extends FunctionNode +{ + private ?Node $field = null; + + /** + * SI prefix multipliers. Used by the SQLite PHP callback. + */ + private const SI_MULTIPLIERS = [ + 'p' => 1e-12, + 'n' => 1e-9, + 'u' => 1e-6, + 'µ' => 1e-6, + 'm' => 1e-3, + 'k' => 1e3, + 'K' => 1e3, + 'M' => 1e6, + 'G' => 1e9, + 'T' => 1e12, + ]; + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->field = $parser->ArithmeticExpression(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getSql(SqlWalker $sqlWalker): string + { + assert($this->field !== null, 'Field is not set'); + + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + $rawField = $this->field->dispatch($sqlWalker); + + // Normalize comma decimal separator to dot for SQL platforms (European locale support) + $fieldSql = "REPLACE({$rawField}, ',', '.')"; + + if ($platform instanceof PostgreSQLPlatform) { + return $this->getPostgreSQLSql($fieldSql); + } + + if ($platform instanceof AbstractMySQLPlatform) { + return $this->getMySQLSql($fieldSql); + } + + // SQLite: comma normalization is handled in the PHP callback + $fieldSql = $rawField; + + if ($platform instanceof SQLitePlatform) { + return "SI_VALUE({$fieldSql})"; + } + + // Fallback: return NULL (no SI sorting available) + return 'NULL'; + } + + /** + * PostgreSQL implementation using substring() with POSIX regex. + */ + private function getPostgreSQLSql(string $field): string + { + // Extract the numeric part using POSIX regex, anchored at start (with optional leading whitespace) + $numericPart = "CAST(substring({$field} FROM '^\\s*(\\d+\\.?\\d*)\\s*[pnuµmkKMGT]?') AS DOUBLE PRECISION)"; + + // Extract the SI prefix character + $prefixPart = "substring({$field} FROM '^\\s*\\d+\\.?\\d*\\s*([pnuµmkKMGT])')"; + + return $this->buildCaseExpression($numericPart, $prefixPart); + } + + /** + * MySQL/MariaDB implementation using REGEXP_SUBSTR. + */ + private function getMySQLSql(string $field): string + { + // Extract the numeric part, anchored at start (with optional leading whitespace) + $numericPart = "CAST(REGEXP_SUBSTR({$field}, '^[[:space:]]*[0-9]+\\.?[0-9]*') AS DECIMAL(30,15))"; + + // Extract the prefix: get the full number+prefix match anchored at start, then take the last char + $fullMatch = "REGEXP_SUBSTR({$field}, '^[[:space:]]*[0-9]+\\.?[0-9]*[[:space:]]*[pnuµmkKMGT]')"; + $prefixPart = "RIGHT({$fullMatch}, 1)"; + + return $this->buildCaseExpression($numericPart, $prefixPart); + } + + /** + * Build a CASE expression that maps an SI prefix character to a multiplier + * and multiplies it with the numeric value. + * + * @param string $numericExpr SQL expression that evaluates to the numeric part + * @param string $prefixExpr SQL expression that evaluates to the SI prefix character + * @return string SQL CASE expression + */ + private function buildCaseExpression(string $numericExpr, string $prefixExpr): string + { + return "(CASE" . + " WHEN {$numericExpr} IS NULL THEN NULL" . + " WHEN {$prefixExpr} = 'p' THEN {$numericExpr} * 1e-12" . + " WHEN {$prefixExpr} = 'n' THEN {$numericExpr} * 1e-9" . + " WHEN {$prefixExpr} = 'u' THEN {$numericExpr} * 1e-6" . + " WHEN {$prefixExpr} = 'µ' THEN {$numericExpr} * 1e-6" . + " WHEN {$prefixExpr} = 'm' THEN {$numericExpr} * 1e-3" . + " WHEN {$prefixExpr} = 'k' THEN {$numericExpr} * 1e3" . + " WHEN {$prefixExpr} = 'K' THEN {$numericExpr} * 1e3" . + " WHEN {$prefixExpr} = 'M' THEN {$numericExpr} * 1e6" . + " WHEN {$prefixExpr} = 'G' THEN {$numericExpr} * 1e9" . + " WHEN {$prefixExpr} = 'T' THEN {$numericExpr} * 1e12" . + " ELSE {$numericExpr} * 1" . + " END)"; + } + + /** + * PHP callback for SQLite's SI_VALUE function. + * Extracts the first numeric value with an optional SI prefix and returns the scaled value. + * + * @param string|null $value The input string + * @return float|null The scaled numeric value, or null if no number found + */ + public static function sqliteSiValue(?string $value): ?float + { + if ($value === null) { + return null; + } + + // Normalize comma decimal separator to dot (European locale support) + $value = str_replace(',', '.', $value); + + // Match a number at the very start (allowing leading whitespace), optionally followed by an SI prefix + if (!preg_match('/^\s*(\d+\.?\d*)\s*([pnuµmkKMGT])?/u', $value, $matches)) { + return null; + } + + $number = (float) $matches[1]; + $prefix = $matches[2] ?? ''; + + if ($prefix === '') { + return $number; + } + + $multiplier = self::SI_MULTIPLIERS[$prefix] ?? 1.0; //@phpstan-ignore-line - fallback to 1.0 if prefix is not recognized (should not happen due to regex) + + return $number * $multiplier; + } +} diff --git a/src/Doctrine/Helpers/FieldHelper.php b/src/Doctrine/Helpers/FieldHelper.php index 11300db3..6b6583f7 100644 --- a/src/Doctrine/Helpers/FieldHelper.php +++ b/src/Doctrine/Helpers/FieldHelper.php @@ -104,7 +104,7 @@ final class FieldHelper { $db_platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform(); - $key = 'field2_' . md5($field_expr); + $key = 'field2_' . hash('xxh3', $field_expr); //If we are on MySQL, we can just use the FIELD function if ($db_platform instanceof AbstractMySQLPlatform) { @@ -121,4 +121,4 @@ final class FieldHelper return $qb; } -} \ No newline at end of file +} diff --git a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php index ad572d4c..aa6108c9 100644 --- a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php +++ b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Doctrine\Middleware; +use App\Doctrine\Functions\SiValueSort; use App\Exceptions\InvalidRegexException; use Doctrine\DBAL\Driver\Connection; use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; @@ -51,6 +52,9 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware //Create a new collation for natural sorting $native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...)); + + //Create a function for SI prefix value sorting + $native_connection->sqliteCreateFunction('SI_VALUE', SiValueSort::sqliteSiValue(...), 1, \PDO::SQLITE_DETERMINISTIC); } } diff --git a/src/Doctrine/Types/BigDecimalType.php b/src/Doctrine/Types/BigDecimalType.php index a9f796dd..2712cce8 100644 --- a/src/Doctrine/Types/BigDecimalType.php +++ b/src/Doctrine/Types/BigDecimalType.php @@ -44,7 +44,7 @@ class BigDecimalType extends Type - return BigDecimal::of($value); + return BigDecimal::of(is_float($value) ? BigDecimal::fromFloatShortest($value) : $value); } /** diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 00cf581a..e0d1bd9d 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -97,7 +97,7 @@ use function in_array; #[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] abstract class Attachment extends AbstractNamedDBElement { - private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, + final public const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'PartCustomState' => PartCustomStateAttachment::class, 'Device' => ProjectAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class, 'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class, 'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class, @@ -107,7 +107,8 @@ abstract class Attachment extends AbstractNamedDBElement /* * The discriminator map used for API platform. The key should be the same as the api platform short type (the @type JSONLD field). */ - private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "AttachmentType" => AttachmentTypeAttachment::class, + private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "PartCustomState" => PartCustomStateAttachment::class, "Project" => ProjectAttachment::class, + "AttachmentType" => AttachmentTypeAttachment::class, "Category" => CategoryAttachment::class, "Footprint" => FootprintAttachment::class, "Manufacturer" => ManufacturerAttachment::class, "Currency" => CurrencyAttachment::class, "Group" => GroupAttachment::class, "MeasurementUnit" => MeasurementUnitAttachment::class, "StorageLocation" => StorageLocationAttachment::class, "Supplier" => SupplierAttachment::class, "User" => UserAttachment::class, "LabelProfile" => LabelAttachment::class]; @@ -135,7 +136,7 @@ abstract class Attachment extends AbstractNamedDBElement * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses. * @phpstan-var class-string */ - protected const ALLOWED_ELEMENT_CLASS = AttachmentContainingDBElement::class; + public const ALLOWED_ELEMENT_CLASS = AttachmentContainingDBElement::class; /** * @var AttachmentUpload|null The options used for uploading a file to this attachment or modify it. @@ -165,9 +166,10 @@ abstract class Attachment extends AbstractNamedDBElement * @var string|null The path to the external source if the file is stored externally or was downloaded from an * external source. Null if there is no external source. */ - #[ORM\Column(type: Types::STRING, nullable: true)] + #[ORM\Column(type: Types::STRING, length: 2048, nullable: true)] #[Groups(['attachment:read'])] #[ApiProperty(example: 'http://example.com/image.jpg')] + #[Assert\Length(max: 2048)] protected ?string $external_path = null; /** @@ -294,6 +296,22 @@ abstract class Attachment extends AbstractNamedDBElement return in_array(strtolower($extension), static::MODEL_EXTS, true); } + /** + * Returns true if this is a locally stored HTML file, which can be shown by the sandbox viewer. + * This is the case if we have an internal path with a html extension. + * @return bool + */ + public function isLocalHTMLFile(): bool + { + if($this->hasInternal()){ + + $extension = pathinfo($this->getFilename(), PATHINFO_EXTENSION); + + return in_array(strtolower($extension), ['html', 'htm'], true); + } + return false; + } + /** * Checks if this attachment has a path to an external file * @@ -550,8 +568,8 @@ abstract class Attachment extends AbstractNamedDBElement */ #[Groups(['attachment:write'])] #[SerializedName('url')] - #[ApiProperty(description: 'Set the path of the attachment here. - Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty + #[ApiProperty(description: 'Set the path of the attachment here. + Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty string if the attachment has an internal file associated and you\'d like to reset the external source. If you set a new (nonempty) file path any associated internal file will be removed!')] public function setURL(?string $url): self diff --git a/src/Entity/Attachments/AttachmentType.php b/src/Entity/Attachments/AttachmentType.php index 22333c16..7a314ffe 100644 --- a/src/Entity/Attachments/AttachmentType.php +++ b/src/Entity/Attachments/AttachmentType.php @@ -134,6 +134,17 @@ class AttachmentType extends AbstractStructuralDBElement #[ORM\OneToMany(mappedBy: 'attachment_type', targetEntity: Attachment::class)] protected Collection $attachments_with_type; + /** + * @var string[]|null A list of allowed targets where this attachment type can be assigned to, as a list of portable names + */ + #[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)] + protected ?array $allowed_targets = null; + + /** + * @var class-string[]|null + */ + protected ?array $allowed_targets_parsed_cache = null; + #[Groups(['attachment_type:read'])] protected ?\DateTimeImmutable $addedDate = null; #[Groups(['attachment_type:read'])] @@ -184,4 +195,81 @@ class AttachmentType extends AbstractStructuralDBElement return $this; } + + /** + * Returns a list of allowed targets as class names (e.g. PartAttachment::class), where this attachment type can be assigned to. If null, there are no restrictions. + * @return class-string[]|null + */ + public function getAllowedTargets(): ?array + { + //Use cached value if available + if ($this->allowed_targets_parsed_cache !== null) { + return $this->allowed_targets_parsed_cache; + } + + if (empty($this->allowed_targets)) { + return null; + } + + $tmp = []; + foreach ($this->allowed_targets as $target) { + if (isset(Attachment::ORM_DISCRIMINATOR_MAP[$target])) { + $tmp[] = Attachment::ORM_DISCRIMINATOR_MAP[$target]; + } + //Otherwise ignore the entry, as it is invalid + } + + //Cache the parsed value + $this->allowed_targets_parsed_cache = $tmp; + return $tmp; + } + + /** + * Sets the allowed targets for this attachment type. Allowed targets are specified as a list of class names (e.g. PartAttachment::class). If null is passed, there are no restrictions. + * @param class-string[]|null $allowed_targets + * @return $this + */ + public function setAllowedTargets(?array $allowed_targets): self + { + if ($allowed_targets === null) { + $this->allowed_targets = null; + } else { + $tmp = []; + foreach ($allowed_targets as $target) { + $discriminator = array_search($target, Attachment::ORM_DISCRIMINATOR_MAP, true); + if ($discriminator !== false) { + $tmp[] = $discriminator; + } else { + throw new \InvalidArgumentException("Invalid allowed target: $target. Allowed targets must be a class name of an Attachment subclass."); + } + } + $this->allowed_targets = $tmp; + } + + //Reset the cache + $this->allowed_targets_parsed_cache = null; + return $this; + } + + /** + * Checks if this attachment type is allowed for the given attachment target. + * @param Attachment|string $attachment + * @return bool + */ + public function isAllowedForTarget(Attachment|string $attachment): bool + { + //If no restrictions are set, allow all targets + if ($this->getAllowedTargets() === null) { + return true; + } + + //Iterate over all allowed targets and check if the attachment is an instance of any of them + foreach ($this->getAllowedTargets() as $allowed_target) { + if (is_a($attachment, $allowed_target, true)) { + return true; + } + } + + return false; + } } diff --git a/src/Entity/Attachments/PartCustomStateAttachment.php b/src/Entity/Attachments/PartCustomStateAttachment.php new file mode 100644 index 00000000..3a561b13 --- /dev/null +++ b/src/Entity/Attachments/PartCustomStateAttachment.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Attachments; + +use App\Entity\Parts\PartCustomState; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +/** + * An attachment attached to a part custom state element. + * @extends Attachment + */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] +class PartCustomStateAttachment extends Attachment +{ + final public const ALLOWED_ELEMENT_CLASS = PartCustomState::class; + + #[ORM\ManyToOne(targetEntity: PartCustomState::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AttachmentContainingDBElement $element = null; +} diff --git a/src/Entity/Base/AbstractCompany.php b/src/Entity/Base/AbstractCompany.php index 57a3f722..7d05c93f 100644 --- a/src/Entity/Base/AbstractCompany.php +++ b/src/Entity/Base/AbstractCompany.php @@ -83,8 +83,8 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement */ #[Assert\Url(requireTld: false)] #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] - #[ORM\Column(type: Types::STRING)] - #[Assert\Length(max: 255)] + #[ORM\Column(type: Types::STRING, length: 2048)] + #[Assert\Length(max: 2048)] protected string $website = ''; #[Groups(['company:read', 'company:write', 'import', 'full', 'extended'])] @@ -93,8 +93,8 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement /** * @var string The link to the website of an article. Use %PARTNUMBER% as placeholder for the part number. */ - #[ORM\Column(type: Types::STRING)] - #[Assert\Length(max: 255)] + #[ORM\Column(type: Types::STRING, length: 2048)] + #[Assert\Length(max: 2048)] #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] protected string $auto_product_url = ''; diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index 9fb5d648..a088b3df 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -33,6 +33,7 @@ use App\Entity\Attachments\LabelAttachment; use App\Entity\Attachments\ManufacturerAttachment; use App\Entity\Attachments\MeasurementUnitAttachment; use App\Entity\Attachments\PartAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; @@ -40,6 +41,7 @@ use App\Entity\Attachments\UserAttachment; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Category; use App\Entity\PriceInformations\Pricedetail; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Entity\Parts\Footprint; @@ -68,7 +70,41 @@ use Symfony\Component\Serializer\Annotation\Groups; * Every database table which are managed with this class (or a subclass of it) * must have the table row "id"!! The ID is the unique key to identify the elements. */ -#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])] +#[DiscriminatorMap(typeProperty: 'type', mapping: [ + 'attachment_type' => AttachmentType::class, + 'attachment' => Attachment::class, + 'attachment_type_attachment' => AttachmentTypeAttachment::class, + 'category_attachment' => CategoryAttachment::class, + 'currency_attachment' => CurrencyAttachment::class, + 'footprint_attachment' => FootprintAttachment::class, + 'group_attachment' => GroupAttachment::class, + 'label_attachment' => LabelAttachment::class, + 'manufacturer_attachment' => ManufacturerAttachment::class, + 'measurement_unit_attachment' => MeasurementUnitAttachment::class, + 'part_attachment' => PartAttachment::class, + 'part_custom_state_attachment' => PartCustomStateAttachment::class, + 'project_attachment' => ProjectAttachment::class, + 'storelocation_attachment' => StorageLocationAttachment::class, + 'supplier_attachment' => SupplierAttachment::class, + 'user_attachment' => UserAttachment::class, + 'category' => Category::class, + 'project' => Project::class, + 'project_bom_entry' => ProjectBOMEntry::class, + 'footprint' => Footprint::class, + 'group' => Group::class, + 'manufacturer' => Manufacturer::class, + 'orderdetail' => Orderdetail::class, + 'part' => Part::class, + 'part_custom_state' => PartCustomState::class, + 'pricedetail' => Pricedetail::class, + 'storelocation' => StorageLocation::class, + 'part_lot' => PartLot::class, + 'currency' => Currency::class, + 'measurement_unit' => MeasurementUnit::class, + 'parameter' => AbstractParameter::class, + 'supplier' => Supplier::class, + 'user' => User::class] +)] #[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)] abstract class AbstractDBElement implements JsonSerializable { diff --git a/src/Entity/EDA/EDACategoryInfo.php b/src/Entity/EDA/EDACategoryInfo.php index 0163dfb3..40b3ff64 100644 --- a/src/Entity/EDA/EDACategoryInfo.php +++ b/src/Entity/EDA/EDACategoryInfo.php @@ -58,7 +58,7 @@ class EDACategoryInfo /** @var bool|null If this is set to true, then this part will be excluded in the simulation */ #[Column(type: Types::BOOLEAN, nullable: true)] #[Groups(['full', 'category:read', 'category:write', 'import'])] - private ?bool $exclude_from_sim = true; + private ?bool $exclude_from_sim = null; /** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */ #[Column(type: Types::STRING, nullable: true)] diff --git a/src/Entity/LabelSystem/LabelProfile.php b/src/Entity/LabelSystem/LabelProfile.php index d3616c34..236c07f7 100644 --- a/src/Entity/LabelSystem/LabelProfile.php +++ b/src/Entity/LabelSystem/LabelProfile.php @@ -41,6 +41,12 @@ declare(strict_types=1); namespace App\Entity\LabelSystem; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; use Doctrine\Common\Collections\Criteria; use App\Entity\Attachments\Attachment; use App\Repository\LabelProfileRepository; @@ -58,6 +64,22 @@ use Symfony\Component\Validator\Constraints as Assert; /** * @extends AttachmentContainingDBElement */ +#[ApiResource( + operations: [ + new Get( + normalizationContext: ['groups' => ['label_profile:read', 'simple']], + security: "is_granted('read', object)", + openapi: new Operation(summary: 'Get a label profile by ID') + ), + new GetCollection( + normalizationContext: ['groups' => ['label_profile:read', 'simple']], + security: "is_granted('@labels.create_labels')", + openapi: new Operation(summary: 'List all available label profiles') + ), + ], + paginationEnabled: false, +)] +#[ApiFilter(SearchFilter::class, properties: ['options.supported_element' => 'exact', 'show_in_dropdown' => 'exact'])] #[UniqueEntity(['name', 'options.supported_element'])] #[ORM\Entity(repositoryClass: LabelProfileRepository::class)] #[ORM\EntityListeners([TreeCacheInvalidationListener::class])] @@ -80,20 +102,21 @@ class LabelProfile extends AttachmentContainingDBElement */ #[Assert\Valid] #[ORM\Embedded(class: 'LabelOptions')] - #[Groups(["extended", "full", "import"])] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected LabelOptions $options; /** * @var string The comment info for this element */ #[ORM\Column(type: Types::TEXT)] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected string $comment = ''; /** * @var bool determines, if this label profile should be shown in the dropdown quick menu */ #[ORM\Column(type: Types::BOOLEAN)] - #[Groups(["extended", "full", "import"])] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected bool $show_in_dropdown = true; public function __construct() diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php index 16bf33f5..34ab8fba 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -46,6 +46,7 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; @@ -58,6 +59,8 @@ use App\Entity\Attachments\UserAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; @@ -158,6 +161,7 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU Part::class => PartParameter::class, StorageLocation::class => StorageLocationParameter::class, Supplier::class => SupplierParameter::class, + PartCustomState::class => PartCustomStateParameter::class, default => throw new \RuntimeException('Unknown target class for parameter: '.$this->getTargetClass()), }; } @@ -173,6 +177,7 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU Manufacturer::class => ManufacturerAttachment::class, MeasurementUnit::class => MeasurementUnitAttachment::class, Part::class => PartAttachment::class, + PartCustomState::class => PartCustomStateAttachment::class, StorageLocation::class => StorageLocationAttachment::class, Supplier::class => SupplierAttachment::class, User::class => UserAttachment::class, diff --git a/src/Entity/LogSystem/LogTargetType.php b/src/Entity/LogSystem/LogTargetType.php index 61a2b081..3b2d8682 100644 --- a/src/Entity/LogSystem/LogTargetType.php +++ b/src/Entity/LogSystem/LogTargetType.php @@ -34,6 +34,7 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; @@ -71,6 +72,7 @@ enum LogTargetType: int case PART_ASSOCIATION = 20; case BULK_INFO_PROVIDER_IMPORT_JOB = 21; case BULK_INFO_PROVIDER_IMPORT_JOB_PART = 22; + case PART_CUSTOM_STATE = 23; /** * Returns the class name of the target type or null if the target type is NONE. @@ -102,6 +104,7 @@ enum LogTargetType: int self::PART_ASSOCIATION => PartAssociation::class, self::BULK_INFO_PROVIDER_IMPORT_JOB => BulkInfoProviderImportJob::class, self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => BulkInfoProviderImportJobPart::class, + self::PART_CUSTOM_STATE => PartCustomState::class }; } diff --git a/src/Entity/LogSystem/PartStockChangeType.php b/src/Entity/LogSystem/PartStockChangeType.php index f69fe95f..79e4c6da 100644 --- a/src/Entity/LogSystem/PartStockChangeType.php +++ b/src/Entity/LogSystem/PartStockChangeType.php @@ -28,6 +28,8 @@ enum PartStockChangeType: string case WITHDRAW = "withdraw"; case MOVE = "move"; + case STOCKTAKE = "stock_take"; + /** * Converts the type to a short representation usable in the extra field of the log entry. * @return string @@ -38,6 +40,7 @@ enum PartStockChangeType: string self::ADD => 'a', self::WITHDRAW => 'w', self::MOVE => 'm', + self::STOCKTAKE => 's', }; } @@ -52,6 +55,7 @@ enum PartStockChangeType: string 'a' => self::ADD, 'w' => self::WITHDRAW, 'm' => self::MOVE, + 's' => self::STOCKTAKE, default => throw new \InvalidArgumentException("Invalid short type: $value"), }; } diff --git a/src/Entity/LogSystem/PartStockChangedLogEntry.php b/src/Entity/LogSystem/PartStockChangedLogEntry.php index 1bac9e9f..a46f2ecf 100644 --- a/src/Entity/LogSystem/PartStockChangedLogEntry.php +++ b/src/Entity/LogSystem/PartStockChangedLogEntry.php @@ -122,6 +122,11 @@ class PartStockChangedLogEntry extends AbstractLogEntry return new self(PartStockChangeType::MOVE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, $move_to_target, action_timestamp: $action_timestamp); } + public static function stocktake(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?\DateTimeInterface $action_timestamp = null): self + { + return new self(PartStockChangeType::STOCKTAKE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, action_timestamp: $action_timestamp); + } + /** * Returns the instock change type of this entry * @return PartStockChangeType diff --git a/src/Entity/Parameters/AbstractParameter.php b/src/Entity/Parameters/AbstractParameter.php index 39f333da..f47f2e82 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -73,7 +73,8 @@ use function sprintf; #[ORM\DiscriminatorMap([0 => CategoryParameter::class, 1 => CurrencyParameter::class, 2 => ProjectParameter::class, 3 => FootprintParameter::class, 4 => GroupParameter::class, 5 => ManufacturerParameter::class, 6 => MeasurementUnitParameter::class, 7 => PartParameter::class, 8 => StorageLocationParameter::class, - 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class])] + 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class, + 12 => PartCustomStateParameter::class])] #[ORM\Table('parameters')] #[ORM\Index(columns: ['name'], name: 'parameter_name_idx')] #[ORM\Index(columns: ['param_group'], name: 'parameter_group_idx')] @@ -105,7 +106,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu "AttachmentType" => AttachmentTypeParameter::class, "Category" => CategoryParameter::class, "Currency" => CurrencyParameter::class, "Project" => ProjectParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class, "Manufacturer" => ManufacturerParameter::class, "MeasurementUnit" => MeasurementUnitParameter::class, - "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class]; + "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class, "PartCustomState" => PartCustomStateParameter::class]; /** * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses. @@ -123,7 +124,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu /** * @var float|null the guaranteed minimum value of this property */ - #[Assert\Type(['float', null])] + #[Assert\Type(['float', 'null'])] #[Assert\LessThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.min_lesser_typical')] #[Assert\LessThan(propertyPath: 'value_max', message: 'parameters.validator.min_lesser_max')] #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] @@ -133,7 +134,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu /** * @var float|null the typical value of this property */ - #[Assert\Type([null, 'float'])] + #[Assert\Type(['null', 'float'])] #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] #[ORM\Column(type: Types::FLOAT, nullable: true)] protected ?float $value_typical = null; @@ -141,7 +142,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu /** * @var float|null the maximum value of this property */ - #[Assert\Type(['float', null])] + #[Assert\Type(['float', 'null'])] #[Assert\GreaterThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.max_greater_typical')] #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] #[ORM\Column(type: Types::FLOAT, nullable: true)] @@ -171,6 +172,13 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu #[Assert\Length(max: 255)] protected string $group = ''; + /** + * @var bool|null Whether this parameter should be exported as a field in the EDA HTTP library API. Null means use system default. + */ + #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] + #[ORM\Column(type: Types::BOOLEAN, nullable: true, options: ['default' => null])] + protected ?bool $eda_visibility = null; + /** * Mapping is done in subclasses. * @@ -460,7 +468,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu return $str; } - + /** * Returns the class of the element that is allowed to be associated with this attachment. * @return string @@ -470,6 +478,21 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu return static::ALLOWED_ELEMENT_CLASS; } + public function isEdaVisibility(): ?bool + { + return $this->eda_visibility; + } + + /** + * @return $this + */ + public function setEdaVisibility(?bool $eda_visibility): self + { + $this->eda_visibility = $eda_visibility; + + return $this; + } + public function getComparableFields(): array { return ['name' => $this->name, 'group' => $this->group, 'element' => $this->element?->getId()]; diff --git a/src/Entity/Parameters/PartCustomStateParameter.php b/src/Entity/Parameters/PartCustomStateParameter.php new file mode 100644 index 00000000..ceedf7b4 --- /dev/null +++ b/src/Entity/Parameters/PartCustomStateParameter.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace App\Entity\Parameters; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\PartCustomState; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; + +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] +class PartCustomStateParameter extends AbstractParameter +{ + final public const ALLOWED_ELEMENT_CLASS = PartCustomState::class; + + /** + * @var PartCustomState the element this para is associated with + */ + #[ORM\ManyToOne(targetEntity: PartCustomState::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; +} diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 99ed3c6d..22f8a3e4 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -118,6 +118,13 @@ class Category extends AbstractPartsContainingDBElement #[ORM\Column(type: Types::TEXT)] protected string $partname_regex = ''; + /** + * @var string The prefix for ipn generation for created parts in this category. + */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: false, options: ['default' => ''])] + protected string $part_ipn_prefix = ''; + /** * @var bool Set to true, if the footprints should be disabled for parts this category (not implemented yet). */ @@ -201,6 +208,15 @@ class Category extends AbstractPartsContainingDBElement $this->eda_info = new EDACategoryInfo(); } + public function __clone() + { + if ($this->id) { + //Clone EDA info to prevent changes to the original EDA info when changing the cloned category + $this->eda_info = clone $this->eda_info; + } + parent::__clone(); + } + public function getPartnameHint(): string { return $this->partname_hint; @@ -225,6 +241,16 @@ class Category extends AbstractPartsContainingDBElement return $this; } + public function getPartIpnPrefix(): string + { + return $this->part_ipn_prefix; + } + + public function setPartIpnPrefix(string $part_ipn_prefix): void + { + $this->part_ipn_prefix = $part_ipn_prefix; + } + public function isDisableFootprints(): bool { return $this->disable_footprints; diff --git a/src/Entity/Parts/Footprint.php b/src/Entity/Parts/Footprint.php index 6b043562..3d8be686 100644 --- a/src/Entity/Parts/Footprint.php +++ b/src/Entity/Parts/Footprint.php @@ -152,6 +152,15 @@ class Footprint extends AbstractPartsContainingDBElement $this->eda_info = new EDAFootprintInfo(); } + public function __clone() + { + if ($this->id) { + //Clone EDA info to prevent changes to the original EDA info when changing the cloned category + $this->eda_info = clone $this->eda_info; + } + parent::__clone(); + } + /**************************************** * Getters ****************************************/ diff --git a/src/Entity/Parts/InfoProviderReference.php b/src/Entity/Parts/InfoProviderReference.php index bfa62f32..810aef0c 100644 --- a/src/Entity/Parts/InfoProviderReference.php +++ b/src/Entity/Parts/InfoProviderReference.php @@ -50,7 +50,7 @@ class InfoProviderReference /** * @var string|null The url of this part inside the provider system or null if this info is not existing */ - #[Column(type: Types::STRING, nullable: true)] + #[Column(type: Types::STRING, length: 2048, nullable: true)] #[Groups(['provider_reference:read', 'full'])] private ?string $provider_url = null; @@ -157,4 +157,4 @@ class InfoProviderReference $ref->last_updated = new \DateTimeImmutable(); return $ref; } -} \ No newline at end of file +} diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 4a503ccd..0209c520 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -62,7 +62,6 @@ use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; use Doctrine\DBAL\Types\Types; -use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -76,13 +75,13 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; * @extends AttachmentContainingDBElement * @template-use ParametersTrait */ -#[UniqueEntity(fields: ['ipn'], message: 'part.ipn.must_be_unique')] #[ORM\Entity(repositoryClass: PartRepository::class)] #[ORM\EntityListeners([TreeCacheInvalidationListener::class])] #[ORM\Table('`parts`')] #[ORM\Index(columns: ['datetime_added', 'name', 'last_modified', 'id', 'needs_review'], name: 'parts_idx_datet_name_last_id_needs')] #[ORM\Index(columns: ['name'], name: 'parts_idx_name')] #[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')] +#[ORM\Index(columns: ['gtin'], name: 'parts_idx_gtin')] #[ApiResource( operations: [ new Get(normalizationContext: [ @@ -108,7 +107,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], )] #[ApiFilter(PropertyFilter::class)] -#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])] +#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit", "partCustomState"])] #[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])] #[ApiFilter(TagFilter::class, properties: ["tags"])] @@ -138,6 +137,7 @@ class Part extends AttachmentContainingDBElement #[UniqueObjectCollection(fields: ['name', 'group', 'element'])] protected Collection $parameters; + /** ************************************************************* * Overridden properties * (They are defined here and not in a trait, to avoid conflicts). diff --git a/src/Entity/Parts/PartCustomState.php b/src/Entity/Parts/PartCustomState.php new file mode 100644 index 00000000..136ff984 --- /dev/null +++ b/src/Entity/Parts/PartCustomState.php @@ -0,0 +1,127 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Parts; + +use ApiPlatform\Metadata\ApiProperty; +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\PartCustomStateAttachment; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Base\AbstractPartsContainingDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\PartCustomStateParameter; +use App\Repository\Parts\PartCustomStateRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\Criteria; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * This entity represents a custom part state. + * If an organisation uses Part-DB and has its custom part states, this is useful. + * + * @extends AbstractPartsContainingDBElement + */ +#[ORM\Entity(repositoryClass: PartCustomStateRepository::class)] +#[ORM\Table('`part_custom_states`')] +#[ORM\Index(columns: ['name'], name: 'part_custom_state_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@part_custom_states.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['part_custom_state:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['part_custom_state:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +class PartCustomState extends AbstractPartsContainingDBElement +{ + /** + * @var string The comment info for this element as markdown + */ + #[Groups(['part_custom_state:read', 'part_custom_state:write', 'full', 'import'])] + protected string $comment = ''; + + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; + + /** + * @var Collection + */ + #[Assert\Valid] + #[ORM\OneToMany(targetEntity: PartCustomStateAttachment::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: PartCustomStateAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected ?Attachment $master_picture_attachment = null; + + /** @var Collection + */ + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: PartCustomStateParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => 'ASC'])] + #[Groups(['part_custom_state:read', 'part_custom_state:write'])] + protected Collection $parameters; + + #[Groups(['part_custom_state:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['part_custom_state:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + } +} diff --git a/src/Entity/Parts/PartLot.php b/src/Entity/Parts/PartLot.php index d893e6de..a15eeb4f 100644 --- a/src/Entity/Parts/PartLot.php +++ b/src/Entity/Parts/PartLot.php @@ -66,7 +66,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ORM\Table(name: 'part_lots')] #[ORM\Index(columns: ['instock_unknown', 'expiration_date', 'id_part'], name: 'part_lots_idx_instock_un_expiration_id_part')] #[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')] -#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')] +#[ORM\Index(name: 'part_lots_idx_barcode', columns: ['vendor_barcode'], options: ['lengths' => [100]])] #[ValidPartLot] #[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')] #[ApiResource( @@ -81,7 +81,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; denormalizationContext: ['groups' => ['part_lot:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], )] #[ApiFilter(PropertyFilter::class)] -#[ApiFilter(LikeFilter::class, properties: ["description", "comment"])] +#[ApiFilter(LikeFilter::class, properties: ["description", "comment", "user_barcode"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] #[ApiFilter(BooleanFilter::class, properties: ['instock_unknown', 'needs_refill'])] #[ApiFilter(RangeFilter::class, properties: ['amount'])] @@ -166,11 +166,18 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor) */ - #[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)] + #[ORM\Column(name: "vendor_barcode", type: Types::TEXT, nullable: true)] #[Groups(['part_lot:read', 'part_lot:write'])] - #[Length(max: 255)] protected ?string $user_barcode = null; + /** + * @var \DateTimeImmutable|null The date when the last stocktake was performed for this part lot. Set to null, if no stocktake was performed yet. + */ + #[Groups(['extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column( type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Year2038BugWorkaround] + protected ?\DateTimeImmutable $last_stocktake_at = null; + public function __clone() { if ($this->id) { @@ -391,6 +398,26 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named return $this; } + /** + * Returns the date when the last stocktake was performed for this part lot. Returns null, if no stocktake was performed yet. + * @return \DateTimeImmutable|null + */ + public function getLastStocktakeAt(): ?\DateTimeImmutable + { + return $this->last_stocktake_at; + } + + /** + * Sets the date when the last stocktake was performed for this part lot. Set to null, if no stocktake was performed yet. + * @param \DateTimeImmutable|null $last_stocktake_at + * @return $this + */ + public function setLastStocktakeAt(?\DateTimeImmutable $last_stocktake_at): self + { + $this->last_stocktake_at = $last_stocktake_at; + return $this; + } + #[Assert\Callback] diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index a502e49e..ac8135b8 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -24,12 +24,15 @@ namespace App\Entity\Parts\PartTraits; use App\Entity\Parts\InfoProviderReference; use App\Validator\Constraints\Year2038BugWorkaround; +use App\Entity\Parts\PartCustomState; +use App\Validator\Constraints\ValidGTIN; use Doctrine\DBAL\Types\Types; use App\Entity\Parts\Part; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\Length; +use App\Validator\Constraints\UniquePartIpnConstraint; /** * Advanced properties of a part, not related to a more specific group. @@ -74,6 +77,7 @@ trait AdvancedPropertyTrait #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] #[Length(max: 100)] + #[UniquePartIpnConstraint] protected ?string $ipn = null; /** @@ -83,6 +87,22 @@ trait AdvancedPropertyTrait #[Groups(['full', 'part:read'])] protected InfoProviderReference $providerReference; + /** + * @var ?PartCustomState the custom state for the part + */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\ManyToOne(targetEntity: PartCustomState::class)] + #[ORM\JoinColumn(name: 'id_part_custom_state')] + protected ?PartCustomState $partCustomState = null; + + /** + * @var string|null The GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code + */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::STRING, nullable: true)] + #[ValidGTIN] + protected ?string $gtin = null; + /** * Checks if this part is marked, for that it needs further review. */ @@ -211,7 +231,46 @@ trait AdvancedPropertyTrait return $this; } + /** + * Gets the custom part state for the part + * Returns null if no specific part state is set. + */ + public function getPartCustomState(): ?PartCustomState + { + return $this->partCustomState; + } + /** + * Sets the custom part state. + * + * @return $this + */ + public function setPartCustomState(?PartCustomState $partCustomState): self + { + $this->partCustomState = $partCustomState; + return $this; + } + /** + * Gets the GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code. + * Returns null if no GTIN is set. + */ + public function getGtin(): ?string + { + return $this->gtin; + } + + /** + * Sets the GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code. + * + * @param string|null $gtin The new GTIN of the part + * + * @return $this + */ + public function setGtin(?string $gtin): self + { + $this->gtin = $gtin; + return $this; + } } diff --git a/src/Entity/PriceInformations/Currency.php b/src/Entity/PriceInformations/Currency.php index ce20caf8..4a811aa0 100644 --- a/src/Entity/PriceInformations/Currency.php +++ b/src/Entity/PriceInformations/Currency.php @@ -204,7 +204,7 @@ class Currency extends AbstractStructuralDBElement return null; } - return BigDecimal::one()->dividedBy($tmp, $tmp->getScale(), RoundingMode::HALF_UP); + return BigDecimal::one()->dividedBy($tmp, $tmp->getScale(), RoundingMode::HalfUp); } /** diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php index 8ed76a46..56428e3a 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -52,6 +52,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\Length; @@ -121,6 +122,13 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N #[ORM\Column(type: Types::BOOLEAN)] protected bool $obsolete = false; + /** + * @var bool|null Whether this orderdetail's supplier part number should be exported as an EDA field. Null means use system default. + */ + #[Groups(['full', 'import', 'orderdetail:read', 'orderdetail:write'])] + #[ORM\Column(type: Types::BOOLEAN, nullable: true, options: ['default' => null])] + protected ?bool $eda_visibility = null; + /** * @var string The URL to the product on the supplier's website */ @@ -147,6 +155,13 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N #[ORM\JoinColumn(name: 'id_supplier')] protected ?Supplier $supplier = null; + /** + * @var bool|null Whether the prices includes VAT or not. Null means, that it is not specified, if the prices includes VAT or not. + */ + #[ORM\Column(type: Types::BOOLEAN, nullable: true)] + #[Groups(['extended', 'full', 'import', 'orderdetail:read', 'orderdetail:write'])] + protected ?bool $prices_includes_vat = null; + public function __construct() { $this->pricedetails = new ArrayCollection(); @@ -388,6 +403,43 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N return $this; } + /** + * Checks if the prices of this orderdetail include VAT. Null means, that it is not specified, if the prices includes + * VAT or not. + * @return bool|null + */ + public function getPricesIncludesVAT(): ?bool + { + return $this->prices_includes_vat; + } + + /** + * Sets whether the prices of this orderdetail include VAT. + * @param bool|null $includesVat + * @return $this + */ + public function setPricesIncludesVAT(?bool $includesVat): self + { + $this->prices_includes_vat = $includesVat; + + return $this; + } + + public function isEdaVisibility(): ?bool + { + return $this->eda_visibility; + } + + /** + * @return $this + */ + public function setEdaVisibility(?bool $eda_visibility): self + { + $this->eda_visibility = $eda_visibility; + + return $this; + } + public function getName(): string { return $this->getSupplierPartNr(); diff --git a/src/Entity/PriceInformations/Pricedetail.php b/src/Entity/PriceInformations/Pricedetail.php index 86a7bcd5..58e02c64 100644 --- a/src/Entity/PriceInformations/Pricedetail.php +++ b/src/Entity/PriceInformations/Pricedetail.php @@ -121,6 +121,8 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface #[Groups(['pricedetail:read:standalone', 'pricedetail:write'])] protected ?Orderdetail $orderdetail = null; + + public function __construct() { $this->price = BigDecimal::zero()->toScale(self::PRICE_PRECISION); @@ -193,10 +195,10 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface #[SerializedName('price_per_unit')] public function getPricePerUnit(float|string|BigDecimal $multiplier = 1.0): BigDecimal { - $tmp = BigDecimal::of($multiplier); + $tmp = is_float($multiplier) ? BigDecimal::fromFloatShortest($multiplier) : BigDecimal::of($multiplier); $tmp = $tmp->multipliedBy($this->price); - return $tmp->dividedBy($this->price_related_quantity, static::PRICE_PRECISION, RoundingMode::HALF_UP); + return $tmp->dividedBy(BigDecimal::fromFloatShortest($this->price_related_quantity), static::PRICE_PRECISION, RoundingMode::HalfUp); } /** @@ -264,6 +266,15 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface return $this->currency?->getIsoCode(); } + /** + * Returns whether the price includes VAT or not. Null means, that it is not specified, if the price includes VAT or not. + * @return bool|null + */ + public function getIncludesVat(): ?bool + { + return $this->orderdetail?->getPricesIncludesVAT(); + } + /******************************************************************************** * * Setters @@ -306,7 +317,7 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface */ public function setPrice(BigDecimal $new_price): self { - $tmp = $new_price->toScale(self::PRICE_PRECISION, RoundingMode::HALF_UP); + $tmp = $new_price->toScale(self::PRICE_PRECISION, RoundingMode::HalfUp); //Only change the object, if the value changes, so that doctrine does not detect it as changed. if ((string) $tmp !== (string) $this->price) { $this->price = $tmp; diff --git a/src/Entity/UserSystem/PermissionData.php b/src/Entity/UserSystem/PermissionData.php index 9ebdc9c9..b7d1ff8f 100644 --- a/src/Entity/UserSystem/PermissionData.php +++ b/src/Entity/UserSystem/PermissionData.php @@ -43,7 +43,7 @@ final class PermissionData implements \JsonSerializable /** * The current schema version of the permission data */ - public const CURRENT_SCHEMA_VERSION = 3; + public const CURRENT_SCHEMA_VERSION = 4; /** * Creates a new Permission Data Instance using the given data. diff --git a/src/EnvVarProcessors/AddSlashEnvVarProcessor.php b/src/EnvVarProcessors/AddSlashEnvVarProcessor.php new file mode 100644 index 00000000..aaf0abc9 --- /dev/null +++ b/src/EnvVarProcessors/AddSlashEnvVarProcessor.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EnvVarProcessors; + +use Symfony\Component\DependencyInjection\EnvVarProcessorInterface; + +/** + * Env var processor that adds a trailing slash to a string if not already present. + */ +final class AddSlashEnvVarProcessor implements EnvVarProcessorInterface +{ + + public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed + { + $env = $getEnv($name); + if (!is_string($env)) { + throw new \InvalidArgumentException(sprintf('The "addSlash" env var processor only works with strings, got %s.', gettype($env))); + } + return rtrim($env, '/') . '/'; + } + + public static function getProvidedTypes(): array + { + return [ + 'addSlash' => 'string', + ]; + } +} diff --git a/src/Services/CustomEnvVarProcessor.php b/src/EnvVarProcessors/CustomEnvVarProcessor.php similarity index 98% rename from src/Services/CustomEnvVarProcessor.php rename to src/EnvVarProcessors/CustomEnvVarProcessor.php index f269cc7d..55a6b94d 100644 --- a/src/Services/CustomEnvVarProcessor.php +++ b/src/EnvVarProcessors/CustomEnvVarProcessor.php @@ -20,7 +20,7 @@ declare(strict_types=1); -namespace App\Services; +namespace App\EnvVarProcessors; use Closure; use Symfony\Component\DependencyInjection\EnvVarProcessorInterface; diff --git a/src/EventListener/RegisterSynonymsAsTranslationParametersListener.php b/src/EventListener/RegisterSynonymsAsTranslationParametersListener.php new file mode 100644 index 00000000..5862fa33 --- /dev/null +++ b/src/EventListener/RegisterSynonymsAsTranslationParametersListener.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventListener; + +use App\Services\ElementTypeNameGenerator; +use App\Services\ElementTypes; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Translation\Translator; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +#[AsEventListener] +readonly class RegisterSynonymsAsTranslationParametersListener +{ + private Translator $translator; + + public function __construct( + #[Autowire(service: 'translator.default')] TranslatorInterface $translator, + private TagAwareCacheInterface $cache, + private ElementTypeNameGenerator $typeNameGenerator) + { + if (!$translator instanceof Translator) { + throw new \RuntimeException('Translator must be an instance of Symfony\Component\Translation\Translator or this listener cannot be used.'); + } + $this->translator = $translator; + } + + public function getSynonymPlaceholders(string $locale): array + { + return $this->cache->get('partdb_synonym_placeholders' . '_' . $locale, function (ItemInterface $item) use ($locale) { + $item->tag('synonyms'); + + + $placeholders = []; + + //Generate a placeholder for each element type + foreach (ElementTypes::cases() as $elementType) { + //Versions with capitalized first letter + $capitalized = ucfirst($elementType->value); //We have only ASCII element type values, so this is sufficient + $placeholders['[' . $capitalized . ']'] = $this->typeNameGenerator->typeLabel($elementType, $locale); + $placeholders['[[' . $capitalized . ']]'] = $this->typeNameGenerator->typeLabelPlural($elementType, $locale); + + //And we have lowercase versions for both + $placeholders['[' . $elementType->value . ']'] = mb_strtolower($this->typeNameGenerator->typeLabel($elementType, $locale)); + $placeholders['[[' . $elementType->value . ']]'] = mb_strtolower($this->typeNameGenerator->typeLabelPlural($elementType, $locale)); + } + + return $placeholders; + }); + } + + public function __invoke(RequestEvent $event): void + { + //If we already added the parameters, skip adding them again + if (isset($this->translator->getGlobalParameters()['@@partdb_synonyms_registered@@'])) { + return; + } + + //Register all placeholders for synonyms + $placeholders = $this->getSynonymPlaceholders($event->getRequest()->getLocale()); + foreach ($placeholders as $key => $value) { + $this->translator->addGlobalParameter($key, $value); + } + + //Register the marker parameter to avoid double registration + $this->translator->addGlobalParameter('@@partdb_synonyms_registered@@', 'registered'); + } +} diff --git a/src/EventSubscriber/MaintenanceModeSubscriber.php b/src/EventSubscriber/MaintenanceModeSubscriber.php new file mode 100644 index 00000000..0ba5aa99 --- /dev/null +++ b/src/EventSubscriber/MaintenanceModeSubscriber.php @@ -0,0 +1,230 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventSubscriber; + +use App\Services\System\UpdateExecutor; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Twig\Environment; + +/** + * Blocks all web requests when maintenance mode is enabled during updates. + */ +readonly class MaintenanceModeSubscriber implements EventSubscriberInterface +{ + public function __construct(private UpdateExecutor $updateExecutor) + { + + } + + public static function getSubscribedEvents(): array + { + return [ + // High priority to run before other listeners + KernelEvents::REQUEST => ['onKernelRequest', 512], //High priority to run before other listeners + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + // Only handle main requests + if (!$event->isMainRequest()) { + return; + } + + // Skip if not in maintenance mode + if (!$this->updateExecutor->isMaintenanceMode()) { + return; + } + + //Allow to view the progress page and health check endpoint + if (preg_match('#^/[a-z]{2}(?:_[A-Z]{2})?/system/update-manager/(progress|health)#', $event->getRequest()->getPathInfo())) { + return; + } + + // Allow CLI requests + if (PHP_SAPI === 'cli') { + return; + } + + // Get maintenance info + $maintenanceInfo = $this->updateExecutor->getMaintenanceInfo(); + + // Calculate how long the update has been running + $duration = null; + if ($maintenanceInfo && isset($maintenanceInfo['enabled_at'])) { + try { + $startedAt = new \DateTime($maintenanceInfo['enabled_at']); + $now = new \DateTime(); + $duration = $now->getTimestamp() - $startedAt->getTimestamp(); + } catch (\Exception) { + // Ignore date parsing errors + } + } + + $content = $this->getSimpleMaintenanceHtml($maintenanceInfo, $duration); + + $response = new Response($content, Response::HTTP_SERVICE_UNAVAILABLE); + $response->headers->set('Retry-After', '30'); + $response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate'); + + $event->setResponse($response); + } + + /** + * Generate a simple maintenance page HTML without Twig. + */ + private function getSimpleMaintenanceHtml(?array $maintenanceInfo, ?int $duration): string + { + $reason = htmlspecialchars($maintenanceInfo['reason'] ?? 'Update in progress'); + $durationText = $duration !== null ? sprintf('%d seconds', $duration) : 'a moment'; + + $startDateStr = $maintenanceInfo['enabled_at'] ?? 'unknown time'; + + return << + + + + + + Part-DB - Maintenance + + + +
+
+ ⚙️ +
+

Part-DB is under maintenance

+

We're making things better. This should only take a moment.

+ +
+ {$reason} +
+ +
+
+
+ +

+ Maintenance mode active since {$startDateStr}
+
+ Started {$durationText} ago
+ This page will automatically refresh every 15 seconds. +

+
+ + +HTML; + } +} diff --git a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php new file mode 100644 index 00000000..690448a5 --- /dev/null +++ b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php @@ -0,0 +1,99 @@ +ipnSuggestSettings->autoAppendSuffix) { + return; + } + + $em = $args->getObjectManager(); + $uow = $em->getUnitOfWork(); + $meta = $em->getClassMetadata(Part::class); + + // Collect all IPNs already reserved in the current flush (so new entities do not collide with each other) + $reservedIpns = []; + + // Helper to assign a collision-free IPN for a Part entity + $ensureUnique = function (Part $part) use ($em, $uow, $meta, &$reservedIpns) { + $ipn = $part->getIpn(); + if ($ipn === null || $ipn === '') { + return; + } + + // Check against IPNs already reserved in the current flush (except itself) + $originalIpn = $ipn; + $candidate = $originalIpn; + $increment = 1; + + $conflicts = function (string $candidate) use ($em, $part, $reservedIpns) { + // Collision within the current flush session? + if (isset($reservedIpns[$candidate]) && $reservedIpns[$candidate] !== $part) { + return true; + } + // Collision with an existing DB row? + $existing = $em->getRepository(Part::class)->findOneBy(['ipn' => $candidate]); + return $existing !== null && $existing->getId() !== $part->getId(); + }; + + while ($conflicts($candidate)) { + $candidate = $originalIpn . '_' . $increment; + $increment++; + } + + if ($candidate !== $ipn) { + $before = $part->getIpn(); + $part->setIpn($candidate); + + // Recompute the change set so Doctrine writes the change + $uow->recomputeSingleEntityChangeSet($meta, $part); + $reservedIpns[$candidate] = $part; + + // If the old IPN was reserved already, clean it up + if ($before !== null && isset($reservedIpns[$before]) && $reservedIpns[$before] === $part) { + unset($reservedIpns[$before]); + } + } else { + // Candidate unchanged, but reserve it so subsequent entities see it + $reservedIpns[$candidate] = $part; + } + }; + + // 1) Iterate over new entities + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if ($entity instanceof Part) { + $ensureUnique($entity); + } + } + + // 2) Iterate over updates (if IPN changed, ensure uniqueness again) + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if ($entity instanceof Part) { + $ensureUnique($entity); + } + } + } +} diff --git a/src/Exceptions/InfoProviderNotActiveException.php b/src/Exceptions/InfoProviderNotActiveException.php new file mode 100644 index 00000000..02f7cfb7 --- /dev/null +++ b/src/Exceptions/InfoProviderNotActiveException.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Exceptions; + +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; + +/** + * An exception denoting that a required info provider is not active. This can be used to display a user-friendly error message, + * when a user tries to use an info provider that is not active. + */ +class InfoProviderNotActiveException extends \RuntimeException +{ + public function __construct(public readonly string $providerKey, public readonly string $friendlyName) + { + parent::__construct(sprintf('The info provider "%s" (%s) is not active.', $this->friendlyName, $this->providerKey)); + } + + /** + * Creates an instance of this exception from an info provider instance + * @param InfoProviderInterface $provider + * @return self + */ + public static function fromProvider(InfoProviderInterface $provider): self + { + return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???'); + } +} diff --git a/assets/controllers/turbo/locale_menu_controller.js b/src/Exceptions/ProviderIDNotSupportedException.php similarity index 67% rename from assets/controllers/turbo/locale_menu_controller.js rename to src/Exceptions/ProviderIDNotSupportedException.php index d55ff8da..429f43ea 100644 --- a/assets/controllers/turbo/locale_menu_controller.js +++ b/src/Exceptions/ProviderIDNotSupportedException.php @@ -1,7 +1,8 @@ +. */ -import { Controller } from '@hotwired/stimulus'; +declare(strict_types=1); -export default class extends Controller { - connect() { - const menu = document.getElementById('locale-select-menu'); - menu.innerHTML = this.element.innerHTML; + +namespace App\Exceptions; + +class ProviderIDNotSupportedException extends \RuntimeException +{ + public function fromProvider(string $providerKey, string $id): self + { + return new self(sprintf('The given ID %s is not supported by the provider %s.', $id, $providerKey,)); } -} \ No newline at end of file +} diff --git a/src/Exceptions/TwigModeException.php b/src/Exceptions/TwigModeException.php index b76d14d3..ea84667a 100644 --- a/src/Exceptions/TwigModeException.php +++ b/src/Exceptions/TwigModeException.php @@ -42,15 +42,14 @@ declare(strict_types=1); namespace App\Exceptions; use RuntimeException; -use Twig\Error\Error; class TwigModeException extends RuntimeException { private const PROJECT_PATH = __DIR__ . '/../../'; - public function __construct(?Error $previous = null) + public function __construct(?\Throwable $previous = null) { - parent::__construct($previous->getMessage(), 0, $previous); + parent::__construct($previous?->getMessage() ?? "Unknown message", 0, $previous); } /** diff --git a/src/Form/AdminPages/AttachmentTypeAdminForm.php b/src/Form/AdminPages/AttachmentTypeAdminForm.php index d777d4d4..7f9e7646 100644 --- a/src/Form/AdminPages/AttachmentTypeAdminForm.php +++ b/src/Form/AdminPages/AttachmentTypeAdminForm.php @@ -22,17 +22,23 @@ declare(strict_types=1); namespace App\Form\AdminPages; +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\PartAttachment; +use App\Entity\Attachments\ProjectAttachment; +use App\Services\ElementTypeNameGenerator; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; use App\Services\Attachments\FileTypeFilterTools; use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Translation\StaticMessage; class AttachmentTypeAdminForm extends BaseEntityAdminForm { - public function __construct(Security $security, protected FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper) + public function __construct(Security $security, protected FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper, private readonly ElementTypeNameGenerator $elementTypeNameGenerator) { parent::__construct($security, $eventCommentNeededHelper); } @@ -41,6 +47,25 @@ class AttachmentTypeAdminForm extends BaseEntityAdminForm { $is_new = null === $entity->getID(); + + $choiceLabel = function (string $class) { + if (!is_a($class, Attachment::class, true)) { + return $class; + } + return new StaticMessage($this->elementTypeNameGenerator->typeLabelPlural($class::ALLOWED_ELEMENT_CLASS)); + }; + + + $builder->add('allowed_targets', ChoiceType::class, [ + 'required' => false, + 'choices' => array_values(Attachment::ORM_DISCRIMINATOR_MAP), + 'choice_label' => $choiceLabel, + 'preferred_choices' => [PartAttachment::class, ProjectAttachment::class], + 'label' => 'attachment_type.edit.allowed_targets', + 'help' => 'attachment_type.edit.allowed_targets.help', + 'multiple' => true, + ]); + $builder->add('filetype_filter', TextType::class, [ 'required' => false, 'label' => 'attachment_type.edit.filetype_filter', diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index 5a4ef5bc..54cb0406 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -57,6 +57,10 @@ class BaseEntityAdminForm extends AbstractType $resolver->setRequired('attachment_class'); $resolver->setRequired('parameter_class'); $resolver->setAllowedTypes('parameter_class', ['string', 'null']); + + $resolver->setDefaults([ + 'warn_on_unsaved_changes' => true, + ]); } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -71,6 +75,7 @@ class BaseEntityAdminForm extends AbstractType 'label' => 'name.label', 'attr' => [ 'placeholder' => 'part.name.placeholder', + 'autofocus' => $is_new, ], 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); @@ -120,6 +125,7 @@ class BaseEntityAdminForm extends AbstractType 'label' => 'entity.edit.alternative_names.label', 'help' => 'entity.edit.alternative_names.help', 'empty_data' => null, + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), 'attr' => [ 'class' => 'tagsinput', 'data-controller' => 'elements--tagsinput', diff --git a/src/Form/AdminPages/CategoryAdminForm.php b/src/Form/AdminPages/CategoryAdminForm.php index 44c1dede..489649ed 100644 --- a/src/Form/AdminPages/CategoryAdminForm.php +++ b/src/Form/AdminPages/CategoryAdminForm.php @@ -84,6 +84,17 @@ class CategoryAdminForm extends BaseEntityAdminForm 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); + $builder->add('part_ipn_prefix', TextType::class, [ + 'required' => false, + 'empty_data' => '', + 'label' => 'category.edit.part_ipn_prefix', + 'help' => 'category.edit.part_ipn_prefix.help', + 'attr' => [ + 'placeholder' => 'category.edit.part_ipn_prefix.placeholder', + ], + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), + ]); + $builder->add('default_description', RichTextEditorType::class, [ 'required' => false, 'empty_data' => '', diff --git a/src/Form/AdminPages/PartCustomStateAdminForm.php b/src/Form/AdminPages/PartCustomStateAdminForm.php new file mode 100644 index 00000000..b8bb2815 --- /dev/null +++ b/src/Form/AdminPages/PartCustomStateAdminForm.php @@ -0,0 +1,27 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\AdminPages; + +class PartCustomStateAdminForm extends BaseEntityAdminForm +{ +} diff --git a/src/Form/AttachmentFormType.php b/src/Form/AttachmentFormType.php index eb484a58..d9fe2cd2 100644 --- a/src/Form/AttachmentFormType.php +++ b/src/Form/AttachmentFormType.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Form; +use App\Form\Type\AttachmentTypeType; use App\Settings\SystemSettings\AttachmentsSettings; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\Attachment; @@ -67,10 +68,10 @@ class AttachmentFormType extends AbstractType 'required' => false, 'empty_data' => '', ]) - ->add('attachment_type', StructuralEntityType::class, [ + ->add('attachment_type', AttachmentTypeType::class, [ 'label' => 'attachment.edit.attachment_type', - 'class' => AttachmentType::class, 'disable_not_selectable' => true, + 'attachment_filter_class' => $options['data_class'] ?? null, 'allow_add' => $this->security->isGranted('@attachment_types.create'), ]); @@ -121,9 +122,7 @@ class AttachmentFormType extends AbstractType ], 'constraints' => [ //new AllowedFileExtension(), - new File([ - 'maxSize' => $options['max_file_size'], - ]), + new File(maxSize: $options['max_file_size']), ], ]); diff --git a/src/Form/Extension/TogglePasswordTypeExtension.php b/src/Form/Extension/TogglePasswordTypeExtension.php index 8f7320df..7d49eeda 100644 --- a/src/Form/Extension/TogglePasswordTypeExtension.php +++ b/src/Form/Extension/TogglePasswordTypeExtension.php @@ -45,9 +45,9 @@ final class TogglePasswordTypeExtension extends AbstractTypeExtension public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'toggle' => false, - 'hidden_label' => 'Hide', - 'visible_label' => 'Show', + 'toggle' => true, + 'hidden_label' => new TranslatableMessage('password_toggle.hide'), + 'visible_label' => new TranslatableMessage('password_toggle.show'), 'hidden_icon' => 'Default', 'visible_icon' => 'Default', 'button_classes' => ['toggle-password-button'], diff --git a/src/Form/Extension/UnsavedChangesExtension.php b/src/Form/Extension/UnsavedChangesExtension.php new file mode 100644 index 00000000..1187eb19 --- /dev/null +++ b/src/Form/Extension/UnsavedChangesExtension.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\Extension; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Adds a `warn_on_unsaved_changes` option to any root form. When set to true, the Stimulus + * `common--dirty-form` controller attributes are merged into the form element's HTML + * attributes, enabling unsaved-change detection without any template boilerplate. + * + * Usage in a form type: + * + * public function configureOptions(OptionsResolver $resolver): void + * { + * $resolver->setDefaults(['warn_on_unsaved_changes' => true]); + * } + * + * Or per-instance from a controller: + * + * $form = $this->createForm(MyFormType::class, $data, ['warn_on_unsaved_changes' => true]); + */ +class UnsavedChangesExtension extends AbstractTypeExtension +{ + public function __construct(private readonly TranslatorInterface $translator) + { + } + + public static function getExtendedTypes(): iterable + { + return [FormType::class]; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('warn_on_unsaved_changes', false); + $resolver->setAllowedTypes('warn_on_unsaved_changes', 'bool'); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + if (!$options['warn_on_unsaved_changes'] || $view->parent !== null) { + return; + } + + $extraAttr = [ + 'data-controller' => 'common--dirty-form', + 'data-common--dirty-form-confirm-title-value' => $this->translator->trans('form.dirty_form.unsaved_changes.title'), + 'data-common--dirty-form-confirm-message-value' => $this->translator->trans('form.dirty_form.unsaved_changes.message'), + ]; + + // Merge data-action so existing actions on the form element are preserved. + $existingAction = $view->vars['attr']['data-action'] ?? ''; + $dirtyActions = 'submit->common--dirty-form#submit reset->common--dirty-form#resetDirtyState'; + $extraAttr['data-action'] = $existingAction !== '' ? $existingAction . ' ' . $dirtyActions : $dirtyActions; + + $view->vars['attr'] = array_merge($view->vars['attr'], $extraAttr); + } +} diff --git a/src/Form/Filters/LogFilterType.php b/src/Form/Filters/LogFilterType.php index c973ad0f..30abf723 100644 --- a/src/Form/Filters/LogFilterType.php +++ b/src/Form/Filters/LogFilterType.php @@ -130,6 +130,7 @@ class LogFilterType extends AbstractType LogTargetType::PART_ASSOCIATION => 'part_association.label', LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.label', LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB_PART => 'bulk_info_provider_import_job_part.label', + LogTargetType::PART_CUSTOM_STATE => 'part_custom_state.label', }, ]); diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index 0bc349c8..f892fccf 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -32,6 +32,7 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\ProjectSystem\Project; @@ -134,11 +135,20 @@ class PartFilterType extends AbstractType 'min' => 0, ]); + $builder->add('gtin', TextConstraintType::class, [ + 'label' => 'part.gtin', + ]); + $builder->add('measurementUnit', StructuralEntityConstraintType::class, [ 'label' => 'part.edit.partUnit', 'entity_class' => MeasurementUnit::class ]); + $builder->add('partCustomState', StructuralEntityConstraintType::class, [ + 'label' => 'part.edit.partCustomState', + 'entity_class' => PartCustomState::class + ]); + $builder->add('lastModified', DateTimeConstraintType::class, [ 'label' => 'lastModified' ]); diff --git a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php index 13e9581e..7df8985e 100644 --- a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php +++ b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Form\InfoProviderSystem; +use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; @@ -61,7 +62,7 @@ class FieldToProviderMappingType extends AbstractType 'style' => 'width: 80px;' ], 'constraints' => [ - new \Symfony\Component\Validator\Constraints\Range(['min' => 1, 'max' => 10]), + new Range(min: 1, max: 10), ], ]); } diff --git a/src/Form/InfoProviderSystem/FromURLFormType.php b/src/Form/InfoProviderSystem/FromURLFormType.php new file mode 100644 index 00000000..39ef50f4 --- /dev/null +++ b/src/Form/InfoProviderSystem/FromURLFormType.php @@ -0,0 +1,87 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\InfoProviderSystem; + +use App\Services\InfoProviderSystem\ProviderRegistry; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Form\FormBuilderInterface; + +class FromURLFormType extends AbstractType +{ + public function __construct(private readonly ProviderRegistry $providerRegistry) + { + + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('url', UrlType::class, [ + 'label' => 'info_providers.from_url.url.label', + 'required' => true, + ]); + + + $builder->add('method', ChoiceType::class, [ + 'expanded' => true, + 'data' => 'generic_web', //Default value + 'label' => 'info_providers.from_url.method', + 'choices' => [ + 'info_providers.from_url.method.generic_web' => 'generic_web', + 'info_providers.from_url.method.ai_web' => 'ai_web', + ], + 'choice_attr' => function ($choice, $key, $value) { + //Disable all providers that are not active + $provider = $this->providerRegistry->getProviderByKey($value); + if (!$provider->isActive()) { + return ['disabled' => 'disabled']; + } + + return []; + }, + + //Render the choices as inline radio buttons + 'label_attr' => [ + 'class' => 'radio-inline', + ], + ]); + + $builder->add('no_cache', CheckboxType::class, [ + 'label' => 'info_providers.from_url.no_cache', + 'required' => false, + ]); + + $builder->add('skip_delegation', CheckboxType::class, [ + 'label' => 'info_providers.from_url.skip_delegation', + 'required' => false, + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'info_providers.search.submit', + ]); + } +} diff --git a/src/Form/InfoProviderSystem/PartSearchType.php b/src/Form/InfoProviderSystem/PartSearchType.php index 9d582ca4..154c1aa3 100644 --- a/src/Form/InfoProviderSystem/PartSearchType.php +++ b/src/Form/InfoProviderSystem/PartSearchType.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace App\Form\InfoProviderSystem; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\SearchType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; @@ -40,8 +41,17 @@ class PartSearchType extends AbstractType 'help' => 'info_providers.search.providers.help', ]); + $builder->add('no_cache_search', CheckboxType::class, [ + 'label' => 'info_providers.no_cache_search', + 'required' => false, + ]); + $builder->add('no_cache_details', CheckboxType::class, [ + 'label' => 'info_providers.no_cache_details', + 'required' => false, + ]); + $builder->add('submit', SubmitType::class, [ 'label' => 'info_providers.search.submit' ]); } -} \ No newline at end of file +} diff --git a/src/Form/InfoProviderSystem/ProviderSelectType.php b/src/Form/InfoProviderSystem/ProviderSelectType.php index 95e10791..bad3edaa 100644 --- a/src/Form/InfoProviderSystem/ProviderSelectType.php +++ b/src/Form/InfoProviderSystem/ProviderSelectType.php @@ -30,6 +30,7 @@ use Symfony\Component\Form\ChoiceList\ChoiceList; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Translation\StaticMessage; class ProviderSelectType extends AbstractType { @@ -70,10 +71,10 @@ class ProviderSelectType extends AbstractType //The choice_label and choice_value only needs to be set if we want the objects $resolver->setDefault('choice_label', function (Options $options){ if ('object' === $options['input']) { - return ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => $choice?->getProviderInfo()['name']); + return ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => new StaticMessage($choice?->getProviderInfo()['name'])); } - return null; + return static fn ($choice, $key, $value) => new StaticMessage($key); }); $resolver->setDefault('choice_value', function (Options $options) { if ('object' === $options['input']) { diff --git a/src/Form/LabelSystem/ScanDialogType.php b/src/Form/LabelSystem/ScanDialogType.php index 13ff8e6f..5f9ce65f 100644 --- a/src/Form/LabelSystem/ScanDialogType.php +++ b/src/Form/LabelSystem/ScanDialogType.php @@ -61,6 +61,8 @@ class ScanDialogType extends AbstractType 'attr' => [ 'autofocus' => true, 'id' => 'scan_dialog_input', + 'style' => 'font-family: var(--bs-font-monospace)', + 'data-controller' => 'elements--nonprintable-char-input', ], ]); @@ -72,10 +74,7 @@ class ScanDialogType extends AbstractType 'placeholder' => 'scan_dialog.mode.auto', 'choice_label' => fn (?BarcodeSourceType $enum) => match($enum) { null => 'scan_dialog.mode.auto', - BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal', - BarcodeSourceType::IPN => 'scan_dialog.mode.ipn', - BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user', - BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp' + default => 'scan_dialog.mode.' . $enum->value, }, ]); diff --git a/src/Form/ParameterType.php b/src/Form/ParameterType.php index 4c2174ae..f68c3921 100644 --- a/src/Form/ParameterType.php +++ b/src/Form/ParameterType.php @@ -54,7 +54,9 @@ use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; use App\Entity\Parts\MeasurementUnit; use App\Form\Type\ExponentialNumberType; +use App\Form\Type\TriStateCheckboxType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -147,6 +149,14 @@ class ParameterType extends AbstractType 'class' => 'form-control-sm', ], ]); + + // Only show the EDA visibility field for part parameters, as it has no function for other entities + if ($options['data_class'] === PartParameter::class) { + $builder->add('eda_visibility', TriStateCheckboxType::class, [ + 'label' => false, + 'required' => false, + ]); + } } public function finishView(FormView $view, FormInterface $form, array $options): void diff --git a/src/Form/Part/EDA/BatchEdaType.php b/src/Form/Part/EDA/BatchEdaType.php new file mode 100644 index 00000000..28fc4a41 --- /dev/null +++ b/src/Form/Part/EDA/BatchEdaType.php @@ -0,0 +1,116 @@ +add('reference_prefix', TextType::class, [ + 'label' => 'eda_info.reference_prefix', + 'required' => false, + 'attr' => ['placeholder' => t('eda_info.reference_prefix.placeholder')], + ]) + ->add('apply_reference_prefix', CheckboxType::class, [ + 'label' => 'batch_eda.apply', + 'required' => false, + 'mapped' => false, + ]) + ->add('value', TextType::class, [ + 'label' => 'eda_info.value', + 'required' => false, + 'attr' => ['placeholder' => t('eda_info.value.placeholder')], + ]) + ->add('apply_value', CheckboxType::class, [ + 'label' => 'batch_eda.apply', + 'required' => false, + 'mapped' => false, + ]) + ->add('kicad_symbol', KicadFieldAutocompleteType::class, [ + 'label' => 'eda_info.kicad_symbol', + 'type' => KicadFieldAutocompleteType::TYPE_SYMBOL, + 'required' => false, + 'attr' => ['placeholder' => t('eda_info.kicad_symbol.placeholder')], + ]) + ->add('apply_kicad_symbol', CheckboxType::class, [ + 'label' => 'batch_eda.apply', + 'required' => false, + 'mapped' => false, + ]) + ->add('kicad_footprint', KicadFieldAutocompleteType::class, [ + 'label' => 'eda_info.kicad_footprint', + 'type' => KicadFieldAutocompleteType::TYPE_FOOTPRINT, + 'required' => false, + 'attr' => ['placeholder' => t('eda_info.kicad_footprint.placeholder')], + ]) + ->add('apply_kicad_footprint', CheckboxType::class, [ + 'label' => 'batch_eda.apply', + 'required' => false, + 'mapped' => false, + ]) + ->add('visibility', TriStateCheckboxType::class, [ + 'label' => 'eda_info.visibility', + 'required' => false, + ]) + ->add('apply_visibility', CheckboxType::class, [ + 'label' => 'batch_eda.apply', + 'required' => false, + 'mapped' => false, + ]) + ->add('exclude_from_bom', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_bom', + 'required' => false, + ]) + ->add('apply_exclude_from_bom', CheckboxType::class, [ + 'label' => 'batch_eda.apply', + 'required' => false, + 'mapped' => false, + ]) + ->add('exclude_from_board', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_board', + 'required' => false, + ]) + ->add('apply_exclude_from_board', CheckboxType::class, [ + 'label' => 'batch_eda.apply', + 'required' => false, + 'mapped' => false, + ]) + ->add('exclude_from_sim', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_sim', + 'required' => false, + ]) + ->add('apply_exclude_from_sim', CheckboxType::class, [ + 'label' => 'batch_eda.apply', + 'required' => false, + 'mapped' => false, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'batch_eda.submit', + 'attr' => ['class' => 'btn btn-primary'], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => null, + ]); + } +} diff --git a/src/Form/Part/EDA/KicadFieldAutocompleteType.php b/src/Form/Part/EDA/KicadFieldAutocompleteType.php index 50de81d0..8a7b0313 100644 --- a/src/Form/Part/EDA/KicadFieldAutocompleteType.php +++ b/src/Form/Part/EDA/KicadFieldAutocompleteType.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace App\Form\Part\EDA; use App\Form\Type\StaticFileAutocompleteType; +use App\Settings\MiscSettings\KiCadEDASettings; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -39,6 +40,13 @@ class KicadFieldAutocompleteType extends AbstractType //Do not use a leading slash here! otherwise it will not work under prefixed reverse proxies public const FOOTPRINT_PATH = 'kicad/footprints.txt'; public const SYMBOL_PATH = 'kicad/symbols.txt'; + public const CUSTOM_FOOTPRINT_PATH = 'kicad/footprints_custom.txt'; + public const CUSTOM_SYMBOL_PATH = 'kicad/symbols_custom.txt'; + + public function __construct( + private readonly KiCadEDASettings $kiCadEDASettings, + ) { + } public function configureOptions(OptionsResolver $resolver): void { @@ -47,8 +55,8 @@ class KicadFieldAutocompleteType extends AbstractType $resolver->setDefaults([ 'file' => fn(Options $options) => match ($options['type']) { - self::TYPE_FOOTPRINT => self::FOOTPRINT_PATH, - self::TYPE_SYMBOL => self::SYMBOL_PATH, + self::TYPE_FOOTPRINT => $this->kiCadEDASettings->useCustomList ? self::CUSTOM_FOOTPRINT_PATH : self::FOOTPRINT_PATH, + self::TYPE_SYMBOL => $this->kiCadEDASettings->useCustomList ? self::CUSTOM_SYMBOL_PATH : self::SYMBOL_PATH, default => throw new \InvalidArgumentException('Invalid type'), } ]); @@ -58,4 +66,4 @@ class KicadFieldAutocompleteType extends AbstractType { return StaticFileAutocompleteType::class; } -} \ No newline at end of file +} diff --git a/src/Form/Part/OrderdetailType.php b/src/Form/Part/OrderdetailType.php index 53240821..6a0dd940 100644 --- a/src/Form/Part/OrderdetailType.php +++ b/src/Form/Part/OrderdetailType.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Form\Part; +use App\Form\Type\TriStateCheckboxType; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Supplier; @@ -73,6 +74,16 @@ class OrderdetailType extends AbstractType 'label' => 'orderdetails.edit.obsolete', ]); + $builder->add('pricesIncludesVAT', TriStateCheckboxType::class, [ + 'required' => false, + 'label' => 'orderdetails.edit.prices_includes_vat', + ]); + + $builder->add('eda_visibility', TriStateCheckboxType::class, [ + 'required' => false, + 'label' => 'orderdetails.edit.eda_visibility', + ]); + //Add pricedetails after we know the data, so we can set the default currency $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options): void { /** @var Orderdetail $orderdetail */ diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index a54a0937..01078ada 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -30,6 +30,7 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartCustomState; use App\Entity\PriceInformations\Orderdetail; use App\Form\AttachmentFormType; use App\Form\ParameterType; @@ -41,6 +42,8 @@ use App\Form\Type\StructuralEntityType; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\LogSystem\EventCommentNeededHelper; use App\Services\LogSystem\EventCommentType; +use App\Settings\MiscSettings\IpnSuggestSettings; +use App\Settings\SystemSettings\LocalizationSettings; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -57,8 +60,13 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class PartBaseType extends AbstractType { - public function __construct(protected Security $security, protected UrlGeneratorInterface $urlGenerator, protected EventCommentNeededHelper $event_comment_needed_helper) - { + public function __construct( + protected Security $security, + protected UrlGeneratorInterface $urlGenerator, + protected EventCommentNeededHelper $event_comment_needed_helper, + protected IpnSuggestSettings $ipnSuggestSettings, + private readonly LocalizationSettings $localizationSettings, + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -70,6 +78,39 @@ class PartBaseType extends AbstractType /** @var PartDetailDTO|null $dto */ $dto = $options['info_provider_dto']; + $descriptionAttr = [ + 'placeholder' => 'part.edit.description.placeholder', + 'rows' => 2, + ]; + + if ($this->ipnSuggestSettings->useDuplicateDescription) { + // Only add attribute when duplicate description feature is enabled + $descriptionAttr['data-ipn-suggestion'] = 'descriptionField'; + } + + $ipnAttr = [ + 'class' => 'ipn-suggestion-field', + 'data-elements--ipn-suggestion-target' => 'input', + 'autocomplete' => 'off', + ]; + + if ($this->ipnSuggestSettings->regex !== null && $this->ipnSuggestSettings->regex !== '') { + $ipnAttr['pattern'] = $this->ipnSuggestSettings->regex; + $ipnAttr['placeholder'] = $this->ipnSuggestSettings->regex; + $ipnAttr['title'] = $this->ipnSuggestSettings->regexHelp; + } + + $ipnOptions = [ + 'required' => false, + 'empty_data' => null, + 'label' => 'part.edit.ipn', + 'attr' => $ipnAttr, + ]; + + if (isset($ipnAttr['pattern']) && $this->ipnSuggestSettings->regexHelp !== null && $this->ipnSuggestSettings->regexHelp !== '') { + $ipnOptions['help'] = $this->ipnSuggestSettings->regexHelp; + } + //Common section $builder ->add('name', TextType::class, [ @@ -77,6 +118,7 @@ class PartBaseType extends AbstractType 'label' => 'part.edit.name', 'attr' => [ 'placeholder' => 'part.edit.name.placeholder', + 'autofocus' => $new_part, ], ]) ->add('description', RichTextEditorType::class, [ @@ -84,10 +126,7 @@ class PartBaseType extends AbstractType 'empty_data' => '', 'label' => 'part.edit.description', 'mode' => 'markdown-single_line', - 'attr' => [ - 'placeholder' => 'part.edit.description.placeholder', - 'rows' => 2, - ], + 'attr' => $descriptionAttr, ]) ->add('minAmount', SIUnitType::class, [ 'attr' => [ @@ -120,6 +159,9 @@ class PartBaseType extends AbstractType 'disable_not_selectable' => true, //Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity) 'required' => !$new_part, + 'attr' => [ + 'data-ipn-suggestion' => 'categoryField', + ] ]) ->add('footprint', StructuralEntityType::class, [ 'class' => Footprint::class, @@ -187,11 +229,19 @@ class PartBaseType extends AbstractType 'disable_not_selectable' => true, 'label' => 'part.edit.partUnit', ]) - ->add('ipn', TextType::class, [ + ->add('partCustomState', StructuralEntityType::class, [ + 'class' => PartCustomState::class, + 'required' => false, + 'disable_not_selectable' => true, + 'label' => 'part.edit.partCustomState', + ]) + ->add('ipn', TextType::class, $ipnOptions) + ->add('gtin', TextType::class, [ 'required' => false, 'empty_data' => null, - 'label' => 'part.edit.ipn', - ]); + 'label' => 'part.gtin', + ]) + ; //Comment section $builder->add('comment', RichTextEditorType::class, [ @@ -236,6 +286,9 @@ class PartBaseType extends AbstractType 'entity' => $part, ]); + $orderdetailPrototype = new Orderdetail(); + $orderdetailPrototype->setPricesIncludesVAT($this->localizationSettings->pricesIncludeTaxByDefault); + //Orderdetails section $builder->add('orderdetails', CollectionType::class, [ 'entry_type' => OrderdetailType::class, @@ -244,7 +297,7 @@ class PartBaseType extends AbstractType 'allow_delete' => true, 'label' => false, 'by_reference' => false, - 'prototype_data' => new Orderdetail(), + 'prototype_data' => $orderdetailPrototype, 'entry_options' => [ 'measurement_unit' => $part->getPartUnit(), ], @@ -316,6 +369,7 @@ class PartBaseType extends AbstractType $resolver->setDefaults([ 'data_class' => Part::class, 'info_provider_dto' => null, + 'warn_on_unsaved_changes' => true, ]); $resolver->setAllowedTypes('info_provider_dto', [PartDetailDTO::class, 'null']); diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php index 7d545340..fc330bb1 100644 --- a/src/Form/Part/PartLotType.php +++ b/src/Form/Part/PartLotType.php @@ -31,6 +31,7 @@ use App\Form\Type\StructuralEntityType; use App\Form\Type\UserSelectType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -110,6 +111,13 @@ class PartLotType extends AbstractType //Do not remove whitespace chars on the beginning and end of the string 'trim' => false, ]); + + $builder->add('last_stocktake_at', DateTimeType::class, [ + 'label' => 'part_lot.edit.last_stocktake_at', + 'widget' => 'single_text', + 'disabled' => !$this->security->isGranted('@parts_stock.stocktake'), + 'required' => false, + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/Security/LoginFormType.php b/src/Form/Security/LoginFormType.php new file mode 100644 index 00000000..184f68ac --- /dev/null +++ b/src/Form/Security/LoginFormType.php @@ -0,0 +1,83 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Security; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +use function Symfony\Component\Translation\t; + +class LoginFormType extends AbstractType +{ + public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, array $options): void + { + $builder + ->add('_username', TextType::class, [ + 'label' => t('login.username.label'), + 'attr' => [ + 'autofocus' => 'autofocus', + 'autocomplete' => 'username', + 'placeholder' => t('login.username.placeholder'), + ] + ]) + ->add('_password', PasswordType::class, [ + 'label' => t('login.password.label'), + 'attr' => [ + 'autocomplete' => 'current-password', + 'placeholder' => t('login.password.placeholder'), + ] + ]) + ->add('_remember_me', CheckboxType::class, [ + 'label' => t('login.rememberme'), + 'required' => false, + ]) + ->add('submit', \Symfony\Component\Form\Extension\Core\Type\SubmitType::class, [ + 'label' => t('login.btn'), + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + // This ensures CSRF protection is active for the login + 'csrf_protection' => true, + 'csrf_field_name' => '_csrf_token', + 'csrf_token_id' => 'authenticate', + 'attr' => [ + 'data-turbo' => 'false', // Disable Turbo for the login form to ensure proper redirection after login + ] + ]); + } + + public function getBlockPrefix(): string + { + // This removes the "login_form_" prefix from field names + // so that Security can find "_username" directly. + return ''; + } +} diff --git a/src/Form/Settings/AiModelsType.php b/src/Form/Settings/AiModelsType.php new file mode 100644 index 00000000..5228bb47 --- /dev/null +++ b/src/Form/Settings/AiModelsType.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Settings; + +use Symfony\AI\Platform\Capability; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * An text input with autocomplete for AI models from the given platform. + * The platform is determined by the value of another form field, which is specified by the "platform_selector" option. This allows to filter the available models based on the selected platform. + */ +final class AiModelsType extends AbstractType +{ + public function __construct(private readonly UrlGeneratorInterface $urlGenerator) + { + } + + public function getParent(): string + { + return TextType::class; + } + + public function configureOptions(OptionsResolver $resolver): void + { + //The target label of the platform select, which is used to filter the models for the selected platform. + $resolver->setRequired('platform_selector'); + $resolver->setAllowedTypes('platform_selector', 'string'); + + //Only show models, that have the given capability. This is used to only show models that support structured output for the AI extractor settings. + $resolver->setDefault('filter_capability', null); + $resolver->setAllowedTypes('filter_capability', ['null', Capability::class]); + } + + public function finishView(FormView $view, FormInterface $form, array $options): void + { + $urlOptions = ['platform' => '__PLATFORM__']; + if ($options['filter_capability'] !== null) { + $urlOptions['capability'] = $options['filter_capability']->value; + } + + $view->vars['attr']['data-url-template'] = $this->urlGenerator->generate('typeahead_ai_models', $urlOptions); + $view->vars['attr']['data-controller'] = 'elements--ai-model-autocomplete'; + + $view->vars['attr']['data-platform-selector'] = $options['platform_selector']; + } +} diff --git a/src/Form/Settings/AiPlatformChoiceType.php b/src/Form/Settings/AiPlatformChoiceType.php new file mode 100644 index 00000000..82ea66b2 --- /dev/null +++ b/src/Form/Settings/AiPlatformChoiceType.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Settings; + +use App\Services\AI\AIPlatformRegistry; +use App\Services\AI\AIPlatforms; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Allow to choose an AI platform from the enabled platforms in the system. This is used in the settings to choose the default platform for AI features. + */ +final class AiPlatformChoiceType extends AbstractType +{ + public function __construct(private readonly AIPlatformRegistry $platformRegistry) + { + } + + public function getParent(): string + { + return EnumType::class; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $choices = array_map(static fn(string $val) => AIPlatforms::from($val), array_keys($this->platformRegistry->getEnabledPlatforms())); + + $resolver->setDefaults([ + 'class' => AIPlatforms::class, + 'choices' => $choices, + 'required' => false, + 'platform_selector_label' => null + ]); + } + + public function finishView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['attr']['data-platform-selector-label'] = $options['platform_selector_label'] ?? $view->vars['id'].'_label'; + } +} diff --git a/src/Form/Settings/KicadListEditorType.php b/src/Form/Settings/KicadListEditorType.php new file mode 100644 index 00000000..cefdbdbc --- /dev/null +++ b/src/Form/Settings/KicadListEditorType.php @@ -0,0 +1,103 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\Settings; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Form type for editing the custom KiCad footprints and symbols lists. + */ +final class KicadListEditorType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('useCustomList', CheckboxType::class, [ + 'label' => 'settings.misc.kicad_eda.use_custom_list', + 'help' => 'settings.misc.kicad_eda.use_custom_list.help', + 'required' => false, + ]) + ->add('customFootprints', TextareaType::class, [ + 'label' => 'settings.misc.kicad_eda.editor.custom_footprints', + 'help' => 'settings.misc.kicad_eda.editor.footprints.help', + 'attr' => [ + 'rows' => 16, + 'spellcheck' => 'false', + 'class' => 'font-monospace', + ], + ]) + ->add('defaultFootprints', TextareaType::class, [ + 'label' => 'settings.misc.kicad_eda.editor.default_footprints', + 'help' => 'settings.misc.kicad_eda.editor.default_files_help', + 'disabled' => true, + 'mapped' => false, + 'data' => $options['default_footprints'], + 'attr' => [ + 'rows' => 16, + 'spellcheck' => 'false', + 'class' => 'font-monospace', + 'readonly' => 'readonly', + ], + ]) + ->add('customSymbols', TextareaType::class, [ + 'label' => 'settings.misc.kicad_eda.editor.custom_symbols', + 'help' => 'settings.misc.kicad_eda.editor.symbols.help', + 'attr' => [ + 'rows' => 16, + 'spellcheck' => 'false', + 'class' => 'font-monospace', + ], + ]) + ->add('defaultSymbols', TextareaType::class, [ + 'label' => 'settings.misc.kicad_eda.editor.default_symbols', + 'help' => 'settings.misc.kicad_eda.editor.default_files_help', + 'disabled' => true, + 'mapped' => false, + 'data' => $options['default_symbols'], + 'attr' => [ + 'rows' => 16, + 'spellcheck' => 'false', + 'class' => 'font-monospace', + 'readonly' => 'readonly', + ], + ]) + ->add('save', SubmitType::class, [ + 'label' => 'save', + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'default_footprints' => '', + 'default_symbols' => '', + ]); + $resolver->setAllowedTypes('default_footprints', 'string'); + $resolver->setAllowedTypes('default_symbols', 'string'); + } +} diff --git a/src/Form/Type/LanguageMenuEntriesType.php b/src/Form/Settings/LanguageMenuEntriesType.php similarity index 95% rename from src/Form/Type/LanguageMenuEntriesType.php rename to src/Form/Settings/LanguageMenuEntriesType.php index a3bba77f..9bc2e850 100644 --- a/src/Form/Type/LanguageMenuEntriesType.php +++ b/src/Form/Settings/LanguageMenuEntriesType.php @@ -21,12 +21,11 @@ declare(strict_types=1); -namespace App\Form\Type; +namespace App\Form\Settings; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\LanguageType; -use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\Intl\Languages; use Symfony\Component\OptionsResolver\OptionsResolver; diff --git a/src/Form/Settings/TypeSynonymRowType.php b/src/Form/Settings/TypeSynonymRowType.php new file mode 100644 index 00000000..ffff14fa --- /dev/null +++ b/src/Form/Settings/TypeSynonymRowType.php @@ -0,0 +1,150 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\Settings; + +use App\Services\ElementTypes; +use App\Settings\SystemSettings\LocalizationSettings; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\Extension\Core\Type\LocaleType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Intl\Locales; +use Symfony\Component\Translation\StaticMessage; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * A single translation row: data source + language + translations (singular/plural). + */ +class TypeSynonymRowType extends AbstractType +{ + + private const PREFERRED_TYPES = [ + ElementTypes::CATEGORY, + ElementTypes::STORAGE_LOCATION, + ElementTypes::FOOTPRINT, + ElementTypes::MANUFACTURER, + ElementTypes::SUPPLIER, + ElementTypes::PROJECT, + ]; + + public function __construct( + private readonly LocalizationSettings $localizationSettings, + private readonly TranslatorInterface $translator, + #[Autowire(param: 'partdb.locale_menu')] private readonly array $preferredLanguagesParam, + ) { + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('dataSource', EnumType::class, [ + 'class' => ElementTypes::class, + 'label' => false, + 'required' => true, + 'constraints' => [ + new Assert\NotBlank(), + ], + 'choice_label' => function (ElementTypes $choice) { + return new StaticMessage( + $this->translator->trans($choice->getDefaultLabelKey()) . ' (' . $this->translator->trans($choice->getDefaultPluralLabelKey()) . ')' + ); + }, + 'row_attr' => ['class' => 'mb-0'], + 'attr' => ['class' => 'form-select-sm'], + 'preferred_choices' => self::PREFERRED_TYPES + ]) + ->add('locale', LocaleType::class, [ + 'label' => false, + 'required' => true, + // Restrict to languages configured in the language menu: disable ChoiceLoader and provide explicit choices + 'choice_loader' => null, + 'choices' => $this->buildLocaleChoices(true), + 'preferred_choices' => $this->getPreferredLocales(), + 'constraints' => [ + new Assert\NotBlank(), + ], + 'row_attr' => ['class' => 'mb-0'], + 'attr' => ['class' => 'form-select-sm'] + ]) + ->add('translation_singular', TextType::class, [ + 'label' => false, + 'required' => true, + 'empty_data' => '', + 'constraints' => [ + new Assert\NotBlank(), + ], + 'row_attr' => ['class' => 'mb-0'], + 'attr' => ['class' => 'form-select-sm'] + ]) + ->add('translation_plural', TextType::class, [ + 'label' => false, + 'required' => true, + 'empty_data' => '', + 'constraints' => [ + new Assert\NotBlank(), + ], + 'row_attr' => ['class' => 'mb-0'], + 'attr' => ['class' => 'form-select-sm'] + ]); + } + + + /** + * Returns only locales configured in the language menu (settings) or falls back to the parameter. + * Format: ['German (DE)' => 'de', ...] + */ + private function buildLocaleChoices(bool $returnPossible = false): array + { + $locales = $this->getPreferredLocales(); + + if ($returnPossible) { + $locales = $this->getPossibleLocales(); + } + + $choices = []; + foreach ($locales as $code) { + $label = Locales::getName($code); + $choices[$label . ' (' . strtoupper($code) . ')'] = $code; + } + return $choices; + } + + /** + * Source of allowed locales: + * 1) LocalizationSettings->languageMenuEntries (if set) + * 2) Fallback: parameter partdb.locale_menu + */ + private function getPreferredLocales(): array + { + $fromSettings = $this->localizationSettings->languageMenuEntries; + return !empty($fromSettings) ? array_values($fromSettings) : array_values($this->preferredLanguagesParam); + } + + private function getPossibleLocales(): array + { + return array_values($this->preferredLanguagesParam); + } +} diff --git a/src/Form/Settings/TypeSynonymsCollectionType.php b/src/Form/Settings/TypeSynonymsCollectionType.php new file mode 100644 index 00000000..4756930a --- /dev/null +++ b/src/Form/Settings/TypeSynonymsCollectionType.php @@ -0,0 +1,223 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\Settings; + +use App\Services\ElementTypes; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Intl\Locales; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Flat collection of translation rows. + * View data: list [{dataSource, locale, translation_singular, translation_plural}, ...] + * Model data: same structure (list). Optionally expands a nested map to a list. + */ +class TypeSynonymsCollectionType extends AbstractType +{ + public function __construct(private readonly TranslatorInterface $translator) + { + } + + private function flattenStructure(array $modelValue): array + { + //If the model is already flattened, return as is + if (array_is_list($modelValue)) { + return $modelValue; + } + + $out = []; + foreach ($modelValue as $dataSource => $locales) { + if (!is_array($locales)) { + continue; + } + foreach ($locales as $locale => $translations) { + if (!is_array($translations)) { + continue; + } + $out[] = [ + //Convert string to enum value + 'dataSource' => ElementTypes::from($dataSource), + 'locale' => $locale, + 'translation_singular' => $translations['singular'] ?? '', + 'translation_plural' => $translations['plural'] ?? '', + ]; + } + } + return $out; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { + //Flatten the structure + $data = $event->getData(); + $event->setData($this->flattenStructure($data)); + }); + + $builder->addModelTransformer(new CallbackTransformer( + // Model -> View + $this->flattenStructure(...), + // View -> Model (keep list; let existing behavior unchanged) + function (array $viewValue) { + //Turn our flat list back into the structured array + + $out = []; + + foreach ($viewValue as $row) { + if (!is_array($row)) { + continue; + } + $dataSource = $row['dataSource'] ?? null; + $locale = $row['locale'] ?? null; + $translation_singular = $row['translation_singular'] ?? null; + $translation_plural = $row['translation_plural'] ?? null; + + if ($dataSource === null || + !is_string($locale) || $locale === '' + ) { + continue; + } + + $out[$dataSource->value][$locale] = [ + 'singular' => is_string($translation_singular) ? $translation_singular : '', + 'plural' => is_string($translation_plural) ? $translation_plural : '', + ]; + } + + return $out; + } + )); + + // Validation and normalization (duplicates + sorting) during SUBMIT + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void { + $form = $event->getForm(); + $rows = $event->getData(); + + if (!is_array($rows)) { + return; + } + + // Duplicate check: (dataSource, locale) must be unique + $seen = []; + $hasDuplicate = false; + + foreach ($rows as $idx => $row) { + if (!is_array($row)) { + continue; + } + $ds = $row['dataSource'] ?? null; + $loc = $row['locale'] ?? null; + + if ($ds !== null && is_string($loc) && $loc !== '') { + $key = $ds->value . '|' . $loc; + if (isset($seen[$key])) { + $hasDuplicate = true; + + if ($form->has((string)$idx)) { + $child = $form->get((string)$idx); + + if ($child->has('dataSource')) { + $child->get('dataSource')->addError( + new FormError($this->translator->trans( + 'settings.synonyms.type_synonyms.collection_type.duplicate', + [], 'validators' + )) + ); + } + if ($child->has('locale')) { + $child->get('locale')->addError( + new FormError($this->translator->trans( + 'settings.synonyms.type_synonyms.collection_type.duplicate', + [], 'validators' + )) + ); + } + } + } else { + $seen[$key] = true; + } + } + } + + if ($hasDuplicate) { + return; + } + + // Overall sort: first by dataSource key, then by localized language name + $sortable = $rows; + + usort($sortable, static function ($a, $b) { + $aDs = $a['dataSource']->value ?? ''; + $bDs = $b['dataSource']->value ?? ''; + + $cmpDs = strcasecmp($aDs, $bDs); + if ($cmpDs !== 0) { + return $cmpDs; + } + + $aLoc = (string)($a['locale'] ?? ''); + $bLoc = (string)($b['locale'] ?? ''); + + $aName = Locales::getName($aLoc); + $bName = Locales::getName($bLoc); + + return strcasecmp($aName, $bName); + }); + + $event->setData($sortable); + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + + // Defaults for the collection and entry type + $resolver->setDefaults([ + 'entry_type' => TypeSynonymRowType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'required' => false, + 'prototype' => true, + 'empty_data' => [], + 'entry_options' => ['label' => false], + ]); + } + + public function getParent(): ?string + { + return CollectionType::class; + } + + public function getBlockPrefix(): string + { + return 'type_synonyms_collection'; + } +} diff --git a/src/Form/Type/AttachmentTypeType.php b/src/Form/Type/AttachmentTypeType.php new file mode 100644 index 00000000..099ed282 --- /dev/null +++ b/src/Form/Type/AttachmentTypeType.php @@ -0,0 +1,56 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Type; + +use App\Entity\Attachments\AttachmentType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Form type to select the AttachmentType to use in an attachment form. This is used to filter the available attachment types based on the target class. + */ +class AttachmentTypeType extends AbstractType +{ + public function getParent(): ?string + { + return StructuralEntityType::class; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null); + + $resolver->setDefault('class', AttachmentType::class); + + $resolver->setDefault('choice_filter', function (Options $options) { + if (is_a($options['class'], AttachmentType::class, true) && $options['attachment_filter_class'] !== null) { + return static function (?AttachmentType $choice) use ($options) { + return $choice?->isAllowedForTarget($options['attachment_filter_class']); + }; + } + return null; + }); + } +} diff --git a/src/Form/Type/BigDecimalMoneyType.php b/src/Form/Type/BigDecimalMoneyType.php index 189416ff..1950391c 100644 --- a/src/Form/Type/BigDecimalMoneyType.php +++ b/src/Form/Type/BigDecimalMoneyType.php @@ -59,6 +59,16 @@ class BigDecimalMoneyType extends AbstractType implements DataTransformerInterfa return null; } - return BigDecimal::of($value); + if ($value instanceof BigDecimal) { + return $value; + } + if (is_float($value)) { + return BigDecimal::fromFloatShortest($value); + } + if (is_string($value)) { + return BigDecimal::of($value); + } + + throw new \InvalidArgumentException(sprintf('Expected a string, float or BigDecimal, got %s', get_debug_type($value))); } } diff --git a/src/Form/Type/BigDecimalNumberType.php b/src/Form/Type/BigDecimalNumberType.php index c225f0a4..04e8e655 100644 --- a/src/Form/Type/BigDecimalNumberType.php +++ b/src/Form/Type/BigDecimalNumberType.php @@ -59,6 +59,17 @@ class BigDecimalNumberType extends AbstractType implements DataTransformerInterf return null; } - return BigDecimal::of($value); + if ($value instanceof BigDecimal) { + return $value; + } + if (is_float($value)) { + return BigDecimal::fromFloatShortest($value); + } + if (is_string($value)) { + return BigDecimal::of($value); + } + + throw new \InvalidArgumentException(sprintf('Expected a string, float or BigDecimal, got %s', get_debug_type($value))); } + } diff --git a/src/Form/Type/LocaleSelectType.php b/src/Form/Type/LocaleSelectType.php index d47fb57f..6dc6f9fc 100644 --- a/src/Form/Type/LocaleSelectType.php +++ b/src/Form/Type/LocaleSelectType.php @@ -30,7 +30,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver; /** * A locale select field that uses the preferred languages from the configuration. - */ class LocaleSelectType extends AbstractType { diff --git a/src/Form/Type/StructuralEntityType.php b/src/Form/Type/StructuralEntityType.php index 1018eeeb..51eb21a1 100644 --- a/src/Form/Type/StructuralEntityType.php +++ b/src/Form/Type/StructuralEntityType.php @@ -110,8 +110,10 @@ class StructuralEntityType extends AbstractType //If no help text is explicitly set, we use the dto value as help text and show it as html $resolver->setDefault('help', fn(Options $options) => $this->dtoText($options['dto_value'])); $resolver->setDefault('help_html', fn(Options $options) => $options['dto_value'] !== null); + - $resolver->setDefault('attr', function (Options $options) { + //Normalize the attr to merge custom attributes + $resolver->setNormalizer('attr', function (Options $options, $value) { $tmp = [ 'data-controller' => $options['controller'], 'data-allow-add' => $options['allow_add'] ? 'true' : 'false', @@ -121,7 +123,7 @@ class StructuralEntityType extends AbstractType $tmp['data-empty-message'] = $options['empty_message']; } - return $tmp; + return array_merge($tmp, $value); }); } diff --git a/src/Form/UserAdminForm.php b/src/Form/UserAdminForm.php index 69be181f..6331dfb7 100644 --- a/src/Form/UserAdminForm.php +++ b/src/Form/UserAdminForm.php @@ -59,6 +59,10 @@ class UserAdminForm extends AbstractType $resolver->setDefault('parameter_class', false); $resolver->setDefault('validation_groups', ['Default', 'permissions:edit']); + + $resolver->setDefaults([ + 'warn_on_unsaved_changes' => true, + ]); } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -177,10 +181,7 @@ class UserAdminForm extends AbstractType 'required' => false, 'mapped' => false, 'disabled' => !$this->security->isGranted('set_password', $entity) || $entity->isSamlUser(), - 'constraints' => [new Length([ - 'min' => 6, - 'max' => 128, - ])], + 'constraints' => [new Length(min: 6, max: 128)], ]) ->add('need_pw_change', CheckboxType::class, [ diff --git a/src/Form/UserSettingsType.php b/src/Form/UserSettingsType.php index 0c7cb169..968d8063 100644 --- a/src/Form/UserSettingsType.php +++ b/src/Form/UserSettingsType.php @@ -92,9 +92,7 @@ class UserSettingsType extends AbstractType 'accept' => 'image/*', ], 'constraints' => [ - new File([ - 'maxSize' => '5M', - ]), + new File(maxSize: '5M'), ], ]) ->add('aboutMe', RichTextEditorType::class, [ diff --git a/src/Helpers/RandomizeUseragentHttpClient.php b/src/Helpers/RandomizeUseragentHttpClient.php new file mode 100644 index 00000000..42e62d11 --- /dev/null +++ b/src/Helpers/RandomizeUseragentHttpClient.php @@ -0,0 +1,177 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Helpers; + +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; + +/** + * HttpClient wrapper that randomizes the user agent for each request, to make it harder for servers to detect and block us. + * It also sets some other headers to make the requests look more like real browser requests. + * When we get a 503, 403 or 429, we assume that the server is blocking us and try again with a different user agent, until we run out of retries. + */ +final class RandomizeUseragentHttpClient implements HttpClientInterface +{ + private const PROFILES = [ + // --- CHROME ON WINDOWS --- + 'chrome_windows' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', + 'Sec-Ch-Ua' => '"Google Chrome";v="142", "Chromium";v="142", "Not=A?Brand";v="99"', + 'Sec-Ch-Ua-Mobile' => '?0', + 'Sec-Ch-Ua-Platform' => '"Windows"', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + ], + + // --- CHROME ON MACOS --- + 'chrome_mac' => [ + 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', + 'Sec-Ch-Ua' => '"Google Chrome";v="141", "Chromium";v="141", "Not=A?Brand";v="99"', + 'Sec-Ch-Ua-Mobile' => '?0', + 'Sec-Ch-Ua-Platform' => '"macOS"', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + ], + + // --- EDGE ON WINDOWS --- + 'edge_windows' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', + 'Sec-Ch-Ua' => '"Microsoft Edge";v="142", "Chromium";v="142", "Not=A?Brand";v="99"', + 'Sec-Ch-Ua-Mobile' => '?0', + 'Sec-Ch-Ua-Platform' => '"Windows"', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + ], + + // --- FIREFOX ON WINDOWS --- + 'firefox_windows' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8', + 'Accept-Language' => 'en-US,en;q=0.5', + // Firefox does not send Sec-Ch-Ua headers by default + ], + + // --- FIREFOX ON LINUX --- + 'firefox_linux' => [ + 'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:137.0) Gecko/20100101 Firefox/137.0', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8', + 'Accept-Language' => 'en-US,en;q=0.5', + ], + + // --- SAFARI ON MACOS --- + 'safari_mac' => [ + 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language' => 'en-US,en;q=0.9', + ], + + // --- CHROME ON ANDROID (Mobile) --- + 'chrome_android' => [ + 'User-Agent' => 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36', + 'Sec-Ch-Ua' => '"Google Chrome";v="142", "Chromium";v="142", "Not=A?Brand";v="99"', + 'Sec-Ch-Ua-Mobile' => '?1', + 'Sec-Ch-Ua-Platform' => '"Android"', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + ], + + // --- SAFARI ON IPHONE (Mobile) --- + 'safari_iphone' => [ + 'User-Agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1', + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language' => 'en-US,en;q=0.9', + ], + ]; + + private const COMMON_HEADERS = [ + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language' => 'en-US,en;q=0.9', + 'Sec-Fetch-Dest' => 'document', + 'Sec-Fetch-Mode' => 'navigate', + 'Sec-Fetch-Site' => 'none', + 'Sec-Fetch-User' => '?1', + 'Upgrade-Insecure-Requests' => '1', + ]; + + private const ENTRY_REFERERS = [ + 'https://www.google.com/', + 'https://www.bing.com/', + 'https://duckduckgo.com/', + 'https://t.co/', // Twitter/X shortener + 'https://www.reddit.com/', + ]; + + private ?string $lastUrl = null; + + public function __construct( + private readonly HttpClientInterface $client, + private readonly int $repeatOnFailure = 1, + ) { + } + + public function request(string $method, string $url, array $options = []): ResponseInterface + { + $repeatsLeft = $this->repeatOnFailure; + do { + $profile = self::PROFILES[array_rand(self::PROFILES)]; + + // Merge common headers with the specific browser profile + $headers = array_merge(self::COMMON_HEADERS, $profile); + + //Add a Referer header if not already set, to make it look more like a real browser request. We use the last URL we visited as the referer, to simulate internal navigation. If we don't have a last URL (first request), we pick a random entry point from common referers. + if (!isset($options['headers']['Referer'])) { + if ($this->lastUrl !== null) { + // If we have a previous URL, use it (Internal Navigation) + $headers['Referer'] = $this->lastUrl; + } else { + // First request? Pick an entry point (External Entry) + $headers['Referer'] = self::ENTRY_REFERERS[array_rand(self::ENTRY_REFERERS)]; + } + } + + // Allow manual overrides from $options + $options['headers'] = array_merge($headers, $options['headers'] ?? []); + + $response = $this->client->request($method, $url, $options); + + //When we get a 503, 403 or 429, we assume that the server is blocking us and try again with a different user agent + if (!in_array($response->getStatusCode(), [403, 429, 503], true)) { + $this->lastUrl = $url; // Update last visited URL for referer in the next request + return $response; + } + + //Otherwise we try again with a different user agent, until we run out of retries + usleep(5000); // Sleep for 5ms to avoid hammering the server too hard in case of multiple retries + } while ($repeatsLeft-- > 0); + + return $response; + } + + public function stream(iterable|ResponseInterface $responses, ?float $timeout = null): ResponseStreamInterface + { + return $this->client->stream($responses, $timeout); + } + + public function withOptions(array $options): static + { + return new self($this->client->withOptions($options), $this->repeatOnFailure); + } +} diff --git a/src/Repository/DBElementRepository.php b/src/Repository/DBElementRepository.php index 2437e848..f737d91d 100644 --- a/src/Repository/DBElementRepository.php +++ b/src/Repository/DBElementRepository.php @@ -109,6 +109,13 @@ class DBElementRepository extends EntityRepository return []; } + //Ensure that all IDs are integers and none is null + foreach ($ids as $id) { + if (!is_int($id)) { + throw new \InvalidArgumentException('Non-integer ID given to findByIDInMatchingOrder: ' . var_export($id, true)); + } + } + $cache_key = implode(',', $ids); //Check if the result is already cached diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index edccd74b..d5ccd3a6 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -22,17 +22,35 @@ declare(strict_types=1); namespace App\Repository; +use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; +use App\Settings\MiscSettings\IpnSuggestSettings; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; +use Symfony\Contracts\Translation\TranslatorInterface; +use Doctrine\ORM\EntityManagerInterface; /** * @extends NamedDBElementRepository */ class PartRepository extends NamedDBElementRepository { + private TranslatorInterface $translator; + private IpnSuggestSettings $ipnSuggestSettings; + + public function __construct( + EntityManagerInterface $em, + TranslatorInterface $translator, + IpnSuggestSettings $ipnSuggestSettings, + ) { + parent::__construct($em, $em->getClassMetadata(Part::class)); + + $this->translator = $translator; + $this->ipnSuggestSettings = $ipnSuggestSettings; + } + /** * Gets the summed up instock of all parts (only parts without a measurement unit). * @@ -84,8 +102,7 @@ class PartRepository extends NamedDBElementRepository ->where('ILIKE(part.name, :query) = TRUE') ->orWhere('ILIKE(part.description, :query) = TRUE') ->orWhere('ILIKE(category.name, :query) = TRUE') - ->orWhere('ILIKE(footprint.name, :query) = TRUE') - ; + ->orWhere('ILIKE(footprint.name, :query) = TRUE'); $qb->setParameter('query', '%'.$query.'%'); @@ -94,4 +111,371 @@ class PartRepository extends NamedDBElementRepository return $qb->getQuery()->getResult(); } + + /** + * Provides IPN (Internal Part Number) suggestions for a given part based on its category, description, + * and configured autocomplete digit length. + * + * This function generates suggestions for common prefixes and incremented prefixes based on + * the part's current category and its hierarchy. If the part is unsaved, a default "n.a." prefix is returned. + * + * @param Part $part The part for which autocomplete suggestions are generated. + * @param string $description description to assist in generating suggestions. + * @param int $suggestPartDigits The number of digits used in autocomplete increments. + * + * @return array An associative array containing the following keys: + * - 'commonPrefixes': List of common prefixes found for the part. + * - 'prefixesPartIncrement': Increments for the generated prefixes, including hierarchical prefixes. + */ + public function autoCompleteIpn(Part $part, string $description, int $suggestPartDigits): array + { + $category = $part->getCategory(); + $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; + + //Show global prefix first if configured + if ($this->ipnSuggestSettings->globalPrefix !== null && $this->ipnSuggestSettings->globalPrefix !== '') { + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $this->ipnSuggestSettings->globalPrefix, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.global_prefix') + ]; + + $increment = $this->generateNextPossibleGlobalIncrement(); + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $this->ipnSuggestSettings->globalPrefix . $increment, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.global_prefix') + ]; + } + + if (strlen($description) > 150) { + $description = substr($description, 0, 150); + } + + if ($description !== '' && $this->ipnSuggestSettings->useDuplicateDescription) { + // Check if the description is already used in another part, + + $suggestionByDescription = $this->getIpnSuggestByDescription($description); + + if ($suggestionByDescription !== null && $suggestionByDescription !== $part->getIpn() && $part->getIpn() !== null && $part->getIpn() !== '') { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $part->getIpn(), + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.current-increment') + ]; + } + + if ($suggestionByDescription !== null) { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $suggestionByDescription, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.increment') + ]; + } + } + + // Validate the category and ensure it's an instance of Category + if ($category instanceof Category) { + $currentPath = $category->getPartIpnPrefix(); + $directIpnPrefixEmpty = $category->getPartIpnPrefix() === ''; + $currentPath = $currentPath === '' ? $this->ipnSuggestSettings->fallbackPrefix : $currentPath; + + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $suggestPartDigits); + + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $currentPath . $this->ipnSuggestSettings->numberSeparator, + 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category') + ]; + + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $currentPath . $this->ipnSuggestSettings->numberSeparator . $increment, + 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category.increment') + ]; + + // Process parent categories + $parentCategory = $category->getParent(); + + while ($parentCategory instanceof Category) { + // Prepend the parent category's prefix to the current path + $effectiveIPNPrefix = $parentCategory->getPartIpnPrefix() === '' ? $this->ipnSuggestSettings->fallbackPrefix : $parentCategory->getPartIpnPrefix(); + + $currentPath = $effectiveIPNPrefix . $this->ipnSuggestSettings->categorySeparator . $currentPath; + + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $currentPath . $this->ipnSuggestSettings->numberSeparator, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment') + ]; + + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $suggestPartDigits); + + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $currentPath . $this->ipnSuggestSettings->numberSeparator . $increment, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.increment') + ]; + + // Move to the next parent category + $parentCategory = $parentCategory->getParent(); + } + } elseif ($part->getID() === null) { + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $this->ipnSuggestSettings->fallbackPrefix, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.not_saved') + ]; + } + + return $ipnSuggestions; + } + + /** + * Suggests the next IPN (Internal Part Number) based on the provided part description. + * + * Searches for parts with similar descriptions and retrieves their existing IPNs to calculate the next suggestion. + * Returns null if the description is empty or no suggestion can be generated. + * + * @param string $description The part description to search for. + * + * @return string|null The suggested IPN, or null if no suggestion is possible. + * + * @throws NonUniqueResultException + */ + public function getIpnSuggestByDescription(string $description): ?string + { + if ($description === '') { + return null; + } + + $qb = $this->createQueryBuilder('part'); + + $qb->select('part') + ->where('part.description LIKE :descriptionPattern') + ->setParameter('descriptionPattern', $description.'%') + ->orderBy('part.id', 'ASC'); + + $partsBySameDescription = $qb->getQuery()->getResult(); + $givenIpnsWithSameDescription = []; + + foreach ($partsBySameDescription as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + $givenIpnsWithSameDescription[] = $part->getIpn(); + } + + return $this->getNextIpnSuggestion($givenIpnsWithSameDescription); + } + + private function generateNextPossibleGlobalIncrement(): string + { + $qb = $this->createQueryBuilder('part'); + + + $qb->select('part.ipn') + ->where('REGEXP(part.ipn, :ipnPattern) = TRUE') + ->setParameter('ipnPattern', '^' . preg_quote($this->ipnSuggestSettings->globalPrefix, '/') . '\d+$') + ->orderBy('NATSORT(part.ipn)', 'DESC') + ->setMaxResults(1) + ; + + $highestIPN = $qb->getQuery()->getOneOrNullResult(); + if ($highestIPN !== null) { + //Remove the prefix and extract the increment part + $incrementPart = substr($highestIPN['ipn'], strlen($this->ipnSuggestSettings->globalPrefix)); + //Extract a number using regex + preg_match('/(\d+)$/', $incrementPart, $matches); + $incrementInt = isset($matches[1]) ? (int) $matches[1] + 1 : 0; + } else { + $incrementInt = 1; + } + + + return str_pad((string) $incrementInt, $this->ipnSuggestSettings->suggestPartDigits, '0', STR_PAD_LEFT); + } + + /** + * Generates the next possible increment for a part within a given category, while ensuring uniqueness. + * + * This method calculates the next available increment for a part's identifier (`ipn`) based on the current path + * and the number of digits specified for the autocomplete feature. It ensures that the generated identifier + * aligns with the expected length and does not conflict with already existing identifiers in the same category. + * + * @param string $currentPath The base path or prefix for the part's identifier. + * @param Part $currentPart The part entity for which the increment is being generated. + * @param int $suggestPartDigits The number of digits reserved for the increment. + * + * @return string The next possible increment as a zero-padded string. + * + * @throws NonUniqueResultException If the query returns non-unique results. + * @throws NoResultException If the query fails to return a result. + */ + private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $suggestPartDigits): string + { + $qb = $this->createQueryBuilder('part'); + + $expectedLength = strlen($currentPath) + strlen($this->ipnSuggestSettings->categorySeparator) + $suggestPartDigits; // Path + '-' + $suggestPartDigits digits + + // Fetch all parts in the given category, sorted by their ID in ascending order + $qb->select('part') + ->where('part.ipn LIKE :ipnPattern') + ->andWhere('LENGTH(part.ipn) = :expectedLength') + ->setParameter('ipnPattern', $currentPath . '%') + ->setParameter('expectedLength', $expectedLength) + ->orderBy('part.id', 'ASC'); + + $parts = $qb->getQuery()->getResult(); + + // Collect all used increments in the category + $usedIncrements = []; + foreach ($parts as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + if ($part->getId() === $currentPart->getId() && $currentPart->getID() !== null) { + // Extract and return the current part's increment directly + $incrementPart = substr($part->getIpn(), -$suggestPartDigits); + if (is_numeric($incrementPart)) { + return str_pad((string) $incrementPart, $suggestPartDigits, '0', STR_PAD_LEFT); + } + } + + // Extract last $autocompletePartDigits digits for possible available part increment + $incrementPart = substr($part->getIpn(), -$suggestPartDigits); + if (is_numeric($incrementPart)) { + $usedIncrements[] = (int) $incrementPart; + } + + } + + // Generate the next free $autocompletePartDigits-digit increment + $nextIncrement = 1; // Start at the beginning + + while (in_array($nextIncrement, $usedIncrements, true)) { + $nextIncrement++; + } + + return str_pad((string) $nextIncrement, $suggestPartDigits, '0', STR_PAD_LEFT); + } + + /** + * Generates the next IPN suggestion based on the maximum numeric suffix found in the given IPNs. + * + * The new IPN is constructed using the base format of the first provided IPN, + * incremented by the next free numeric suffix. If no base IPNs are found, + * returns null. + * + * @param array $givenIpns List of IPNs to analyze. + * + * @return string|null The next suggested IPN, or null if no base IPNs can be derived. + */ + private function getNextIpnSuggestion(array $givenIpns): ?string { + $maxSuffix = 0; + + foreach ($givenIpns as $ipn) { + // Check whether the IPN contains a suffix "_ " + if (preg_match('/_(\d+)$/', $ipn, $matches)) { + $suffix = (int)$matches[1]; + if ($suffix > $maxSuffix) { + $maxSuffix = $suffix; // Höchste Nummer speichern + } + } + } + + // Find the basic format (the IPN without suffix) from the first IPN + $baseIpn = $givenIpns[0] ?? ''; + $baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Remove existing "_ " + + if ($baseIpn === '') { + return null; + } + + // Generate next free possible IPN + return $baseIpn . '_' . ($maxSuffix + 1); + } + + /** + * Finds a part based on the provided info provider key and ID, with an option for case sensitivity. + * If no part is found with the given provider key and ID, null is returned. + * @param string $providerID + * @param string|null $providerKey If null, the provider key will not be included in the search criteria, and only the provider ID will be used for matching. + * @param bool $caseInsensitive If true, the provider ID comparison will be case-insensitive. Default is true. + * @return Part|null + */ + public function getPartByProviderInfo(string $providerID, ?string $providerKey = null, bool $caseInsensitive = true): ?Part + { + $qb = $this->createQueryBuilder('part'); + $qb->select('part'); + + if ($providerKey) { + $qb->where("part.providerReference.provider_key = :providerKey"); + $qb->setParameter('providerKey', $providerKey); + } + + + if ($caseInsensitive) { + $qb->andWhere("LOWER(part.providerReference.provider_id) = LOWER(:providerID)"); + } else { + $qb->andWhere("part.providerReference.provider_id = :providerID"); + } + + $qb->setParameter('providerID', $providerID); + + return $qb->getQuery()->getOneOrNullResult(); + } + + /** + * Finds a part based on the provided MPN (Manufacturer Part Number), with an option for case sensitivity. + * If no part is found with the given MPN, null is returned. + * @param string $mpn + * @param string|null $manufacturerName If provided, the search will also include a match for the manufacturer's name. If null, the manufacturer name will not be included in the search criteria. + * @param bool $caseInsensitive If true, the MPN comparison will be case-insensitive. Default is true (case-insensitive). + * @return Part|null + */ + public function getPartByMPN(string $mpn, ?string $manufacturerName = null, bool $caseInsensitive = true): ?Part + { + $qb = $this->createQueryBuilder('part'); + $qb->select('part'); + + if ($caseInsensitive) { + $qb->where("LOWER(part.manufacturer_product_number) = LOWER(:mpn)"); + } else { + $qb->where("part.manufacturer_product_number = :mpn"); + } + + if ($manufacturerName !== null) { + $qb->leftJoin('part.manufacturer', 'manufacturer'); + + if ($caseInsensitive) { + $qb->andWhere("LOWER(manufacturer.name) = LOWER(:manufacturerName)"); + } else { + $qb->andWhere("manufacturer.name = :manufacturerName"); + } + $qb->setParameter('manufacturerName', $manufacturerName); + } + + $qb->setParameter('mpn', $mpn); + + return $qb->getQuery()->getOneOrNullResult(); + } + + /** + * Finds a part based on the provided SPN (Supplier Part Number), with an option for case sensitivity. + * If no part is found with the given SPN, null is returned. + * @param string $spn + * @param bool $caseInsensitive + * @return Part|null + */ + public function getPartBySPN(string $spn, bool $caseInsensitive = true): ?Part + { + $qb = $this->createQueryBuilder('part'); + $qb->select('part'); + + $qb->leftJoin('part.orderdetails', 'o'); + + if ($caseInsensitive) { + $qb->where("LOWER(o.supplierpartnr) = LOWER(:spn)"); + } else { + $qb->where("o.supplierpartnr = :spn"); + } + + $qb->setParameter('spn', $spn); + + return $qb->getQuery()->getOneOrNullResult(); + } } diff --git a/src/Repository/Parts/PartCustomStateRepository.php b/src/Repository/Parts/PartCustomStateRepository.php new file mode 100644 index 00000000..d66221a2 --- /dev/null +++ b/src/Repository/Parts/PartCustomStateRepository.php @@ -0,0 +1,48 @@ +. + */ +namespace App\Repository\Parts; + +use App\Entity\Parts\PartCustomState; +use App\Repository\AbstractPartsContainingRepository; +use InvalidArgumentException; + +class PartCustomStateRepository extends AbstractPartsContainingRepository +{ + public function getParts(object $element, string $nameOrderDirection = "ASC"): array + { + if (!$element instanceof PartCustomState) { + throw new InvalidArgumentException('$element must be an PartCustomState!'); + } + + return $this->getPartsByField($element, $nameOrderDirection, 'partUnit'); + } + + public function getPartsCount(object $element): int + { + if (!$element instanceof PartCustomState) { + throw new InvalidArgumentException('$element must be an PartCustomState!'); + } + + return $this->getPartsCountByField($element, 'partUnit'); + } +} diff --git a/src/Repository/StructuralDBElementRepository.php b/src/Repository/StructuralDBElementRepository.php index 781c7622..7fc38671 100644 --- a/src/Repository/StructuralDBElementRepository.php +++ b/src/Repository/StructuralDBElementRepository.php @@ -243,6 +243,14 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit return $result[0]; } + //If the name contains category delimiters like ->, try to find the element by its full path + if (str_contains($name, '->')) { + $tmp = $this->getEntityByPath($name, '->'); + if (count($tmp) > 0) { + return $tmp[count($tmp) - 1]; + } + } + //If we find nothing, return null return null; } diff --git a/src/Security/Voter/AttachmentVoter.php b/src/Security/Voter/AttachmentVoter.php index bd7ae4df..df3d73a7 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Security\Voter; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\AttachmentContainingDBElement; @@ -99,6 +100,8 @@ final class AttachmentVoter extends Voter $param = 'measurement_units'; } elseif (is_a($subject, PartAttachment::class, true)) { $param = 'parts'; + } elseif (is_a($subject, PartCustomStateAttachment::class, true)) { + $param = 'part_custom_states'; } elseif (is_a($subject, StorageLocationAttachment::class, true)) { $param = 'storelocations'; } elseif (is_a($subject, SupplierAttachment::class, true)) { diff --git a/src/Security/Voter/ParameterVoter.php b/src/Security/Voter/ParameterVoter.php index f59bdeaf..5dc30ea2 100644 --- a/src/Security/Voter/ParameterVoter.php +++ b/src/Security/Voter/ParameterVoter.php @@ -22,6 +22,7 @@ declare(strict_types=1); */ namespace App\Security\Voter; +use App\Entity\Parameters\PartCustomStateParameter; use App\Services\UserSystem\VoterHelper; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractDBElement; @@ -97,6 +98,8 @@ final class ParameterVoter extends Voter $param = 'measurement_units'; } elseif (is_a($subject, PartParameter::class, true)) { $param = 'parts'; + } elseif (is_a($subject, PartCustomStateParameter::class, true)) { + $param = 'part_custom_states'; } elseif (is_a($subject, StorageLocationParameter::class, true)) { $param = 'storelocations'; } elseif (is_a($subject, SupplierParameter::class, true)) { diff --git a/src/Security/Voter/PartLotVoter.php b/src/Security/Voter/PartLotVoter.php index 87c3d135..5748f4af 100644 --- a/src/Security/Voter/PartLotVoter.php +++ b/src/Security/Voter/PartLotVoter.php @@ -58,13 +58,13 @@ final class PartLotVoter extends Voter { } - protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element', 'withdraw', 'add', 'move']; + protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element', 'withdraw', 'add', 'move', 'stocktake']; protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { $user = $this->helper->resolveUser($token); - if (in_array($attribute, ['withdraw', 'add', 'move'], true)) + if (in_array($attribute, ['withdraw', 'add', 'move', 'stocktake'], true)) { $base_permission = $this->helper->isGranted($token, 'parts_stock', $attribute, $vote); diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index ad0299a7..16d38e05 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Security\Voter; use App\Entity\Attachments\AttachmentType; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -53,6 +54,7 @@ final class StructureVoter extends Voter Supplier::class => 'suppliers', Currency::class => 'currencies', MeasurementUnit::class => 'measurement_units', + PartCustomState::class => 'part_custom_states', ]; public function __construct(private readonly VoterHelper $helper) diff --git a/src/Serializer/APIPlatform/SkippableItemNormalizer.php b/src/Serializer/APIPlatform/SkippableItemNormalizer.php index 5568c4cb..61873615 100644 --- a/src/Serializer/APIPlatform/SkippableItemNormalizer.php +++ b/src/Serializer/APIPlatform/SkippableItemNormalizer.php @@ -23,9 +23,13 @@ declare(strict_types=1); namespace App\Serializer\APIPlatform; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Serializer\ItemNormalizer; use Symfony\Component\DependencyInjection\Attribute\AsDecorator; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerAwareInterface; @@ -35,6 +39,10 @@ use Symfony\Component\Serializer\SerializerInterface; * This class decorates API Platform's ItemNormalizer to allow skipping the normalization process by setting the * DISABLE_ITEM_NORMALIZER context key to true. This is useful for all kind of serialization operations, where the API * Platform subsystem should not be used. + * + * It also works around a bug in API Platform's AbstractItemNormalizer where IRI strings for abstract resource classes + * with a discriminator map fail deserialization when objectToPopulate is null (the discriminator is checked before + * the IRI string check). See: https://github.com/Part-DB/Part-DB-server/issues/1370 */ #[AsDecorator("api_platform.serializer.normalizer.item")] class SkippableItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface @@ -42,13 +50,44 @@ class SkippableItemNormalizer implements NormalizerInterface, DenormalizerInterf public const DISABLE_ITEM_NORMALIZER = 'DISABLE_ITEM_NORMALIZER'; - public function __construct(private readonly ItemNormalizer $inner) - { - + public function __construct( + private readonly ItemNormalizer $inner, + private readonly IriConverterInterface $iriConverter, + ) { } public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed { + // API Platform's AbstractItemNormalizer has a bug: when objectToPopulate is null and data is an IRI + // string, it tries to resolve the discriminator class from [$iri_string] before reaching the IRI + // check (line 271). For abstract resource classes with a discriminator map (e.g. Attachment), this + // fails because the array has no _type key. Fix by resolving IRI strings directly. + // See: https://github.com/Part-DB/Part-DB-server/issues/1370 + if (is_string($data) || (is_array($data) && isset($data['@id']) && is_string($data['@id']))) { + if (is_array($data)) { + $iri = $data['@id']; + } else { + $iri = $data; + } + + try { + return $this->iriConverter->getResourceFromIri($iri, $context + ['fetch_data' => true]); + } catch (ItemNotFoundException $e) { + if (false === ($context['denormalize_throw_on_relation_not_found'] ?? true)) { + return null; + } + if (!isset($context['not_normalizable_value_exceptions'])) { + throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); + } + throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [$type], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); + } catch (InvalidArgumentException $e) { + if (!isset($context['not_normalizable_value_exceptions'])) { + throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $iri), $e->getCode(), $e); + } + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Invalid IRI "%s".', $data), $data, [$type], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); + } + } + return $this->inner->denormalize($data, $type, $format, $context); } @@ -87,4 +126,4 @@ class SkippableItemNormalizer implements NormalizerInterface, DenormalizerInterf 'object' => false ]; } -} \ No newline at end of file +} diff --git a/src/Serializer/PartNormalizer.php b/src/Serializer/PartNormalizer.php index e6dc7416..2f608d53 100644 --- a/src/Serializer/PartNormalizer.php +++ b/src/Serializer/PartNormalizer.php @@ -55,6 +55,15 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm 'spn' => 'supplier_part_number', 'supplier_product_number' => 'supplier_part_number', 'storage_location' => 'storelocation', + //EDA/KiCad field aliases + 'kicad_symbol' => 'eda_kicad_symbol', + 'kicad_footprint' => 'eda_kicad_footprint', + 'kicad_reference' => 'eda_reference_prefix', + 'kicad_value' => 'eda_value', + 'eda_exclude_bom' => 'eda_exclude_from_bom', + 'eda_exclude_board' => 'eda_exclude_from_board', + 'eda_exclude_sim' => 'eda_exclude_from_sim', + 'eda_invisible' => 'eda_visibility', ]; public function __construct( @@ -193,9 +202,45 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm } } + //Handle EDA/KiCad fields + $this->applyEdaFields($object, $data); + return $object; } + /** + * Apply EDA/KiCad fields from CSV data to the Part's EDAPartInfo. + */ + private function applyEdaFields(Part $part, array $data): void + { + $edaInfo = $part->getEdaInfo(); + + if (!empty($data['eda_kicad_symbol'])) { + $edaInfo->setKicadSymbol(trim((string) $data['eda_kicad_symbol'])); + } + if (!empty($data['eda_kicad_footprint'])) { + $edaInfo->setKicadFootprint(trim((string) $data['eda_kicad_footprint'])); + } + if (!empty($data['eda_reference_prefix'])) { + $edaInfo->setReferencePrefix(trim((string) $data['eda_reference_prefix'])); + } + if (!empty($data['eda_value'])) { + $edaInfo->setValue(trim((string) $data['eda_value'])); + } + if (isset($data['eda_exclude_from_bom']) && $data['eda_exclude_from_bom'] !== '') { + $edaInfo->setExcludeFromBom(filter_var($data['eda_exclude_from_bom'], FILTER_VALIDATE_BOOLEAN)); + } + if (isset($data['eda_exclude_from_board']) && $data['eda_exclude_from_board'] !== '') { + $edaInfo->setExcludeFromBoard(filter_var($data['eda_exclude_from_board'], FILTER_VALIDATE_BOOLEAN)); + } + if (isset($data['eda_exclude_from_sim']) && $data['eda_exclude_from_sim'] !== '') { + $edaInfo->setExcludeFromSim(filter_var($data['eda_exclude_from_sim'], FILTER_VALIDATE_BOOLEAN)); + } + if (isset($data['eda_visibility']) && $data['eda_visibility'] !== '') { + $edaInfo->setVisibility(filter_var($data['eda_visibility'], FILTER_VALIDATE_BOOLEAN)); + } + } + /** * @return bool[] */ diff --git a/src/Serializer/StructuralElementDenormalizer.php b/src/Serializer/StructuralElementDenormalizer.php index 9f4256f9..3da5f796 100644 --- a/src/Serializer/StructuralElementDenormalizer.php +++ b/src/Serializer/StructuralElementDenormalizer.php @@ -42,6 +42,8 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz private const ALREADY_CALLED = 'STRUCTURAL_DENORMALIZER_ALREADY_CALLED'; + private const PARENT_ELEMENT = 'STRUCTURAL_DENORMALIZER_PARENT_ELEMENT'; + private array $object_cache = []; public function __construct( @@ -89,37 +91,59 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz $context[self::ALREADY_CALLED][] = $data; + //In the first step, denormalize without children + $context_without_children = $context; + $context_without_children['groups'] = array_filter( + $context_without_children['groups'] ?? [], + static fn($group) => $group !== 'include_children', + ); + //Also unset any parent element, to avoid infinite loops. We will set the parent element in the next step, when we denormalize the children + unset($context_without_children[self::PARENT_ELEMENT]); + /** @var AbstractStructuralDBElement $entity */ + $entity = $this->denormalizer->denormalize($data, $type, $format, $context_without_children); - /** @var AbstractStructuralDBElement $deserialized_entity */ - $deserialized_entity = $this->denormalizer->denormalize($data, $type, $format, $context); + //Assign the parent element to the denormalized entity, so it can be used in the denormalization of the children (e.g. for path generation) + if (isset($context[self::PARENT_ELEMENT]) && $context[self::PARENT_ELEMENT] instanceof $entity && $entity->getID() === null) { + $entity->setParent($context[self::PARENT_ELEMENT]); + } //Check if we already have the entity in the database (via path) /** @var StructuralDBElementRepository $repo */ $repo = $this->entityManager->getRepository($type); - $path = $deserialized_entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW); + $path = $entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW); $db_elements = $repo->getEntityByPath($path, AbstractStructuralDBElement::PATH_DELIMITER_ARROW); if ($db_elements !== []) { //We already have the entity in the database, so we can return it - return end($db_elements); + $entity = end($db_elements); } //Check if we have created the entity in this request before (so we don't create multiple entities for the same path) //Entities get saved in the cache by type and path //We use a different cache for this then the objects created by a string value (saved in repo). However, that should not be a problem - //unless the user data has mixed structure between json data and a string path + //unless the user data has mixed structure between JSON data and a string path if (isset($this->object_cache[$type][$path])) { - return $this->object_cache[$type][$path]; + $entity = $this->object_cache[$type][$path]; + } else { + //Save the entity in the cache + $this->object_cache[$type][$path] = $entity; } - //Save the entity in the cache - $this->object_cache[$type][$path] = $deserialized_entity; + //In the next step we can denormalize the children, and add our children to the entity. + if (in_array('include_children', $context['groups'], true) && isset($data['children']) && is_array($data['children'])) { + foreach ($data['children'] as $child_data) { + $child_entity = $this->denormalize($child_data, $type, $format, array_merge($context, [self::PARENT_ELEMENT => $entity])); + if ($child_entity !== null && !$entity->getChildren()->contains($child_entity)) { + $entity->addChild($child_entity); + } + } + } //We don't have the entity in the database, so we have to persist it - $this->entityManager->persist($deserialized_entity); + $this->entityManager->persist($entity); - return $deserialized_entity; + return $entity; } public function getSupportedTypes(?string $format): array diff --git a/src/Services/AI/AIPlatformRegistry.php b/src/Services/AI/AIPlatformRegistry.php new file mode 100644 index 00000000..408bb181 --- /dev/null +++ b/src/Services/AI/AIPlatformRegistry.php @@ -0,0 +1,94 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\AI; + +use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface; +use Symfony\AI\Platform\PlatformInterface; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + +final readonly class AIPlatformRegistry +{ + /** + * All registered platforms, indexed by their service tag name (e.g. "openrouter", "lmstudio") + * @var array $allPlatforms + */ + private array $allPlatforms; + + /** + * All registered platforms, indexed by their AIPlatforms enum value (e.g. AIPlatforms::OPENROUTER->value) + * @var array $enabledPlatforms + */ + private array $enabledPlatforms; + + public function __construct( + SettingsManagerInterface $settingsManager, + #[AutowireIterator(tag: 'ai.platform', indexAttribute: 'name')] + iterable $platforms, + ) { + $this->allPlatforms = iterator_to_array($platforms); + + //Check which platforms are active based on the settings and store them in $activePlatforms + $tmp = []; + foreach (AIPlatforms::cases() as $platform) { + if (isset($this->allPlatforms[$platform->toServiceTagName()])) { + //Check if the platform is active by calling its isActive() on the settings class + $settings = $settingsManager->get($platform->toSettingsClass()); + if (!$settings->isAIPlatformEnabled()) { + continue; + } + + $tmp[$platform->value] = $this->allPlatforms[$platform->toServiceTagName()]; + } + } + $this->enabledPlatforms = $tmp; + } + + public function getPlatform(AIPlatforms $platform): PlatformInterface + { + if (!isset($this->enabledPlatforms[$platform->value])) { + throw new \InvalidArgumentException(sprintf('AI platform "%s" is not active or does not exist.', $platform->name)); + } + + return $this->enabledPlatforms[$platform->value]; + } + + /** + * Check if the given platform is active (i.e. it is registered and its settings are properly configured) + * @param AIPlatforms $platform + * @return bool + */ + public function isEnabled(AIPlatforms $platform): bool + { + return isset($this->enabledPlatforms[$platform->value]); + } + + /** + * Returns an array of all active platforms, indexed by their AIPlatforms enum value (e.g. AIPlatforms::OPENROUTER->value) + * @return PlatformInterface[] + */ + public function getEnabledPlatforms(): array + { + return $this->enabledPlatforms; + } +} diff --git a/assets/controllers/turbo/title_controller.js b/src/Services/AI/AIPlatformSettingsInterface.php similarity index 65% rename from assets/controllers/turbo/title_controller.js rename to src/Services/AI/AIPlatformSettingsInterface.php index 6bbebdf7..c400db46 100644 --- a/assets/controllers/turbo/title_controller.js +++ b/src/Services/AI/AIPlatformSettingsInterface.php @@ -1,31 +1,33 @@ -/* - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - connect() { - //If we encounter an element with this, then change the title of our document according to data-title - this.changeTitle(this.element.dataset.title); - } - - changeTitle(title) { - document.title = title; - } -} \ No newline at end of file +. + */ + +declare(strict_types=1); + + +namespace App\Services\AI; + +interface AIPlatformSettingsInterface +{ + /** + * Returns true, if the AI platform is enabled in the settings and can be used, false otherwise. + * @return bool + */ + public function isAIPlatformEnabled(): bool; +} diff --git a/src/Services/AI/AIPlatforms.php b/src/Services/AI/AIPlatforms.php new file mode 100644 index 00000000..2f4d6317 --- /dev/null +++ b/src/Services/AI/AIPlatforms.php @@ -0,0 +1,64 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\AI; + +use App\Settings\AISettings\LMStudioSettings; +use App\Settings\AISettings\OpenRouterSettings; +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum AIPlatforms: string implements TranslatableInterface +{ + case OPENROUTER = 'openrouter'; + case LMSTUDIO = 'lmstudio'; + + /** + * Returns the name attribute of the service tag for this platform, which is used to register the platform in the AIPlatformRegistry + * @return string + */ + public function toServiceTagName(): string + { + return $this->value; + } + + /** + * Returns the class name of the settings class for this platform, which implements AIPlatformSettingsInterface + * @return string + * @phpstan-return class-string + */ + public function toSettingsClass(): string + { + return match ($this) { + self::LMSTUDIO => LMStudioSettings::class, + self::OPENROUTER => OpenRouterSettings::class, + }; + } + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + $key = 'settings.ai.' . $this->value; + + return $translator->trans($key, locale: $locale); + } +} diff --git a/src/Services/AI/AcceptAllModelsCatalog.php b/src/Services/AI/AcceptAllModelsCatalog.php new file mode 100644 index 00000000..a2f5c33a --- /dev/null +++ b/src/Services/AI/AcceptAllModelsCatalog.php @@ -0,0 +1,61 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\AI; + +use Symfony\AI\Platform\Bridge\Generic\CompletionsModel; +use Symfony\AI\Platform\Exception\ModelNotFoundException; +use Symfony\AI\Platform\Model; +use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +/** + * This is a wrapper, to allow accepting all models, even if they are not contained in the decorated ModelCatalogInterface. + * This is a workaround for outdated/incomplete model catalogs provided by AI platforms, which do not contain all available models, or do not update their catalogs frequently enough. + */ +#[AsDecorator('ai.platform.model_catalog.lmstudio')] +#[AsDecorator('ai.platform.model_catalog.openrouter')] +final readonly class AcceptAllModelsCatalog implements ModelCatalogInterface +{ + + public function __construct(private ModelCatalogInterface $decorated) + { + } + + public function getModel(string $modelName): Model + { + //Use the actual values when its available. + try { + return $this->decorated->getModel($modelName); + } catch (ModelNotFoundException $e) { + //If the model is not found, return a generic model with the given name and no capabilities. + return new CompletionsModel($modelName, []); + } + } + + public function getModels(): array + { + //Return the actual models catalog here for correct autocompletition + return $this->decorated->getModels(); + } +} diff --git a/src/Services/Attachments/AttachmentManager.php b/src/Services/Attachments/AttachmentManager.php index 1075141b..c661b0f4 100644 --- a/src/Services/Attachments/AttachmentManager.php +++ b/src/Services/Attachments/AttachmentManager.php @@ -156,8 +156,8 @@ class AttachmentManager //Taken from: https://www.php.net/manual/de/function.filesize.php#106569 and slightly modified $sz = 'BKMGTP'; - $factor = (int) floor((strlen((string) $bytes) - 1) / 3); + $factor = min((int) floor((strlen((string) $bytes) - 1) / 3), strlen($sz) - 1); //Use real (10 based) SI prefixes - return sprintf("%.{$decimals}f", $bytes / 1000 ** $factor).@$sz[$factor]; + return sprintf("%.{$decimals}f", $bytes / 1000 ** $factor).$sz[$factor]; } } diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index 9fbc3fe3..25f6142f 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -30,6 +30,7 @@ use App\Entity\Attachments\AttachmentUpload; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; use App\Entity\Attachments\LabelAttachment; +use App\Entity\Attachments\PartCustomStateAttachment; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; @@ -43,6 +44,8 @@ use App\Exceptions\AttachmentDownloadException; use App\Settings\SystemSettings\AttachmentsSettings; use Hshn\Base64EncodedFile\HttpFoundation\File\Base64EncodedFile; use Hshn\Base64EncodedFile\HttpFoundation\File\UploadedBase64EncodedFile; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; use const DIRECTORY_SEPARATOR; use InvalidArgumentException; use RuntimeException; @@ -75,11 +78,14 @@ class AttachmentSubmitHandler protected FileTypeFilterTools $filterTools, protected AttachmentsSettings $settings, protected readonly SVGSanitizer $SVGSanitizer, + #[Autowire(env: "bool:ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK")] + private readonly bool $allow_local_network_downloads = false, ) { //The mapping used to determine which folder will be used for an attachment type $this->folder_mapping = [ PartAttachment::class => 'part', + PartCustomStateAttachment::class => 'part_custom_state', AttachmentTypeAttachment::class => 'attachment_type', CategoryAttachment::class => 'category', CurrencyAttachment::class => 'currency', @@ -93,6 +99,10 @@ class AttachmentSubmitHandler UserAttachment::class => 'user', LabelAttachment::class => 'label_profile', ]; + + if (!$this->allow_local_network_downloads) { + $this->httpClient = new NoPrivateNetworkHttpClient($this->httpClient); + } } /** @@ -135,7 +145,10 @@ class AttachmentSubmitHandler $attachment->getName() ); - return $safeName.'-'.uniqid('', false).'.'.$extension; + // Generate a 12-character URL-safe random string, which should avoid collisions and prevent from guessing file paths. + $random = str_replace(['+', '/', '='], ['0', '1', '2'], base64_encode(random_bytes(9))); + + return $safeName.'-'.$random.'.'.$extension; } /** @@ -368,6 +381,7 @@ class AttachmentSubmitHandler ], ]; + $response = $this->httpClient->request('GET', $url, $opts); //Digikey wants TLSv1.3, so try again with that if we get a 403 if ($response->getStatusCode() === 403) { @@ -429,8 +443,8 @@ class AttachmentSubmitHandler $new_path = $this->pathResolver->realPathToPlaceholder($new_path); //Save the path to the attachment $attachment->setInternalPath($new_path); - } catch (TransportExceptionInterface) { - throw new AttachmentDownloadException('Transport error!'); + } catch (TransportExceptionInterface $exception) { + throw new AttachmentDownloadException('Transport error: '.$exception->getMessage()); } return $attachment; diff --git a/src/Services/Attachments/AttachmentURLGenerator.php b/src/Services/Attachments/AttachmentURLGenerator.php index e505408f..89856650 100644 --- a/src/Services/Attachments/AttachmentURLGenerator.php +++ b/src/Services/Attachments/AttachmentURLGenerator.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\Attachments; +use App\Settings\SystemSettings\AttachmentsSettings; use Imagine\Exception\RuntimeException; use App\Entity\Attachments\Attachment; use InvalidArgumentException; @@ -40,7 +41,7 @@ class AttachmentURLGenerator public function __construct(protected Packages $assets, protected AttachmentPathResolver $pathResolver, protected UrlGeneratorInterface $urlGenerator, protected AttachmentManager $attachmentHelper, - protected CacheManager $thumbnailManager, protected LoggerInterface $logger) + protected CacheManager $thumbnailManager, protected LoggerInterface $logger, private readonly AttachmentsSettings $attachmentsSettings) { //Determine a normalized path to the public folder (assets are relative to this folder) $this->public_path = $this->pathResolver->parameterToAbsolutePath('public'); @@ -99,6 +100,10 @@ class AttachmentURLGenerator return null; } + if ($this->attachmentsSettings->showHTMLAttachments && $attachment->isLocalHTMLFile()) { + return $this->urlGenerator->generate('attachment_html_sandbox', ['id' => $attachment->getID()]); + } + $asset_path = $this->absolutePathToAssetPath($absolute_path); //If path is not relative to public path or marked as secure, serve it via controller if (null === $asset_path || $attachment->isSecure()) { diff --git a/src/Services/Attachments/FileTypeFilterTools.php b/src/Services/Attachments/FileTypeFilterTools.php index d689fda3..198d4f79 100644 --- a/src/Services/Attachments/FileTypeFilterTools.php +++ b/src/Services/Attachments/FileTypeFilterTools.php @@ -139,7 +139,7 @@ class FileTypeFilterTools { $filter = trim($filter); - return $this->cache->get('filter_exts_'.md5($filter), function (ItemInterface $item) use ($filter) { + return $this->cache->get('filter_exts_'.hash('xxh3', $filter), function (ItemInterface $item) use ($filter) { $elements = explode(',', $filter); $extensions = []; diff --git a/src/Services/Attachments/PartPreviewGenerator.php b/src/Services/Attachments/PartPreviewGenerator.php index ba6e5db0..9aedba74 100644 --- a/src/Services/Attachments/PartPreviewGenerator.php +++ b/src/Services/Attachments/PartPreviewGenerator.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Services\Attachments; use App\Entity\Parts\Footprint; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\StorageLocation; diff --git a/src/Services/Cache/UserCacheKeyGenerator.php b/src/Services/Cache/UserCacheKeyGenerator.php index ac5487a5..b9ff57c4 100644 --- a/src/Services/Cache/UserCacheKeyGenerator.php +++ b/src/Services/Cache/UserCacheKeyGenerator.php @@ -57,7 +57,7 @@ class UserCacheKeyGenerator //If the user is null, then treat it as anonymous user. //When the anonymous user is passed as user then use this path too. if (!($user instanceof User) || User::ID_ANONYMOUS === $user->getID()) { - return 'user$_'.User::ID_ANONYMOUS; + return 'user$_'.User::ID_ANONYMOUS . '_'.$locale; } //Use the unique user id and the locale to generate the key diff --git a/src/Services/EDA/KiCadHelper.php b/src/Services/EDA/KiCadHelper.php index 75c2cc34..42cc2518 100644 --- a/src/Services/EDA/KiCadHelper.php +++ b/src/Services/EDA/KiCadHelper.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Services\EDA; +use App\Entity\Attachments\Attachment; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Part; @@ -43,6 +44,9 @@ class KiCadHelper /** @var int The maximum level of the shown categories. 0 Means only the top level categories are shown. -1 means only a single one containing */ private readonly int $category_depth; + /** @var bool Whether to resolve actual datasheet PDF URLs (true) or use Part-DB page links (false) */ + private readonly bool $datasheetAsPdf; + public function __construct( private readonly NodesListBuilder $nodesListBuilder, private readonly TagAwareCacheInterface $kicadCache, @@ -51,9 +55,10 @@ class KiCadHelper private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityURLGenerator $entityURLGenerator, private readonly TranslatorInterface $translator, - KiCadEDASettings $kiCadEDASettings, + private readonly KiCadEDASettings $kiCadEDASettings, ) { $this->category_depth = $kiCadEDASettings->categoryDepth; + $this->datasheetAsPdf = $kiCadEDASettings->datasheetAsPdf ?? true; } /** @@ -115,11 +120,16 @@ class KiCadHelper } //Format the category for KiCAD + // Use the category comment as description if available, otherwise use the Part-DB URL + $description = $category->getComment(); + if ($description === null || $description === '') { + $description = $this->entityURLGenerator->listPartsURL($category); + } + $result[] = [ 'id' => (string)$category->getId(), 'name' => $category->getFullPath('/'), - //Show the category link as the category description, this also fixes an segfault in KiCad see issue #878 - 'description' => $this->entityURLGenerator->listPartsURL($category), + 'description' => $description, ]; } @@ -131,11 +141,13 @@ class KiCadHelper * Returns an array of objects containing all parts for the given category in the format required by KiCAD. * The result is cached for performance and invalidated on category or part changes. * @param Category|null $category + * @param bool $minimal If true, only return id and name (faster for symbol chooser listing) * @return array */ - public function getCategoryParts(?Category $category): array + public function getCategoryParts(?Category $category, bool $minimal = false): array { - return $this->kicadCache->get('kicad_category_parts_'.($category?->getID() ?? 0) . '_' . $this->category_depth, + $cacheKey = 'kicad_category_parts_'.($category?->getID() ?? 0) . '_' . $this->category_depth . ($minimal ? '_min' : ''); + return $this->kicadCache->get($cacheKey, function (ItemInterface $item) use ($category) { $item->tag([ $this->tagGenerator->getElementTypeCacheTag(Category::class), @@ -189,7 +201,8 @@ class KiCadHelper "symbolIdStr" => $part->getEdaInfo()->getKicadSymbol() ?? $part->getCategory()?->getEdaInfo()->getKicadSymbol() ?? "", "exclude_from_bom" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBom() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBom() ?? false), "exclude_from_board" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBoard() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBoard() ?? false), - "exclude_from_sim" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromSim() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromSim() ?? true), + "exclude_from_sim" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromSim() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromSim() ?? false), + "description" => $part->getDescription(), "fields" => [] ]; @@ -198,14 +211,22 @@ class KiCadHelper $result["fields"]["value"] = $this->createField($part->getEdaInfo()->getValue() ?? $part->getName(), true); $result["fields"]["keywords"] = $this->createField($part->getTags()); - //Use the part info page as datasheet link. It must be an absolute URL. - $result["fields"]["datasheet"] = $this->createField( - $this->urlGenerator->generate( - 'part_info', - ['id' => $part->getId()], - UrlGeneratorInterface::ABSOLUTE_URL) + //Use the part info page as Part-DB link. It must be an absolute URL. + $partUrl = $this->urlGenerator->generate( + 'part_info', + ['id' => $part->getId()], + UrlGeneratorInterface::ABSOLUTE_URL ); + //Try to find an actual datasheet attachment (configurable: PDF URL vs Part-DB page link) + if ($this->datasheetAsPdf) { + $datasheetUrl = $this->findDatasheetUrl($part); + $result["fields"]["datasheet"] = $this->createField($datasheetUrl ?? $partUrl); + } else { + $result["fields"]["datasheet"] = $this->createField($partUrl); + } + $result["fields"]["Part-DB URL"] = $this->createField($partUrl); + //Add basic fields $result["fields"]["description"] = $this->createField($part->getDescription()); if ($part->getCategory() !== null) { @@ -233,6 +254,10 @@ class KiCadHelper } $result["fields"]["Part-DB Unit"] = $this->createField($unit); } + if ($part->getPartCustomState() !== null) { + $customState = $part->getPartCustomState()->getName(); + $result["fields"]["Part-DB Custom state"] = $this->createField($customState); + } if ($part->getMass()) { $result["fields"]["Mass"] = $this->createField($part->getMass() . ' g'); } @@ -241,32 +266,7 @@ class KiCadHelper $result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn()); } - // Add supplier information from orderdetails (include obsolete orderdetails) - if ($part->getOrderdetails(false)->count() > 0) { - $supplierCounts = []; - - foreach ($part->getOrderdetails(false) as $orderdetail) { - if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') { - $supplierName = $orderdetail->getSupplier()->getName(); - - $supplierName .= " SPN"; // Append "SPN" to the supplier name to indicate Supplier Part Number - - if (!isset($supplierCounts[$supplierName])) { - $supplierCounts[$supplierName] = 0; - } - $supplierCounts[$supplierName]++; - - // Create field name with sequential number if more than one from same supplier (e.g. "Mouser", "Mouser 2", etc.) - $fieldName = $supplierCounts[$supplierName] > 1 - ? $supplierName . ' ' . $supplierCounts[$supplierName] - : $supplierName; - - $result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr()); - } - } - } - - //Add fields for KiCost: + //Add KiCost manufacturer fields (always present, independent of orderdetails) if ($part->getManufacturer() !== null) { $result["fields"]["manf"] = $this->createField($part->getManufacturer()->getName()); } @@ -274,13 +274,74 @@ class KiCadHelper $result['fields']['manf#'] = $this->createField($part->getManufacturerProductNumber()); } - //For each supplier, add a field with the supplier name and the supplier part number for KiCost - if ($part->getOrderdetails(false)->count() > 0) { - foreach ($part->getOrderdetails(false) as $orderdetail) { + // Add supplier information from orderdetails (include obsolete orderdetails) + // If any orderdetail has eda_visibility explicitly set to true, only export those; + // otherwise export all (backward compat when no flags are set) + $allOrderdetails = $part->getOrderdetails(false); + if ($allOrderdetails->count() > 0) { + $hasExplicitEdaVisibility = false; + foreach ($allOrderdetails as $od) { + if ($od->isEdaVisibility() !== null) { + $hasExplicitEdaVisibility = true; + break; + } + } + + $supplierCounts = []; + foreach ($allOrderdetails as $orderdetail) { if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') { - $fieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#'; + // When explicit flags exist, filter by resolved visibility + $resolvedVisibility = $orderdetail->isEdaVisibility() ?? $this->kiCadEDASettings->defaultOrderdetailsVisibility; + if ($hasExplicitEdaVisibility && !$resolvedVisibility) { + continue; + } + + $supplierName = $orderdetail->getSupplier()->getName() . ' SPN'; + + if (!isset($supplierCounts[$supplierName])) { + $supplierCounts[$supplierName] = 0; + } + $supplierCounts[$supplierName]++; + + // Create field name with sequential number if more than one from same supplier + $fieldName = $supplierCounts[$supplierName] > 1 + ? $supplierName . ' ' . $supplierCounts[$supplierName] + : $supplierName; $result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr()); + + //Also add a KiCost-compatible field (supplier_name# = SPN) + $kicostFieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#'; + $result["fields"][$kicostFieldName] = $this->createField($orderdetail->getSupplierPartNr()); + } + } + } + + //Add stock quantity and storage locations (only count non-expired lots with known quantity) + $totalStock = 0; + $locations = []; + foreach ($part->getPartLots() as $lot) { + $isAvailable = !$lot->isInstockUnknown() && $lot->isExpired() !== true; + if ($isAvailable) { + $totalStock += $lot->getAmount(); + if ($lot->getAmount() > 0 && $lot->getStorageLocation() !== null) { + $locations[] = $lot->getStorageLocation()->getName(); + } + } + } + $result['fields']['Stock'] = $this->createField($totalStock); + if ($locations !== []) { + $result['fields']['Storage Location'] = $this->createField(implode(', ', array_unique($locations))); + } + + //Add parameters marked for EDA export (explicit true, or system default when null) + foreach ($part->getParameters() as $parameter) { + $paramVisibility = $parameter->isEdaVisibility() ?? $this->kiCadEDASettings->defaultParameterVisibility; + if ($paramVisibility && $parameter->getName() !== '') { + $fieldName = $parameter->getName(); + //Don't overwrite hardcoded fields + if (!isset($result['fields'][$fieldName])) { + $result['fields'][$fieldName] = $this->createField($parameter->getFormattedValue()); } } } @@ -340,7 +401,7 @@ class KiCadHelper //If the user set a visibility, then use it if ($eda_info->getVisibility() !== null) { - return $part->getEdaInfo()->getVisibility(); + return $eda_info->getVisibility(); } //If the part has a category, then use the category visibility if possible @@ -391,4 +452,64 @@ class KiCadHelper 'visible' => $this->boolToKicadBool($visible), ]; } -} \ No newline at end of file + + /** + * Finds the URL to the actual datasheet file for the given part. + * Searches attachments by type name, attachment name, and file extension. + * @return string|null The datasheet URL, or null if no datasheet was found. + */ + private function findDatasheetUrl(Part $part): ?string + { + $firstPdf = null; + + foreach ($part->getAttachments() as $attachment) { + //Check if the attachment type name contains "datasheet" + $typeName = $attachment->getAttachmentType()?->getName() ?? ''; + if (str_contains(mb_strtolower($typeName), 'datasheet')) { + return $this->getAttachmentUrl($attachment); + } + + //Check if the attachment name contains "datasheet" + $name = mb_strtolower($attachment->getName()); + if (str_contains($name, 'datasheet') || str_contains($name, 'data sheet')) { + return $this->getAttachmentUrl($attachment); + } + + //Track first PDF as fallback (check internal extension or external URL path) + if ($firstPdf === null) { + $extension = $attachment->getExtension(); + if ($extension === null && $attachment->hasExternal()) { + $urlPath = parse_url($attachment->getExternalPath(), PHP_URL_PATH); + $extension = is_string($urlPath) ? strtolower(pathinfo($urlPath, PATHINFO_EXTENSION)) : null; + } + if ($extension === 'pdf') { + $firstPdf = $attachment; + } + } + } + + //Use first PDF attachment as fallback + if ($firstPdf !== null) { + return $this->getAttachmentUrl($firstPdf); + } + + return null; + } + + /** + * Returns an absolute URL for viewing the given attachment. + * Prefers the external URL (direct link) over the internal view route. + */ + private function getAttachmentUrl(Attachment $attachment): string + { + if ($attachment->hasExternal()) { + return $attachment->getExternalPath(); + } + + return $this->urlGenerator->generate( + 'attachment_view', + ['id' => $attachment->getId()], + UrlGeneratorInterface::ABSOLUTE_URL + ); + } +} diff --git a/src/Services/EDA/KicadListFileManager.php b/src/Services/EDA/KicadListFileManager.php new file mode 100644 index 00000000..3d405026 --- /dev/null +++ b/src/Services/EDA/KicadListFileManager.php @@ -0,0 +1,158 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\EDA; + +use RuntimeException; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; + +/** + * Manages the KiCad footprints and symbols list files, including reading, writing and ensuring their existence. + */ +final class KicadListFileManager implements CacheWarmerInterface +{ + private const FOOTPRINTS_PATH = '/public/kicad/footprints.txt'; + private const SYMBOLS_PATH = '/public/kicad/symbols.txt'; + private const CUSTOM_FOOTPRINTS_PATH = '/public/kicad/footprints_custom.txt'; + private const CUSTOM_SYMBOLS_PATH = '/public/kicad/symbols_custom.txt'; + + private const CUSTOM_TEMPLATE = <<<'EOT' + # Custom KiCad autocomplete entries. One entry per line. + + EOT; + + public function __construct( + #[Autowire('%kernel.project_dir%')] + private readonly string $projectDir, + ) { + } + + public function getFootprintsContent(): string + { + return $this->readFile(self::FOOTPRINTS_PATH); + } + + public function getCustomFootprintsContent(): string + { + //Ensure that the custom file exists, so that the UI can always display it without error. + $this->createCustomFileIfNotExists(self::CUSTOM_FOOTPRINTS_PATH); + return $this->readFile(self::CUSTOM_FOOTPRINTS_PATH); + } + + public function getSymbolsContent(): string + { + return $this->readFile(self::SYMBOLS_PATH); + } + + public function getCustomSymbolsContent(): string + { + //Ensure that the custom file exists, so that the UI can always display it without error. + $this->createCustomFileIfNotExists(self::CUSTOM_SYMBOLS_PATH); + return $this->readFile(self::CUSTOM_SYMBOLS_PATH); + } + + public function saveCustom(string $footprints, string $symbols): void + { + $this->writeFile(self::CUSTOM_FOOTPRINTS_PATH, $this->normalizeContent($footprints)); + $this->writeFile(self::CUSTOM_SYMBOLS_PATH, $this->normalizeContent($symbols)); + } + + private function readFile(string $path): string + { + $fullPath = $this->projectDir . $path; + + if (!is_file($fullPath)) { + return ''; + } + + $content = file_get_contents($fullPath); + if ($content === false) { + throw new RuntimeException(sprintf('Failed to read KiCad list file "%s".', $fullPath)); + } + + return $content; + } + + private function writeFile(string $path, string $content): void + { + $fullPath = $this->projectDir . $path; + $tmpPath = $fullPath . '.tmp'; + + if (file_put_contents($tmpPath, $content, LOCK_EX) === false) { + throw new RuntimeException(sprintf('Failed to write KiCad list file "%s".', $fullPath)); + } + + if (!rename($tmpPath, $fullPath)) { + @unlink($tmpPath); + throw new RuntimeException(sprintf('Failed to replace KiCad list file "%s".', $fullPath)); + } + } + + private function normalizeContent(string $content): string + { + $normalized = str_replace(["\r\n", "\r"], "\n", $content); + + if ($normalized !== '' && !str_ends_with($normalized, "\n")) { + $normalized .= "\n"; + } + + return $normalized; + } + + private function createCustomFileIfNotExists(string $path): void + { + $fullPath = $this->projectDir . $path; + + if (!is_file($fullPath)) { + if (file_put_contents($fullPath, self::CUSTOM_TEMPLATE, LOCK_EX) === false) { + throw new RuntimeException(sprintf('Failed to create custom footprints file "%s".', $fullPath)); + } + } + } + + /** + * Ensures that the custom footprints and symbols files exist, so that the UI can always display them without error. + * @return void + */ + public function createCustomFilesIfNotExist(): void + { + $this->createCustomFileIfNotExists(self::CUSTOM_FOOTPRINTS_PATH); + $this->createCustomFileIfNotExists(self::CUSTOM_SYMBOLS_PATH); + } + + + public function isOptional(): bool + { + return false; + } + + /** + * Ensure that the custom footprints and symbols files exist and generate them on cache warmup, so that the frontend + * can always display them without error, even if the user has not yet visited the settings page. + */ + public function warmUp(string $cacheDir, ?string $buildDir = null): array + { + $this->createCustomFilesIfNotExist(); + return []; + } +} diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index 326707b7..19bb19f5 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -24,66 +24,31 @@ namespace App\Services; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; -use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\NamedElementInterface; -use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob; -use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart; -use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; -use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; -use App\Entity\Parts\PartAssociation; use App\Entity\Parts\PartLot; -use App\Entity\Parts\StorageLocation; -use App\Entity\Parts\Supplier; -use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Pricedetail; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Entity\UserSystem\Group; -use App\Entity\UserSystem\User; use App\Exceptions\EntityNotSupportedException; +use App\Settings\SynonymSettings; use Symfony\Contracts\Translation\TranslatorInterface; /** * @see \App\Tests\Services\ElementTypeNameGeneratorTest */ -class ElementTypeNameGenerator +final readonly class ElementTypeNameGenerator { - protected array $mapping; - public function __construct(protected TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator) + public function __construct( + private TranslatorInterface $translator, + private EntityURLGenerator $entityURLGenerator, + private SynonymSettings $synonymsSettings, + ) { - //Child classes has to become before parent classes - $this->mapping = [ - Attachment::class => $this->translator->trans('attachment.label'), - Category::class => $this->translator->trans('category.label'), - AttachmentType::class => $this->translator->trans('attachment_type.label'), - Project::class => $this->translator->trans('project.label'), - ProjectBOMEntry::class => $this->translator->trans('project_bom_entry.label'), - Footprint::class => $this->translator->trans('footprint.label'), - Manufacturer::class => $this->translator->trans('manufacturer.label'), - MeasurementUnit::class => $this->translator->trans('measurement_unit.label'), - Part::class => $this->translator->trans('part.label'), - PartLot::class => $this->translator->trans('part_lot.label'), - StorageLocation::class => $this->translator->trans('storelocation.label'), - Supplier::class => $this->translator->trans('supplier.label'), - Currency::class => $this->translator->trans('currency.label'), - Orderdetail::class => $this->translator->trans('orderdetail.label'), - Pricedetail::class => $this->translator->trans('pricedetail.label'), - Group::class => $this->translator->trans('group.label'), - User::class => $this->translator->trans('user.label'), - AbstractParameter::class => $this->translator->trans('parameter.label'), - LabelProfile::class => $this->translator->trans('label_profile.label'), - PartAssociation::class => $this->translator->trans('part_association.label'), - BulkInfoProviderImportJob::class => $this->translator->trans('bulk_info_provider_import_job.label'), - BulkInfoProviderImportJobPart::class => $this->translator->trans('bulk_info_provider_import_job_part.label'), - ]; } /** @@ -97,27 +62,69 @@ class ElementTypeNameGenerator * @return string the localized label for the entity type * * @throws EntityNotSupportedException when the passed entity is not supported + * @deprecated Use label() instead */ public function getLocalizedTypeLabel(object|string $entity): string { - $class = is_string($entity) ? $entity : $entity::class; - - //Check if we have a direct array entry for our entity class, then we can use it - if (isset($this->mapping[$class])) { - return $this->mapping[$class]; - } - - //Otherwise iterate over array and check for inheritance (needed when the proxy element from doctrine are passed) - foreach ($this->mapping as $class_to_check => $translation) { - if (is_a($entity, $class_to_check, true)) { - return $translation; - } - } - - //When nothing was found throw an exception - throw new EntityNotSupportedException(sprintf('No localized label for the element with type %s was found!', is_object($entity) ? $entity::class : (string) $entity)); + return $this->typeLabel($entity); } + private function resolveSynonymLabel(ElementTypes $type, ?string $locale, bool $plural): ?string + { + $locale ??= $this->translator->getLocale(); + + if ($this->synonymsSettings->isSynonymDefinedForType($type)) { + if ($plural) { + $syn = $this->synonymsSettings->getPluralSynonymForType($type, $locale); + } else { + $syn = $this->synonymsSettings->getSingularSynonymForType($type, $locale); + } + + if ($syn === null) { + //Try to fall back to english + if ($plural) { + $syn = $this->synonymsSettings->getPluralSynonymForType($type, 'en'); + } else { + $syn = $this->synonymsSettings->getSingularSynonymForType($type, 'en'); + } + } + + return $syn; + } + + return null; + } + + /** + * Gets a localized label for the type of the entity. If user defined synonyms are defined, + * these are used instead of the default labels. + * @param object|string $entity + * @param string|null $locale + * @return string + */ + public function typeLabel(object|string $entity, ?string $locale = null): string + { + $type = ElementTypes::fromValue($entity); + + return $this->resolveSynonymLabel($type, $locale, false) + ?? $this->translator->trans($type->getDefaultLabelKey(), locale: $locale); + } + + /** + * Similar to label(), but returns the plural version of the label. + * @param object|string $entity + * @param string|null $locale + * @return string + */ + public function typeLabelPlural(object|string $entity, ?string $locale = null): string + { + $type = ElementTypes::fromValue($entity); + + return $this->resolveSynonymLabel($type, $locale, true) + ?? $this->translator->trans($type->getDefaultPluralLabelKey(), locale: $locale); + } + + /** * Returns a string like in the format ElementType: ElementName. * For example this could be something like: "Part: BC547". @@ -132,7 +139,7 @@ class ElementTypeNameGenerator */ public function getTypeNameCombination(NamedElementInterface $entity, bool $use_html = false): string { - $type = $this->getLocalizedTypeLabel($entity); + $type = $this->typeLabel($entity); if ($use_html) { return '' . $type . ': ' . htmlspecialchars($entity->getName()); } @@ -142,7 +149,7 @@ class ElementTypeNameGenerator /** - * Returns a HTML formatted label for the given enitity in the format "Type: Name" (on elements with a name) and + * Returns a HTML formatted label for the given entity in the format "Type: Name" (on elements with a name) and * "Type: ID" (on elements without a name). If possible the value is given as a link to the element. * @param AbstractDBElement $entity The entity for which the label should be generated * @param bool $include_associated If set to true, the associated entity (like the part belonging to a part lot) is included in the label to give further information @@ -163,7 +170,7 @@ class ElementTypeNameGenerator } else { //Target does not have a name $tmp = sprintf( '%s: %s', - $this->getLocalizedTypeLabel($entity), + $this->typeLabel($entity), $entity->getID() ); } @@ -207,7 +214,7 @@ class ElementTypeNameGenerator { return sprintf( '%s: %s [%s]', - $this->getLocalizedTypeLabel($class), + $this->typeLabel($class), $id, $this->translator->trans('log.target_deleted') ); diff --git a/src/Services/ElementTypes.php b/src/Services/ElementTypes.php new file mode 100644 index 00000000..6ce8f851 --- /dev/null +++ b/src/Services/ElementTypes.php @@ -0,0 +1,229 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services; + +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentType; +use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob; +use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart; +use App\Entity\LabelSystem\LabelProfile; +use App\Entity\Parameters\AbstractParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Entity\UserSystem\Group; +use App\Entity\UserSystem\User; +use App\Exceptions\EntityNotSupportedException; +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum ElementTypes: string implements TranslatableInterface +{ + case ATTACHMENT = "attachment"; + case CATEGORY = "category"; + case ATTACHMENT_TYPE = "attachment_type"; + case PROJECT = "project"; + case PROJECT_BOM_ENTRY = "project_bom_entry"; + case FOOTPRINT = "footprint"; + case MANUFACTURER = "manufacturer"; + case MEASUREMENT_UNIT = "measurement_unit"; + case PART = "part"; + case PART_LOT = "part_lot"; + case STORAGE_LOCATION = "storage_location"; + case SUPPLIER = "supplier"; + case CURRENCY = "currency"; + case ORDERDETAIL = "orderdetail"; + case PRICEDETAIL = "pricedetail"; + case GROUP = "group"; + case USER = "user"; + case PARAMETER = "parameter"; + case LABEL_PROFILE = "label_profile"; + case PART_ASSOCIATION = "part_association"; + case BULK_INFO_PROVIDER_IMPORT_JOB = "bulk_info_provider_import_job"; + case BULK_INFO_PROVIDER_IMPORT_JOB_PART = "bulk_info_provider_import_job_part"; + case PART_CUSTOM_STATE = "part_custom_state"; + + //Child classes has to become before parent classes + private const CLASS_MAPPING = [ + Attachment::class => self::ATTACHMENT, + Category::class => self::CATEGORY, + AttachmentType::class => self::ATTACHMENT_TYPE, + Project::class => self::PROJECT, + ProjectBOMEntry::class => self::PROJECT_BOM_ENTRY, + Footprint::class => self::FOOTPRINT, + Manufacturer::class => self::MANUFACTURER, + MeasurementUnit::class => self::MEASUREMENT_UNIT, + Part::class => self::PART, + PartLot::class => self::PART_LOT, + StorageLocation::class => self::STORAGE_LOCATION, + Supplier::class => self::SUPPLIER, + Currency::class => self::CURRENCY, + Orderdetail::class => self::ORDERDETAIL, + Pricedetail::class => self::PRICEDETAIL, + Group::class => self::GROUP, + User::class => self::USER, + AbstractParameter::class => self::PARAMETER, + LabelProfile::class => self::LABEL_PROFILE, + PartAssociation::class => self::PART_ASSOCIATION, + BulkInfoProviderImportJob::class => self::BULK_INFO_PROVIDER_IMPORT_JOB, + BulkInfoProviderImportJobPart::class => self::BULK_INFO_PROVIDER_IMPORT_JOB_PART, + PartCustomState::class => self::PART_CUSTOM_STATE, + ]; + + /** + * Gets the default translation key for the label of the element type (singular form). + */ + public function getDefaultLabelKey(): string + { + return match ($this) { + self::ATTACHMENT => 'attachment.label', + self::CATEGORY => 'category.label', + self::ATTACHMENT_TYPE => 'attachment_type.label', + self::PROJECT => 'project.label', + self::PROJECT_BOM_ENTRY => 'project_bom_entry.label', + self::FOOTPRINT => 'footprint.label', + self::MANUFACTURER => 'manufacturer.label', + self::MEASUREMENT_UNIT => 'measurement_unit.label', + self::PART => 'part.label', + self::PART_LOT => 'part_lot.label', + self::STORAGE_LOCATION => 'storelocation.label', + self::SUPPLIER => 'supplier.label', + self::CURRENCY => 'currency.label', + self::ORDERDETAIL => 'orderdetail.label', + self::PRICEDETAIL => 'pricedetail.label', + self::GROUP => 'group.label', + self::USER => 'user.label', + self::PARAMETER => 'parameter.label', + self::LABEL_PROFILE => 'label_profile.label', + self::PART_ASSOCIATION => 'part_association.label', + self::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.label', + self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => 'bulk_info_provider_import_job_part.label', + self::PART_CUSTOM_STATE => 'part_custom_state.label', + }; + } + + public function getDefaultPluralLabelKey(): string + { + return match ($this) { + self::ATTACHMENT => 'attachment.labelp', + self::CATEGORY => 'category.labelp', + self::ATTACHMENT_TYPE => 'attachment_type.labelp', + self::PROJECT => 'project.labelp', + self::PROJECT_BOM_ENTRY => 'project_bom_entry.labelp', + self::FOOTPRINT => 'footprint.labelp', + self::MANUFACTURER => 'manufacturer.labelp', + self::MEASUREMENT_UNIT => 'measurement_unit.labelp', + self::PART => 'part.labelp', + self::PART_LOT => 'part_lot.labelp', + self::STORAGE_LOCATION => 'storelocation.labelp', + self::SUPPLIER => 'supplier.labelp', + self::CURRENCY => 'currency.labelp', + self::ORDERDETAIL => 'orderdetail.labelp', + self::PRICEDETAIL => 'pricedetail.labelp', + self::GROUP => 'group.labelp', + self::USER => 'user.labelp', + self::PARAMETER => 'parameter.labelp', + self::LABEL_PROFILE => 'label_profile.labelp', + self::PART_ASSOCIATION => 'part_association.labelp', + self::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.labelp', + self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => 'bulk_info_provider_import_job_part.labelp', + self::PART_CUSTOM_STATE => 'part_custom_state.labelp', + }; + } + + /** + * Used to get a user-friendly representation of the object that can be translated. + * For this the singular default label key is used. + * @param TranslatorInterface $translator + * @param string|null $locale + * @return string + */ + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return $translator->trans($this->getDefaultLabelKey(), locale: $locale); + } + + /** + * Determines the ElementType from a value, which can either be an enum value, an ElementTypes instance, a class name or an object instance. + * @param string|object $value + * @return self + */ + public static function fromValue(string|object $value): self + { + if ($value instanceof self) { + return $value; + } + if (is_object($value)) { + return self::fromClass($value); + } + + + //Otherwise try to parse it as enum value first + $enumValue = self::tryFrom($value); + + //Otherwise try to get it from class name + return $enumValue ?? self::fromClass($value); + } + + /** + * Determines the ElementType from a class name or object instance. + * @param string|object $class + * @throws EntityNotSupportedException if the class is not supported + * @return self + */ + public static function fromClass(string|object $class): self + { + if (is_object($class)) { + $className = get_class($class); + } else { + $className = $class; + } + + if (array_key_exists($className, self::CLASS_MAPPING)) { + return self::CLASS_MAPPING[$className]; + } + + //Otherwise we need to check for inheritance + foreach (self::CLASS_MAPPING as $entityClass => $elementType) { + if (is_a($className, $entityClass, true)) { + return $elementType; + } + } + + throw new EntityNotSupportedException(sprintf('No localized label for the element with type %s was found!', $className)); + } + +} diff --git a/src/Services/EntityMergers/EntityMerger.php b/src/Services/EntityMergers/EntityMerger.php index c0be84ee..f8cf8a11 100644 --- a/src/Services/EntityMergers/EntityMerger.php +++ b/src/Services/EntityMergers/EntityMerger.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace App\Services\EntityMergers; use App\Services\EntityMergers\Mergers\EntityMergerInterface; -use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; /** * This service is used to merge two entities together. @@ -32,7 +32,7 @@ use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; */ class EntityMerger { - public function __construct(#[TaggedIterator('app.entity_merger')] protected iterable $mergers) + public function __construct(#[AutowireIterator('app.entity_merger')] protected iterable $mergers) { } @@ -73,4 +73,4 @@ class EntityMerger } return $merger->merge($target, $other, $context); } -} \ No newline at end of file +} diff --git a/src/Services/EntityMergers/Mergers/PartMerger.php b/src/Services/EntityMergers/Mergers/PartMerger.php index 01b53e25..8397257e 100644 --- a/src/Services/EntityMergers/Mergers/PartMerger.php +++ b/src/Services/EntityMergers/Mergers/PartMerger.php @@ -59,12 +59,14 @@ class PartMerger implements EntityMergerInterface $this->useOtherValueIfNotEmtpy($target, $other, 'manufacturer_product_number'); $this->useOtherValueIfNotEmtpy($target, $other, 'mass'); $this->useOtherValueIfNotEmtpy($target, $other, 'ipn'); + $this->useOtherValueIfNotEmtpy($target, $other, 'gtin'); //Merge relations to other entities $this->useOtherValueIfNotNull($target, $other, 'manufacturer'); $this->useOtherValueIfNotNull($target, $other, 'footprint'); $this->useOtherValueIfNotNull($target, $other, 'category'); $this->useOtherValueIfNotNull($target, $other, 'partUnit'); + $this->useOtherValueIfNotNull($target, $other, 'partCustomState'); //We assume that the higher value is the correct one for minimum instock $this->useLargerValue($target, $other, 'minamount'); @@ -183,4 +185,4 @@ class PartMerger implements EntityMergerInterface } } } -} \ No newline at end of file +} diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 4a2d6425..b4343d73 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -27,6 +27,7 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\PartAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Parameters\PartParameter; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -108,6 +109,7 @@ class EntityURLGenerator MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; try { @@ -219,6 +221,7 @@ class EntityURLGenerator MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -249,6 +252,7 @@ class EntityURLGenerator MeasurementUnit::class => 'measurement_unit_edit', Group::class => 'group_edit', LabelProfile::class => 'label_profile_edit', + PartCustomState::class => 'part_custom_state_edit', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -280,6 +284,7 @@ class EntityURLGenerator MeasurementUnit::class => 'measurement_unit_new', Group::class => 'group_new', LabelProfile::class => 'label_profile_new', + PartCustomState::class => 'part_custom_state_new', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity)); @@ -311,6 +316,7 @@ class EntityURLGenerator MeasurementUnit::class => 'measurement_unit_clone', Group::class => 'group_clone', LabelProfile::class => 'label_profile_clone', + PartCustomState::class => 'part_custom_state_clone', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -356,6 +362,7 @@ class EntityURLGenerator MeasurementUnit::class => 'measurement_unit_delete', Group::class => 'group_delete', LabelProfile::class => 'label_profile_delete', + PartCustomState::class => 'part_custom_state_delete', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); diff --git a/src/Services/Formatters/SIFormatter.php b/src/Services/Formatters/SIFormatter.php index b83501fa..1f25dbe6 100644 --- a/src/Services/Formatters/SIFormatter.php +++ b/src/Services/Formatters/SIFormatter.php @@ -59,10 +59,10 @@ class SIFormatter $prefixes_neg = ['', 'm', 'μ', 'n', 'p', 'f', 'a', 'z', 'y']; if ($magnitude >= 0) { - $nearest = (int) floor(abs($magnitude) / 3); + $nearest = min((int) floor(abs($magnitude) / 3), count($prefixes_pos) - 1); $symbol = $prefixes_pos[$nearest]; } else { - $nearest = (int) round(abs($magnitude) / 3); + $nearest = min((int) round(abs($magnitude) / 3), count($prefixes_neg) - 1); $symbol = $prefixes_neg[$nearest]; } diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 862fa463..7cef3f81 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -22,6 +22,8 @@ declare(strict_types=1); */ namespace App\Services\ImportExportSystem; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Orderdetail; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; @@ -134,7 +136,7 @@ class BOMImporter private function parseKiCADPCB(string $data): array { - $csv = Reader::createFromString($data); + $csv = Reader::fromString($data); $csv->setDelimiter(';'); $csv->setHeaderOffset(0); @@ -175,7 +177,7 @@ class BOMImporter */ private function validateKiCADPCB(string $data): array { - $csv = Reader::createFromString($data); + $csv = Reader::fromString($data); $csv->setDelimiter(';'); $csv->setHeaderOffset(0); @@ -202,7 +204,7 @@ class BOMImporter // Handle potential BOM (Byte Order Mark) at the beginning $data = preg_replace('/^\xEF\xBB\xBF/', '', $data); - $csv = Reader::createFromString($data); + $csv = Reader::fromString($data); $csv->setDelimiter($delimiter); $csv->setHeaderOffset(0); @@ -262,7 +264,7 @@ class BOMImporter // Handle potential BOM (Byte Order Mark) at the beginning $data = preg_replace('/^\xEF\xBB\xBF/', '', $data); - $csv = Reader::createFromString($data); + $csv = Reader::fromString($data); $csv->setDelimiter($delimiter); $csv->setHeaderOffset(0); @@ -274,6 +276,16 @@ class BOMImporter $entries_by_key = []; // Track entries by name+part combination $mapped_entries = []; // Collect all mapped entries for validation + // Fetch suppliers once for efficiency + $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll(); + $supplierSPNKeys = []; + $suppliersByName = []; // Map supplier names to supplier objects + foreach ($suppliers as $supplier) { + $supplierName = $supplier->getName(); + $supplierSPNKeys[] = $supplierName . ' SPN'; + $suppliersByName[$supplierName] = $supplier; + } + foreach ($csv->getRecords() as $offset => $entry) { // Apply field mapping to translate column names $mapped_entry = $this->applyFieldMapping($entry, $field_mapping, $field_priorities); @@ -349,10 +361,49 @@ class BOMImporter } } - // Create unique key for this entry (name + part ID) - $entry_key = $name . '|' . ($part ? $part->getID() : 'null'); + // Try to link existing part based on supplier part number if no Part-DB ID is given + if ($part === null) { + // Check all available supplier SPN fields + foreach ($suppliersByName as $supplierName => $supplier) { + $supplier_spn = null; - // Check if we already have an entry with the same name and part + if (isset($mapped_entry[$supplierName . ' SPN']) && !empty(trim($mapped_entry[$supplierName . ' SPN']))) { + $supplier_spn = trim($mapped_entry[$supplierName . ' SPN']); + } + + if ($supplier_spn !== null) { + // Query for orderdetails with matching supplier and SPN + $orderdetail = $this->entityManager->getRepository(Orderdetail::class) + ->findOneBy([ + 'supplier' => $supplier, + 'supplierpartnr' => $supplier_spn, + ]); + + if ($orderdetail !== null && $orderdetail->getPart() !== null) { + $part = $orderdetail->getPart(); + $name = $part->getName(); // Update name with actual part name + + $this->logger->info('Linked BOM entry to existing part via supplier SPN', [ + 'supplier' => $supplierName, + 'supplier_spn' => $supplier_spn, + 'part_id' => $part->getID(), + 'part_name' => $part->getName(), + ]); + + break; // Stop searching once a match is found + } + } + } + } + + // Create unique key for this entry. + // When linked to a Part-DB part, use the part ID as key (merges footprint variants). + // Otherwise, use name (which includes package) to avoid merging unrelated components. + $entry_key = $part !== null + ? 'part:' . $part->getID() + : 'name:' . $name; + + // Check if we already have an entry with the same key if (isset($entries_by_key[$entry_key])) { // Merge with existing entry $existing_entry = $entries_by_key[$entry_key]; @@ -366,14 +417,22 @@ class BOMImporter $existing_quantity = $existing_entry->getQuantity(); $existing_entry->setQuantity($existing_quantity + $quantity); + // Track footprint variants in comment when merging entries with different packages + $currentPackage = trim($mapped_entry['Package'] ?? ''); + if ($currentPackage !== '' && !str_contains($existing_entry->getComment(), $currentPackage)) { + $comment = $existing_entry->getComment(); + $existing_entry->setComment($comment . ', Footprint variant: ' . $currentPackage); + } + $this->logger->info('Merged duplicate BOM entry', [ 'name' => $name, - 'part_id' => $part ? $part->getID() : null, + 'part_id' => $part?->getID(), 'original_quantity' => $existing_quantity, 'added_quantity' => $quantity, 'new_quantity' => $existing_quantity + $quantity, 'original_mountnames' => $existing_mountnames, 'added_mountnames' => $designator, + 'package' => $currentPackage, ]); continue; // Skip creating new entry @@ -400,9 +459,14 @@ class BOMImporter if (isset($mapped_entry['Manufacturer'])) { $comment_parts[] = 'Manf: ' . $mapped_entry['Manufacturer']; } - if (isset($mapped_entry['LCSC'])) { - $comment_parts[] = 'LCSC: ' . $mapped_entry['LCSC']; + + // Add supplier part numbers dynamically + foreach ($supplierSPNKeys as $spnKey) { + if (isset($mapped_entry[$spnKey]) && !empty($mapped_entry[$spnKey])) { + $comment_parts[] = $spnKey . ': ' . $mapped_entry[$spnKey]; + } } + if (isset($mapped_entry['Supplier and ref'])) { $comment_parts[] = $mapped_entry['Supplier and ref']; } @@ -485,7 +549,7 @@ class BOMImporter ]; // Add dynamic supplier fields based on available suppliers in the database - $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); + $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll(); foreach ($suppliers as $supplier) { $supplierName = $supplier->getName(); $targets[$supplierName . ' SPN'] = [ @@ -520,7 +584,7 @@ class BOMImporter ]; // Add supplier-specific patterns - $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); + $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll(); foreach ($suppliers as $supplier) { $supplierName = $supplier->getName(); $supplierLower = strtolower($supplierName); @@ -657,26 +721,36 @@ class BOMImporter return $mapped; } + /** + * Try to detect the separator used in the CSV data by analyzing the first line and counting occurrences of common delimiters. + * @param string $data + * @return string + */ + public function detectDelimiter(string $data): string + { + $delimiters = [',', ';', "\t"]; + $lines = explode("\n", $data, 2); + $header_line = $lines[0] ?? ''; + $delimiter_counts = []; + foreach ($delimiters as $delim) { + $delimiter_counts[$delim] = substr_count($header_line, $delim); + } + // Choose the delimiter with the highest count, default to comma if all are zero + $max_count = max($delimiter_counts); + $delimiter = array_search($max_count, $delimiter_counts, true); + if ($max_count === 0 || $delimiter === false) { + $delimiter = ','; + } + return $delimiter; + } + /** * Detect available fields in CSV data for field mapping UI */ public function detectFields(string $data, ?string $delimiter = null): array { if ($delimiter === null) { - // Detect delimiter by counting occurrences in the first row (header) - $delimiters = [',', ';', "\t"]; - $lines = explode("\n", $data, 2); - $header_line = $lines[0] ?? ''; - $delimiter_counts = []; - foreach ($delimiters as $delim) { - $delimiter_counts[$delim] = substr_count($header_line, $delim); - } - // Choose the delimiter with the highest count, default to comma if all are zero - $max_count = max($delimiter_counts); - $delimiter = array_search($max_count, $delimiter_counts, true); - if ($max_count === 0 || $delimiter === false) { - $delimiter = ','; - } + $delimiter = $this->detectDelimiter($data); } // Handle potential BOM (Byte Order Mark) at the beginning $data = preg_replace('/^\xEF\xBB\xBF/', '', $data); diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 70feb8e6..ab87a905 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\ImportExportSystem; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Helpers\FilenameSanatizer; @@ -177,7 +178,7 @@ class EntityExporter $colIndex = 1; foreach ($columns as $column) { - $cellCoordinate = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex) . $rowIndex; + $cellCoordinate = Coordinate::stringFromColumnIndex($colIndex) . $rowIndex; $worksheet->setCellValue($cellCoordinate, $column); $colIndex++; } @@ -265,11 +266,14 @@ class EntityExporter //Sanitize the filename $filename = FilenameSanatizer::sanitizeFilename($filename); + //Remove percent for fallback + $fallback = str_replace("%", "_", $filename); + // Create the disposition of the file $disposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename, - u($filename)->ascii()->toString(), + $fallback, ); // Set the content disposition $response->headers->set('Content-Disposition', $disposition); diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php index 459866ba..c33d6e6a 100644 --- a/src/Services/ImportExportSystem/EntityImporter.php +++ b/src/Services/ImportExportSystem/EntityImporter.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\ImportExportSystem; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parts\Category; @@ -167,7 +168,7 @@ class EntityImporter } //Only return objects once - return array_values(array_unique($valid_entities)); + return array_values(array_unique($valid_entities, SORT_REGULAR)); } /** @@ -218,11 +219,6 @@ class EntityImporter $entities = [$entities]; } - //The serializer has only set the children attributes. We also have to change the parent value (the real value in DB) - if ($entities[0] instanceof AbstractStructuralDBElement) { - $this->correctParentEntites($entities, null); - } - //Set the parent of the imported elements to the given options foreach ($entities as $entity) { if ($entity instanceof AbstractStructuralDBElement) { @@ -296,6 +292,14 @@ class EntityImporter return $resolver; } + private function persistRecursively(AbstractStructuralDBElement $entity): void + { + $this->em->persist($entity); + foreach ($entity->getChildren() as $child) { + $this->persistRecursively($child); + } + } + /** * This method deserializes the given file and writes the entities to the database (and flush the db). * The imported elements will be checked (validated) before written to database. @@ -321,7 +325,11 @@ class EntityImporter //Iterate over each $entity write it to DB (the invalid entities were already filtered out). foreach ($entities as $entity) { - $this->em->persist($entity); + if ($entity instanceof AbstractStructuralDBElement) { + $this->persistRecursively($entity); + } else { + $this->em->persist($entity); + } } //Save changes to database, when no error happened, or we should continue on error. @@ -399,7 +407,7 @@ class EntityImporter * * @param File $file The Excel file to convert * @param string $delimiter The CSV delimiter to use - * + * * @return string The CSV data as string */ protected function convertExcelToCsv(File $file, string $delimiter = ';'): string @@ -419,18 +427,18 @@ class EntityImporter 'worksheet_title' => $worksheet->getTitle() ]); - $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); - + $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); + for ($row = 1; $row <= $highestRow; $row++) { $rowData = []; // Read all columns using numeric index for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) { - $col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex); + $col = Coordinate::stringFromColumnIndex($colIndex); try { $cellValue = $worksheet->getCell("{$col}{$row}")->getCalculatedValue(); $rowData[] = $cellValue ?? ''; - + } catch (\Exception $e) { $this->logger->warning('Error reading cell value', [ 'cell' => "{$col}{$row}", @@ -483,21 +491,4 @@ class EntityImporter throw $e; } } - - - /** - * This functions corrects the parent setting based on the children value of the parent. - * - * @param iterable $entities the list of entities that should be fixed - * @param AbstractStructuralDBElement|null $parent the parent, to which the entity should be set - */ - protected function correctParentEntites(iterable $entities, ?AbstractStructuralDBElement $parent = null): void - { - foreach ($entities as $entity) { - /** @var AbstractStructuralDBElement $entity */ - $entity->setParent($parent); - //Do the same for the children of entity - $this->correctParentEntites($entity->getChildren(), $entity); - } - } } diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php index 1f842c23..ec23d34b 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php @@ -29,6 +29,7 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use Doctrine\ORM\EntityManagerInterface; @@ -148,6 +149,26 @@ class PKDatastructureImporter return is_countable($partunit_data) ? count($partunit_data) : 0; } + public function importPartCustomStates(array $data): int + { + if (!isset($data['partcustomstate'])) { + return 0; //Not all PartKeepr installations have custom states + } + + $partCustomStateData = $data['partcustomstate']; + foreach ($partCustomStateData as $partCustomState) { + $customState = new PartCustomState(); + $customState->setName($partCustomState['name']); + + $this->setIDOfEntity($customState, $partCustomState['id']); + $this->em->persist($customState); + } + + $this->em->flush(); + + return is_countable($partCustomStateData) ? count($partCustomStateData) : 0; + } + public function importCategories(array $data): int { if (!isset($data['partcategory'])) { diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php index f36e48ce..880d77be 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php @@ -39,10 +39,10 @@ class PKImportHelper * Existing users and groups are not purged. * This is needed to avoid ID collisions. */ - public function purgeDatabaseForImport(): void + public function purgeDatabaseForImport(?EntityManagerInterface $entityManager = null, array $excluded_tables = ['users', 'groups', 'u2f_keys', 'internal', 'migration_versions']): void { //We use the ResetAutoIncrementORMPurger to reset the auto increment values of the tables. Also it normalizes table names before checking for exclusion. - $purger = new ResetAutoIncrementORMPurger($this->em, ['users', 'groups', 'u2f_keys', 'internal', 'migration_versions']); + $purger = new ResetAutoIncrementORMPurger($entityManager ?? $this->em, $excluded_tables); $purger->purge(); } diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php index 1e4cd3ba..08b1c301 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php @@ -89,7 +89,7 @@ trait PKImportHelperTrait //Use mime type to determine the extension like PartKeepr does in legacy implementation (just use the second part of the mime type) //See UploadedFile.php:291 in PartKeepr (https://github.com/partkeepr/PartKeepr/blob/f6176c3354b24fa39ac8bc4328ee0df91de3d5b6/src/PartKeepr/UploadedFileBundle/Entity/UploadedFile.php#L291) if (!empty ($attachment_row['mimetype'])) { - $attachment_row['extension'] = explode('/', (string) $attachment_row['mimetype'])[1]; + $attachment_row['extension'] = explode('/', (string) $attachment_row['mimetype'])[1] ?? ''; } else { //If the mime type is empty, we use the original extension $attachment_row['extension'] = pathinfo((string) $attachment_row['originalname'], PATHINFO_EXTENSION); @@ -150,6 +150,11 @@ trait PKImportHelperTrait $target->addAttachment($attachment); $this->em->persist($attachment); + + //If the attachment is an image, and the target has no master picture yet, set it + if ($attachment->isPicture() && $target->getMasterPictureAttachment() === null) { + $target->setMasterPictureAttachment($attachment); + } } $this->em->flush(); diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php index 80c2dbf7..e63ec7f1 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php @@ -91,6 +91,11 @@ class PKPartImporter $this->setAssociationField($entity, 'partUnit', MeasurementUnit::class, $part['partUnit_id']); } + if (isset($part['partCustomState_id'])) { + $this->setAssociationField($entity, 'partCustomState', MeasurementUnit::class, + $part['partCustomState_id']); + } + //Create a part lot to store the stock level and location $lot = new PartLot(); $lot->setAmount((float) ($part['stockLevel'] ?? 0)); @@ -281,7 +286,7 @@ class PKPartImporter //Partkeepr stores the price per item, we need to convert it to the price per packaging unit $price_per_item = BigDecimal::of($partdistributor['price']); $packaging_unit = (float) ($partdistributor['packagingUnit'] ?? 1); - $pricedetail->setPrice($price_per_item->multipliedBy($packaging_unit)); + $pricedetail->setPrice($price_per_item->multipliedBy(BigDecimal::fromFloatShortest($packaging_unit))); $pricedetail->setPriceRelatedQuantity($packaging_unit); //We have to set the minimum discount quantity to the packaging unit (PartKeepr does not know this concept) //But in Part-DB the minimum discount qty have to be unique across a orderdetail diff --git a/src/Services/InfoProviderSystem/BulkInfoProviderService.php b/src/Services/InfoProviderSystem/BulkInfoProviderService.php index 586fb873..79420134 100644 --- a/src/Services/InfoProviderSystem/BulkInfoProviderService.php +++ b/src/Services/InfoProviderSystem/BulkInfoProviderService.php @@ -46,7 +46,6 @@ final class BulkInfoProviderService } $partResults = []; - $hasAnyResults = false; // Group providers by batch capability $batchProviders = []; @@ -88,7 +87,6 @@ final class BulkInfoProviderService ); if (!empty($allResults)) { - $hasAnyResults = true; $searchResults = $this->formatSearchResults($allResults); } @@ -99,10 +97,6 @@ final class BulkInfoProviderService ); } - if (!$hasAnyResults) { - throw new \RuntimeException('No search results found for any of the selected parts'); - } - $response = new BulkSearchResponseDTO($partResults); // Prefetch details if requested diff --git a/src/Services/InfoProviderSystem/CreateFromUrlHelper.php b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php new file mode 100644 index 00000000..0291142f --- /dev/null +++ b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php @@ -0,0 +1,109 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem; + +use App\Entity\UserSystem\User; +use App\Exceptions\ProviderIDNotSupportedException; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use Symfony\Bundle\SecurityBundle\Security; + +final readonly class CreateFromUrlHelper +{ + public function __construct(private Security $security, + private ProviderRegistry $providerRegistry, + private PartInfoRetriever $infoRetriever, + ) + { + } + + /** + * Checks if at least one provider can create parts from an URL and the current user is allowed to use it. + * This is used to determine if the "From URL" feature should be shown to the user. + * @return bool + */ + public function canCreateFromUrl(): bool + { + if (!$this->security->isGranted('@info_providers.create_parts')) { + return false; + } + + //Check if either the generic web provider or the ai web provider is active + $genericWebProvider = $this->providerRegistry->getProviderByKey('generic_web'); + $aiWebProvider = $this->providerRegistry->getProviderByKey('ai_web'); + + return $genericWebProvider->isActive() || $aiWebProvider->isActive(); + } + + /** + * Delegates the URL to another provider if possible, otherwise return null + * @param string $url + * @return SearchResultDTO|null + */ + public function delegateToOtherProvider(string $url, InfoProviderInterface $callingInfoProvider): ?SearchResultDTO + { + //Extract domain from url: + $host = parse_url($url, PHP_URL_HOST); + if ($host === false || $host === null) { + return null; + } + + $provider = $this->providerRegistry->getProviderHandlingDomain($host); + + if ($provider !== null && $provider->isActive() && $provider->getProviderKey() !== $callingInfoProvider->getProviderKey()) { + try { + $id = $provider->getIDFromURL($url); + if ($id !== null) { + $results = $this->infoRetriever->searchByKeyword($id, [$provider]); + if (count($results) > 0) { + return $results[0]; + } + } + return null; + } catch (ProviderIDNotSupportedException $e) { + //Ignore and continue + return null; + } + } + + return null; + } + + /** + * Delegates the URL to another provider if possible and returns the details, otherwise return null + * @param string $url + * @param InfoProviderInterface $callingInfoProvider + * @return PartDetailDTO|null + */ + public function delegateToOtherProviderDetails(string $url, InfoProviderInterface $callingInfoProvider): ?PartDetailDTO + { + $delegatedResult = $this->delegateToOtherProvider($url, $callingInfoProvider); + if ($delegatedResult !== null) { + return $this->infoRetriever->getDetailsForSearchResult($delegatedResult); + } + + return null; + } +} diff --git a/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php b/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php new file mode 100644 index 00000000..a61e7465 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php @@ -0,0 +1,252 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; + +/** + * This class allows to convert the JSON data returned by an LLM into the DTOs used by the info provider system later. + */ +final class DTOJsonSchemaConverter +{ + /** + * Returns the JSON schema, that defines the expected structure of the JSON data returned by the LLM. + * @return array + */ + public function getJSONSchema(): array + { + return [ + 'name' => 'clock', + 'strict' => true, + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string', 'description' => 'Product name'], + 'description' => ['type' => 'string', 'description' => 'A short description of the product, maybe containing the most important things. Onnly One line.'], + 'manufacturer' => ['type' => ['string', 'null'], 'description' => 'Manufacturer name'], + 'mpn' => ['type' => ['string', 'null'], 'description' => 'Manufacturer Part Number'], + 'category' => ['type' => ['string', 'null'], 'description' => 'Product category, e.g. "Passive components -> Resistors"'], + 'manufacturing_status' => ['type' => ['string', 'null'], 'enum' => ['active', 'obsolete', 'nrfnd', 'discontinued', null], 'description' => 'Manufacturing status'], + 'footprint' => ['type' => ['string', 'null'], 'description' => 'Package/footprint type, like "SOT-23", "DIP-8", "QFN-32" etc.'], + 'mass' => ['type' => ['number', 'null'], 'description' => 'Mass of the product in grams'], + 'gtin' => ['type' => ['string', 'null'], 'description' => 'Global Trade Item Number (GTIN) / EAN / UPC code for barcodes'], + 'notes' => ['type' => ['string', 'null'], 'description' => 'Optional long description of the part with more details than description. Can be markdown formatted.'], + 'parameters' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'symbol' => ['type' => ['string', 'null'], 'description' => 'An optional quantity symbol for the parameter in latex code, like R_1'], + 'value_typical' => ['type' => ['number', 'null'], 'description' => 'The typical value of the parameter. For example, for a resistor this could be 100 for a 100 Ohm resistor. Also used if only one numeric value is given. If used an unit should be given'], + 'value_min' => ['type' => ['number', 'null'], 'description' => 'If a range is given for the parameter, this is the minimum value. Null if no range is given.'], + 'value_max' => ['type' => ['number', 'null'], 'description' => 'If a range is given for the parameter, this is the maximum value. Null if not a range.'], + 'value_text' => ['type' => ['string', 'null'], 'description' => 'When a value is not numeric it can be put here as text. Only use if it does not fit in value_min, value_typical or value_max. E.g. "Yes", "Red", etc.'], + 'group' => ['type' => ['string', 'null'], 'description' => 'An optional group name for the parameter, e.g. "Electrical parameters", "Mechanical parameters" etc.'], + 'unit' => ['type' => ['string', 'null'], 'description' => 'The unit of the parameter values, e.g. kg, Ohm, V, etc.'], + ], + 'required' => ['name', 'value_typical', 'value_min', 'value_max', 'value_text'] + ], + ], + 'datasheets' => [ + 'description' => 'A list of datasheets, manuals, or other technical documents related to the product. Not images, but actual documents, preferably PDFs.', + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'url' => ['type' => 'string'], + 'description' => ['type' => 'string'], + ], + 'required' => ['url'], + ], + ], + 'images' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'url' => ['type' => 'string'], + 'description' => ['type' => 'string'], + ], + 'required' => ['url'], + ], + ], + 'vendor_infos' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'distributor_name' => ['type' => 'string', 'description' => 'Name of the distributor or vendor. Typically the shop name'], + 'order_number' => ['type' => ['string', 'null'], 'description' => 'The order number or SKU used by the distributor. Optional, but can help to find the product on the distributor website.'], + 'product_url' => ['type' => 'string'], + 'prices_include_vat' => ['type' => ['boolean', 'null'], 'description' => 'Whether the prices include VAT or not. Null if unknown.'], + 'prices' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'minimum_quantity' => ['type' => 'integer', 'description' => 'Minimum quantity for this price tier. 1 when no tiered pricing is available.'], + 'price' => ['type' => 'number', 'description' => 'Price for the given minimum quantity.'], + 'currency' => ['type' => 'string', 'description' => 'Currency ISO code, e.g. USD'], + ], + 'required' => ['minimum_quantity', 'price', 'currency'], + ], + ], + ], + 'required' => ['distributor_name', 'product_url'], + ], + ], + 'manufacturer_product_url' => ['type' => ['string', 'null'], 'description' => 'Manufacturer product page URL'], + ], + 'required' => ['name', 'description'], + ] + ]; + } + + public function jsonToDTO(array $data, string $providerKey, string $providerId, ?string $productUrl = null, string $distributorNameFallback = '???'): PartDetailDTO + { + // Map manufacturing status + $manufacturingStatus = null; + if (!empty($data['manufacturing_status'])) { + $status = strtolower((string) $data['manufacturing_status']); + $manufacturingStatus = match ($status) { + 'active' => ManufacturingStatus::ACTIVE, + 'obsolete', 'discontinued' => ManufacturingStatus::DISCONTINUED, + 'nrfnd', 'not recommended for new designs' => ManufacturingStatus::NRFND, + 'eol' => ManufacturingStatus::EOL, + 'announced' => ManufacturingStatus::ANNOUNCED, + default => null, + }; + } + + // Build parameters + $parameters = null; + if (!empty($data['parameters']) && is_array($data['parameters'])) { + $parameters = []; + foreach ($data['parameters'] as $p) { + if (!empty($p['name'])) { + $parameters[] = new ParameterDTO( + name: $p['name'], + value_text: $p['value_text'] ?? null, + value_typ: isset($p['value_typical']) && is_numeric($p['value_typical']) ? (float) $p['value_typical'] : null, + value_min: isset($p['value_min']) && is_numeric($p['value_min']) ? (float) $p['value_min'] : null, + value_max: isset($p['value_max']) && is_numeric($p['value_max']) ? (float) $p['value_max'] : null, + unit: $p['unit'] ?? null, + symbol: $p['symbol'] ?? null, + group: $p['group'] ?? null, + ); + } + } + } + + // Build datasheets + $datasheets = null; + if (!empty($data['datasheets']) && is_array($data['datasheets'])) { + $datasheets = []; + foreach ($data['datasheets'] as $d) { + if (!empty($d['url'])) { + $datasheets[] = new FileDTO( + url: $d['url'], + name: $d['description'] ?? 'Datasheet' + ); + } + } + } + + // Build images + $images = null; + if (!empty($data['images']) && is_array($data['images'])) { + $images = []; + foreach ($data['images'] as $i) { + if (!empty($i['url'])) { + $images[] = new FileDTO( + url: $i['url'], + name: $i['description'] ?? 'Image' + ); + } + } + } + + // Build vendor infos + $vendorInfos = null; + if (!empty($data['vendor_infos']) && is_array($data['vendor_infos'])) { + $vendorInfos = []; + foreach ($data['vendor_infos'] as $v) { + $prices = []; + if (!empty($v['prices']) && is_array($v['prices'])) { + foreach ($v['prices'] as $p) { + $prices[] = new PriceDTO( + minimum_discount_amount: (int) ($p['minimum_quantity'] ?? 1), + price: (string) ($p['price'] ?? 0), + currency_iso_code: $p['currency'] ?? null, + price_related_quantity: 1, + ); + } + } + + $vendorInfos[] = new PurchaseInfoDTO( + distributor_name: $v['distributor_name'] ?? $distributorNameFallback, + order_number: $v['order_number'] ?? 'Unknown', + prices: $prices, + product_url: $v['product_url'] ?? $productUrl, + prices_include_vat: $v['prices_include_vat'] ?? null, + ); + } + } + + // Get preview image URL + $previewImageUrl = null; + if (!empty($data['images']) && is_array($data['images']) && !empty($data['images'][0]['url'])) { + $previewImageUrl = $data['images'][0]['url']; + } + + return new PartDetailDTO( + provider_key: $providerKey, + provider_id: $providerId, + name: $data['name'] ?? 'Unknown', + description: $data['description'] ?? '', + category: $data['category'] ?? null, + manufacturer: $data['manufacturer'] ?? null, + mpn: $data['mpn'] ?? null, + preview_image_url: $previewImageUrl, + manufacturing_status: $manufacturingStatus, + provider_url: $productUrl, + footprint: $data['footprint'] ?? null, + gtin: $data['gtin'] ?? null, + notes: $data['notes'] ?? null, + datasheets: $datasheets, + images: $images, + parameters: $parameters, + vendor_infos: $vendorInfos, + mass: isset($data['mass']) && is_numeric($data['mass']) ? (float) $data['mass'] : null, + manufacturer_product_url: $data['manufacturer_product_url'] ?? null, + ); + } + +} diff --git a/src/Services/InfoProviderSystem/DTOs/BrowserSubmittedPage.php b/src/Services/InfoProviderSystem/DTOs/BrowserSubmittedPage.php new file mode 100644 index 00000000..0f4fbf5f --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/BrowserSubmittedPage.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\InfoProviderSystem\DTOs; + +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Represents a webpage submitted by the browser extension, held temporarily in the application cache. + */ +final readonly class BrowserSubmittedPage +{ + /** + * @var string A unique token for this page, derived from the URL and HTML content. Used to identify the page in the cache without storing the full HTML in the session. + */ + public string $token; + + public function __construct( + #[Assert\Url()] + #[Assert\NotBlank] + public string $url, + #[Assert\NotBlank] + #[Assert\Length(max: 5 * 1024 * 1024)] // Limit to 5 MB to prevent abuse + public string $html, + #[Assert\NotBlank] + public string $title, + public \DateTimeImmutable $submittedAt = new \DateTimeImmutable(), + ) { + $this->token = hash('xxh3', $url . '|' . $html); + } +} diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php index 50b7f4cf..47d8ac69 100644 --- a/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php @@ -22,24 +22,41 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; + /** * Represents a mapping between a part field and the info providers that should search in that field. */ readonly class BulkSearchFieldMappingDTO { + /** @var string[] $providers Array of provider keys to search with (e.g., ['digikey', 'farnell']) */ + public array $providers; + /** * @param string $field The field to search in (e.g., 'mpn', 'name', or supplier-specific fields like 'digikey_spn') - * @param string[] $providers Array of provider keys to search with (e.g., ['digikey', 'farnell']) + * @param string[]|InfoProviderInterface[] $providers Array of provider keys to search with (e.g., ['digikey', 'farnell']) * @param int $priority Priority for this field mapping (1-10, lower numbers = higher priority) */ public function __construct( public string $field, - public array $providers, + array $providers = [], public int $priority = 1 ) { if ($priority < 1 || $priority > 10) { throw new \InvalidArgumentException('Priority must be between 1 and 10'); } + + //Ensure that providers are provided as keys + foreach ($providers as &$provider) { + if ($provider instanceof InfoProviderInterface) { + $provider = $provider->getProviderKey(); + } + if (!is_string($provider)) { + throw new \InvalidArgumentException('Providers must be provided as strings or InfoProviderInterface instances'); + } + } + unset($provider); + $this->providers = $providers; } /** diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php index 58e9e240..3db09de3 100644 --- a/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; +use Doctrine\ORM\Exception\ORMException; use App\Entity\Parts\Part; use Doctrine\ORM\EntityManagerInterface; use Traversable; @@ -176,7 +177,7 @@ readonly class BulkSearchResponseDTO implements \ArrayAccess, \IteratorAggregate * @param array $data * @param EntityManagerInterface $entityManager * @return BulkSearchResponseDTO - * @throws \Doctrine\ORM\Exception\ORMException + * @throws ORMException */ public static function fromSerializableRepresentation(array $data, EntityManagerInterface $entityManager): BulkSearchResponseDTO { diff --git a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php index 41d50510..9700ae57 100644 --- a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php @@ -42,6 +42,7 @@ class PartDetailDTO extends SearchResultDTO ?ManufacturingStatus $manufacturing_status = null, ?string $provider_url = null, ?string $footprint = null, + ?string $gtin = null, public readonly ?string $notes = null, /** @var FileDTO[]|null */ public readonly ?array $datasheets = null, @@ -68,6 +69,7 @@ class PartDetailDTO extends SearchResultDTO manufacturing_status: $manufacturing_status, provider_url: $provider_url, footprint: $footprint, + gtin: $gtin ); } } diff --git a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php index 2acf3e57..cf1f577d 100644 --- a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php @@ -39,7 +39,9 @@ readonly class PriceDTO public string $price, /** @var string The currency of the used ISO code of this price detail */ public ?string $currency_iso_code, - /** @var bool If the price includes tax */ + /** @var bool If the price includes tax + * @deprecated Use the prices_include_vat property of the PurchaseInfoDTO instead, as this property is not reliable if there are multiple prices with different values for includes_tax + */ public ?bool $includes_tax = true, /** @var float the price related quantity */ public ?float $price_related_quantity = 1.0, diff --git a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php index 9ac142ff..446d04dc 100644 --- a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php @@ -29,6 +29,9 @@ namespace App\Services\InfoProviderSystem\DTOs; */ readonly class PurchaseInfoDTO { + /** @var bool|null If the prices contain VAT or not. Null if state is unknown. */ + public ?bool $prices_include_vat; + public function __construct( public string $distributor_name, public string $order_number, @@ -36,6 +39,7 @@ readonly class PurchaseInfoDTO public array $prices, /** @var string|null An url to the product page of the vendor */ public ?string $product_url = null, + ?bool $prices_include_vat = null, ) { //Ensure that the prices are PriceDTO instances @@ -44,5 +48,17 @@ readonly class PurchaseInfoDTO throw new \InvalidArgumentException('The prices array must only contain PriceDTO instances'); } } + + //If no prices_include_vat information is given, try to deduct it from the prices + if ($prices_include_vat === null) { + $vatValues = array_unique(array_map(fn(PriceDTO $price) => $price->includes_tax, $this->prices)); + if (count($vatValues) === 1) { + $this->prices_include_vat = $vatValues[0]; //Use the value of the prices if they are all the same + } else { + $this->prices_include_vat = null; //If there are different values for the prices, we cannot determine if the prices include VAT or not + } + } else { + $this->prices_include_vat = $prices_include_vat; + } } } diff --git a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php index a70b2486..085ae17e 100644 --- a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php @@ -59,6 +59,8 @@ class SearchResultDTO public readonly ?string $provider_url = null, /** @var string|null A footprint representation of the providers page */ public readonly ?string $footprint = null, + /** @var string|null The GTIN / EAN of the part */ + public readonly ?string $gtin = null, ) { if ($preview_image_url !== null) { @@ -90,6 +92,7 @@ class SearchResultDTO 'manufacturing_status' => $this->manufacturing_status?->value, 'provider_url' => $this->provider_url, 'footprint' => $this->footprint, + 'gtin' => $this->gtin, ]; } @@ -112,6 +115,7 @@ class SearchResultDTO manufacturing_status: isset($data['manufacturing_status']) ? ManufacturingStatus::tryFrom($data['manufacturing_status']) : null, provider_url: $data['provider_url'] ?? null, footprint: $data['footprint'] ?? null, + gtin: $data['gtin'] ?? null, ); } } diff --git a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php index a655a0df..c7c15673 100644 --- a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php +++ b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php @@ -94,7 +94,6 @@ final class DTOtoEntityConverter $entity->setPrice($dto->getPriceAsBigDecimal()); $entity->setPriceRelatedQuantity($dto->price_related_quantity); - //Currency TODO if ($dto->currency_iso_code !== null) { $entity->setCurrency($this->getCurrency($dto->currency_iso_code)); } else { @@ -117,6 +116,8 @@ final class DTOtoEntityConverter $entity->addPricedetail($this->convertPrice($price)); } + $entity->setPricesIncludesVAT($dto->prices_include_vat); + return $entity; } @@ -175,6 +176,8 @@ final class DTOtoEntityConverter $entity->setManufacturingStatus($dto->manufacturing_status ?? ManufacturingStatus::NOT_SET); $entity->setManufacturerProductURL($dto->manufacturer_product_url ?? ''); + $entity->setGtin($dto->gtin); + //Set the provider reference on the part $entity->setProviderReference(InfoProviderReference::fromPartDTO($dto)); diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index 0eb74642..f5ff144d 100644 --- a/src/Services/InfoProviderSystem/PartInfoRetriever.php +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -24,10 +24,15 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem; use App\Entity\Parts\Part; +use App\Exceptions\InfoProviderNotActiveException; +use App\Exceptions\OAuthReconnectRequiredException; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use Psr\Http\Client\ClientExceptionInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -48,9 +53,15 @@ final class PartInfoRetriever * Search for a keyword in the given providers. The results can be cached * @param string[]|InfoProviderInterface[] $providers A list of providers to search in, either as provider keys or as provider instances * @param string $keyword The keyword to search for + * @param array $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation. * @return SearchResultDTO[] The search results + * @throws InfoProviderNotActiveException if any of the given providers is not active + * @throws ClientException if any of the providers throws an exception during the search + * @throws \InvalidArgumentException if any of the given providers is not a valid provider key or instance + * @throws TransportException if any of the providers throws an exception during the search + * @throws OAuthReconnectRequiredException if any of the providers throws an exception during the search that indicates that the OAuth token needs to be refreshed */ - public function searchByKeyword(string $keyword, array $providers): array + public function searchByKeyword(string $keyword, array $providers, array $options = []): array { $results = []; @@ -61,7 +72,7 @@ final class PartInfoRetriever //Ensure that the provider is active if (!$provider->isActive()) { - throw new \RuntimeException("The provider with key {$provider->getProviderKey()} is not active!"); + throw InfoProviderNotActiveException::fromProvider($provider); } if (!$provider instanceof InfoProviderInterface) { @@ -69,7 +80,7 @@ final class PartInfoRetriever } /** @noinspection SlowArrayOperationsInLoopInspection */ - $results = array_merge($results, $this->searchInProvider($provider, $keyword)); + $results = array_merge($results, $this->searchInProvider($provider, $keyword, $options)); } return $results; @@ -79,15 +90,31 @@ final class PartInfoRetriever * Search for a keyword in the given provider. The result is cached for 7 days. * @return SearchResultDTO[] */ - protected function searchInProvider(InfoProviderInterface $provider, string $keyword): array + protected function searchInProvider(InfoProviderInterface $provider, string $keyword, array $options = []): array { //Generate key and escape reserved characters from the provider id - $escaped_keyword = urlencode($keyword); - return $this->partInfoCache->get("search_{$provider->getProviderKey()}_{$escaped_keyword}", function (ItemInterface $item) use ($provider, $keyword) { - //Set the expiration time - $item->expiresAfter(!$this->debugMode ? self::CACHE_RESULT_EXPIRATION : 1); + $escaped_keyword = hash('xxh3', $keyword); - return $provider->searchByKeyword($keyword); + $no_cache = $options[InfoProviderInterface::OPTION_NO_CACHE] ?? false; + + //Exclude the no_cache option from the options hash, since it should not affect the cache key, as it only determines whether to bypass the cache or not, but does not change the actual search results + $options_without_cache = $options; + unset($options_without_cache[InfoProviderInterface::OPTION_NO_CACHE]); + //Generate a hash for the options, to ensure that different options result in different cache entries + $options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR)); + + $cache_key = "search_{$provider->getProviderKey()}_{$escaped_keyword}_{$options_hash}"; + + //If no_cache is set, bypass the cache and get fresh results from the provider + if ($no_cache) { + $this->partInfoCache->delete($cache_key); + } + + return $this->partInfoCache->get($cache_key, function (ItemInterface $item) use ($provider, $keyword, $options) { + //Set the expiration time + $item->expiresAfter(!$this->debugMode ? self::CACHE_RESULT_EXPIRATION : 10); + + return $provider->searchByKeyword($keyword, $options); }); } @@ -96,24 +123,39 @@ final class PartInfoRetriever * The result is cached for 4 days. * @param string $provider_key * @param string $part_id + * @param array $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation. * @return PartDetailDTO + * @throws InfoProviderNotActiveException if the the given providers is not active */ - public function getDetails(string $provider_key, string $part_id): PartDetailDTO + public function getDetails(string $provider_key, string $part_id, array $options = []): PartDetailDTO { $provider = $this->provider_registry->getProviderByKey($provider_key); //Ensure that the provider is active if (!$provider->isActive()) { - throw new \RuntimeException("The provider with key $provider_key is not active!"); + throw InfoProviderNotActiveException::fromProvider($provider); } - //Generate key and escape reserved characters from the provider id - $escaped_part_id = urlencode($part_id); - return $this->partInfoCache->get("details_{$provider_key}_{$escaped_part_id}", function (ItemInterface $item) use ($provider, $part_id) { - //Set the expiration time - $item->expiresAfter(!$this->debugMode ? self::CACHE_DETAIL_EXPIRATION : 1); + //Exclude the no_cache option from the options hash, since it should not affect the cache key, as it only determines whether to bypass the cache or not, but does not change the actual search results + $options_without_cache = $options; + unset($options_without_cache[InfoProviderInterface::OPTION_NO_CACHE]); + //Generate a hash for the options, to ensure that different options result in different cache entries + $options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR)); - return $provider->getDetails($part_id); + //Generate key and escape reserved characters from the provider id + $escaped_part_id = hash('xxh3', $part_id); + $cache_key = "details_{$provider_key}_{$escaped_part_id}_{$options_hash}"; + + //Delete the cache entry if no_cache is set, to ensure that the next get call will fetch fresh data from the provider, instead of returning stale data from the cache. + if ($options[InfoProviderInterface::OPTION_NO_CACHE] ?? false) { + $this->partInfoCache->delete($cache_key); + } + + return $this->partInfoCache->get($cache_key, function (ItemInterface $item) use ($provider, $part_id, $options) { + //Set the expiration time + $item->expiresAfter(!$this->debugMode ? self::CACHE_DETAIL_EXPIRATION : 10); + + return $provider->getDetails($part_id, $options); }); } @@ -133,16 +175,16 @@ final class PartInfoRetriever */ public function dtoToPart(PartDetailDTO $search_result): Part { - return $this->createPart($search_result->provider_key, $search_result->provider_id); + return $this->dto_to_entity_converter->convertPart($search_result); } /** * Use the given details to create a part entity */ - public function createPart(string $provider_key, string $part_id): Part + public function createPart(string $provider_key, string $part_id, array $options): Part { - $details = $this->getDetails($provider_key, $part_id); + $details = $this->getDetails($provider_key, $part_id, $options); return $this->dto_to_entity_converter->convertPart($details); } -} \ No newline at end of file +} diff --git a/src/Services/InfoProviderSystem/ProviderRegistry.php b/src/Services/InfoProviderSystem/ProviderRegistry.php index f6c398d2..18b8a37a 100644 --- a/src/Services/InfoProviderSystem/ProviderRegistry.php +++ b/src/Services/InfoProviderSystem/ProviderRegistry.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use App\Services\InfoProviderSystem\Providers\URLHandlerInfoProviderInterface; /** * This class keeps track of all registered info providers and allows to find them by their key @@ -47,6 +48,8 @@ final class ProviderRegistry */ private array $providers_disabled = []; + private array $providers_by_domain = []; + /** * @var bool Whether the registry has been initialized */ @@ -78,6 +81,14 @@ final class ProviderRegistry $this->providers_by_name[$key] = $provider; if ($provider->isActive()) { $this->providers_active[$key] = $provider; + if ($provider instanceof URLHandlerInfoProviderInterface) { + foreach ($provider->getHandledDomains() as $domain) { + if (isset($this->providers_by_domain[$domain])) { + throw new \LogicException("Domain $domain is already handled by another provider"); + } + $this->providers_by_domain[$domain] = $provider; + } + } } else { $this->providers_disabled[$key] = $provider; } @@ -139,4 +150,29 @@ final class ProviderRegistry return $this->providers_disabled; } -} \ No newline at end of file + + public function getProviderHandlingDomain(string $domain): (InfoProviderInterface&URLHandlerInfoProviderInterface)|null + { + if (!$this->initialized) { + $this->initStructures(); + } + + //Check if the domain is directly existing: + if (isset($this->providers_by_domain[$domain])) { + return $this->providers_by_domain[$domain]; + } + + //Otherwise check for subdomains: + $parts = explode('.', $domain); + while (count($parts) > 2) { + array_shift($parts); + $check_domain = implode('.', $parts); + if (isset($this->providers_by_domain[$check_domain])) { + return $this->providers_by_domain[$check_domain]; + } + } + + //If we found nothing, return null + return null; + } +} diff --git a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php new file mode 100644 index 00000000..6539e69b --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php @@ -0,0 +1,331 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Exceptions\ProviderIDNotSupportedException; +use App\Helpers\RandomizeUseragentHttpClient; +use App\Services\AI\AIPlatformRegistry; +use App\Services\InfoProviderSystem\SubmittedPageStorage; +use App\Services\InfoProviderSystem\CreateFromUrlHelper; +use App\Services\InfoProviderSystem\DTOJsonSchemaConverter; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Settings\InfoProviderSystem\AIExtractorSettings; +use Jkphl\Micrometa; +use League\HTMLToMarkdown\HtmlConverter; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\DomCrawler\UriResolver; +use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; +use Symfony\Component\Intl\Languages; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +use function Symfony\Component\String\u; + + +final class AIWebProvider implements InfoProviderInterface +{ + use FixAndValidateUrlTrait; + + private const DISTRIBUTOR_NAME = 'Website'; + + private readonly HttpClientInterface $httpClient; + + public function __construct( + HttpClientInterface $httpClient, + private readonly AIExtractorSettings $settings, + private readonly AIPlatformRegistry $AIPlatformRegistry, + private readonly DTOJsonSchemaConverter $jsonSchemaConverter, + private readonly CacheItemPoolInterface $partInfoCache, + private readonly CreateFromUrlHelper $createFromUrlHelper, + private readonly SubmittedPageStorage $browserHtmlStorage, + ) { + //Use NoPrivateNetworkHttpClient to prevent SSRF vulnerabilities, and RandomizeUseragentHttpClient to make it harder for servers to block us + $this->httpClient = (new RandomizeUseragentHttpClient(new NoPrivateNetworkHttpClient($httpClient)))->withOptions( + [ + 'timeout' => 15, + ] + ); + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'AI Web Extractor', + 'description' => 'Extract part info from any URL using LLM', + //'url' => 'https://openrouter.ai', + 'disabled_help' => 'Configure AI settings', + 'settings_class' => AIExtractorSettings::class, + ]; + } + + public function getProviderKey(): string + { + return 'ai_web'; + } + + public function isActive(): bool + { + return $this->settings->platform !== null && $this->settings->model !== null && $this->settings->model !== ''; + } + + public function searchByKeyword(string $keyword, array $options = []): array + { + $url = $this->fixAndValidateURL($keyword); + + if (!($options[self::OPTION_SKIP_DELEGATION] ?? false)) { + //Before loading the page, try to delegate to another provider + $delegatedPart = $this->createFromUrlHelper->delegateToOtherProvider($url, $this); + if ($delegatedPart !== null) { + return [$delegatedPart]; + } + } + + try { + + $new_options = $options; + $new_options[self::OPTION_SKIP_DELEGATION] = true; //Skip delegation for the getDetails call to prevent infinite loops + + return [ + $this->getDetails($keyword, $new_options) + ]; } catch (ProviderIDNotSupportedException $e) { + return []; + } + } + + public function getDetails(string $id, array $options = []): PartDetailDTO + { + $url = $this->fixAndValidateURL($id); + + if (!($options[self::OPTION_SKIP_DELEGATION] ?? false)) { + //Before loading the page, try to delegate to another provider + $delegatedPart = $this->createFromUrlHelper->delegateToOtherProviderDetails($url, $this); + if ($delegatedPart !== null) { + return $delegatedPart; + } + } + + //Check if we have a cached result for this URL, to avoid unnecessary LLM calls, which can be slow and costly. + $cacheKey = 'ai_web_'.hash('xxh3', $url); + + //If ignore cache option is set, skip cache and fetch fresh data + if ($options[self::OPTION_NO_CACHE] ?? false) { + $this->partInfoCache->deleteItem($cacheKey); + } + + //Return cached result if available + $cacheItem = $this->partInfoCache->getItem($cacheKey); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + // Use pre-fetched browser HTML if the option is set and a stored page is available for this URL + $html = null; + if (($token = ($options[self::OPTION_SUBMITTED_PAGE_TOKEN] ?? '')) !== '') { + $html = $this->browserHtmlStorage->retrieve($token)?->html; + } + + //Otherwise fetch it ourselves. + if ($html === null) { + $response = $this->httpClient->request('GET', $url); + $html = $response->getContent(); + } + + //Convert html to markdown, to provide a cleaner input to the LLM. + $markdown = $this->htmlToMarkdown($html, $url); + //Truncate markdown to max content length, if needed + $markdown = u($markdown)->truncate($this->settings->maxContentLength, '... [truncated]')->toString(); + + //Extract structured data using traditional methods, to provide additional context to the LLM. This can help improve accuracy, especially for technical specifications that might be in tables or specific formats. + $structuredData = $this->extractStructuredData($html, $url); + + // Call LLM + $llmResponse = $this->callLLM($markdown, $url, $structuredData); + + // Build and return PartDetailDTO + $result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, $this->getProviderKey(), $url, $url, self::DISTRIBUTOR_NAME); + + // Cache the result for future use, to improve performance and reduce costs. + $cacheItem->set($result); + $cacheItem->expiresAfter(3600 * 2); //Cache for 2 hours, as web content can change frequently, but we still want to benefit from caching for repeated accesses. + $this->partInfoCache->save($cacheItem); + + return $result; + } + + /** + * Extracts structured data from the HTML using microformats. + * @param string $html + * @param string $url + * @return string JSON encoded structured data + */ + private function extractStructuredData(string $html, string $url): string + { + try { + //Only parse microdata, json-ld and rdfa, as they are the most common formats for structured data on product pages. Links and microformat only create clutter for the LLM + $micrometa = new Micrometa\Ports\Parser(Micrometa\Ports\Format::JSON_LD | Micrometa\Ports\Format::MICRODATA | Micrometa\Ports\Format::RDFA_LITE); + $items = $micrometa($url, $html); + } catch (\RuntimeException $exception) { + //If parsing fails, try again without rdfa, as it seems to cause problems on pages like ebay + try { + $micrometa = new Micrometa\Ports\Parser(Micrometa\Ports\Format::JSON_LD | Micrometa\Ports\Format::MICRODATA); + $items = $micrometa($url, $html); + } catch (\RuntimeException $exception) { + //If it still fails, return empty structured data + return '{}'; + } + } + + return json_encode($items->toObject(), JSON_THROW_ON_ERROR); + } + + private function htmlToMarkdown(string $html, string $url): string + { + + $crawler = new Crawler($html); + + //Replace relative URLs with absolute URLs, to ensure that the LLM has full context and can access the links if needed. + $baseUrl = $crawler->getBaseHref() ?? $url; + + //Replace all relative links with their absolute counnterparts, to provide more context to the LLM and to ensure that any links included in the markdown are valid and can be accessed if needed. + $crawler->filter('a')->each(function (Crawler $node) use ($baseUrl) { + $href = $node->attr('href'); + if ($href) { + $absoluteUrl = UriResolver::resolve($href, $baseUrl); + //@phpstan-ignore-next-line we know that getNode(0) will always return a DOMElement, because the crawler is initialized with valid HTML and we are filtering for 'a' tags, which are always DOMElements. + $node->getNode(0)->setAttribute('href', $absoluteUrl); + } + }); + + $crawler->filter('img')->each(function (Crawler $node) use ($baseUrl) { + $src = $node->attr('src'); + if ($src) { + $absoluteUrl = UriResolver::resolve($src, $baseUrl); + //@phpstan-ignore-next-line we know that getNode(0) will always return a DOMElement, because the crawler is initialized with valid HTML and we are filtering for 'a' tags, which are always DOMElements. + $node->getNode(0)->setAttribute('src', $absoluteUrl); + } + }); + + //Extract only the main content of the page to avoid overwhelming the LLM with irrelevant information. + $mainContent = $crawler->filter('main, article, #content'); + + // If we found a specific content area, get its HTML; otherwise, use the whole body. + //Concat the html of all matched nodes, to provide more context to the LLM, especially for pages that use multiple sections for product info. + if ($mainContent->count() > 0) { + $htmlToConvert = ''; + foreach ($mainContent as $node) { + $htmlToConvert .= $node->ownerDocument->saveHTML($node); + $htmlToConvert .= "\n\n"; // Add some spacing between sections + } + } else { + //Use the whole body content, as it might contain relevant information, especially for simpler pages that don't have a clear main/content section. + $htmlToConvert = $crawler->outerHtml(); + } + + + //Concert to markdown + $converter = new HtmlConverter([ + 'strip_tags' => true, // Removes tags that aren't Markdown-compatible (like
) + 'hard_break' => true, // Preserves line breaks + 'remove_nodes' => 'nav footer script style' // Extra safety layer + ]); + + return $converter->convert($htmlToConvert); + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::PARAMETERS, + ]; + } + + private function callLLM(string $htmlContent, string $url, ?string $structuredData = null): array + { + $input = new MessageBag( + Message::forSystem($this->buildSystemPrompt()), + Message::ofUser("Extract part information from this webpage content:\n\nURL: $url\n\n$htmlContent") + ); + + if ($structuredData) { + $input->add(Message::ofUser("Following data was extracted using traditional methods, but might be incomplete or inaccurate. + Enrich it with the actual website data:\n\n".$structuredData)); + } + + try { + $aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') ); + + //'openai/gpt-5-mini' + $result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [ + 'response_format' => [ + 'type' => 'json_schema', + 'json_schema' => $this->jsonSchemaConverter->getJSONSchema(), + ] + ]); + } catch (\Throwable $e) { + throw new \RuntimeException('LLM invocation failed: '.$e->getMessage(), previous: $e); + } + + return $result->getResult()->getContent(); + } + + private function buildSystemPrompt(): string + { + $tmp = <<<'PROMPT' +You are an expert at extracting electronic component information from web pages. Extract structured data in JSON format, from markdown extracted from a product page. +Focus on the main content of the page, such as product descriptions, specifications, and tables. Ignore navigation menus, footers, and sidebars. + +Rules: +- manufacturing_status: Use "active", "obsolete", "nrfnd" (not recommended for new designs), "discontinued", or null +- parameters: Extract technical specs like voltage, current, temperature, etc. and put them into the fields according to the JSON schema. Include units if available. +- prices: Extract pricing tiers with minimum_quantity, price, and currency code +- URLs must be absolute (include https://...) +- If information is not found, use null +- Try to avoid duplicating parameters, if the same parameter is mentioned multiple times, or if it is already used in another field. +- Include only the 1 to 3 most relevant images, such as the main product image or important diagrams. Ignore decorative images, logos, or icons. +- Extract GTIN / EAN if available, as it can be useful for matching parts across different sources, even if the part number is different. +- Include detailed product description into notes field, as it can contain important information that doesn't fit into other fields, such as features, applications, or unique selling points. + +PROMPT; + + if ($this->settings->outputLanguage === null) { + $tmp .= "\n\nProvide the response in the same language of the webpage."; + } else { + $tmp .= "\n\nThe response must be in ". Languages::getName($this->settings->outputLanguage, 'en') ." language. Translate texts if needed."; + } + + if ($this->settings->additionalInstructions) { + $tmp .= "\n\nAdditional instructions:\n" . $this->settings->additionalInstructions; + } + + return $tmp; + } + +} diff --git a/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php index 549f117a..cd918439 100644 --- a/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php +++ b/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php @@ -34,7 +34,8 @@ interface BatchInfoProviderInterface extends InfoProviderInterface * Search for multiple keywords in a single batch operation and return the results, ordered by the keywords. * This allows for a more efficient search compared to running multiple single searches. * @param string[] $keywords + * @param array $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation. * @return array An associative array where the key is the keyword and the value is the search results for that keyword */ - public function searchByKeywordsBatch(array $keywords): array; + public function searchByKeywordsBatch(array $keywords, array $options = []): array; } diff --git a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php new file mode 100644 index 00000000..ca6e26e1 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php @@ -0,0 +1,679 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Settings\InfoProviderSystem\BuerklinSettings; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProviderInterface +{ + + private const ENDPOINT_URL = 'https://www.buerklin.com/buerklinws/v2/buerklin'; + + public const DISTRIBUTOR_NAME = 'Buerklin'; + + private const CACHE_TTL = 600; + /** + * Local in-request cache to avoid hitting the PSR cache repeatedly for the same product. + * @var array + */ + private array $productCache = []; + + public function __construct( + private readonly HttpClientInterface $client, + private readonly CacheItemPoolInterface $partInfoCache, + private readonly BuerklinSettings $settings, + ) { + + } + + /** + * Gets the latest OAuth token for the Buerklin API, or creates a new one if none is available + * TODO: Rework this to use the OAuth token manager system in the database... + * @return string + */ + private function getToken(): string + { + // Cache token to avoid hammering the auth server on every request + $cacheKey = 'buerklin.oauth.token'; + $item = $this->partInfoCache->getItem($cacheKey); + + if ($item->isHit()) { + $token = $item->get(); + if (is_string($token) && $token !== '') { + return $token; + } + } + + // Buerklin OAuth2 password grant (ROPC) + $resp = $this->client->request('POST', 'https://www.buerklin.com/authorizationserver/oauth/token/', [ + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + 'body' => [ + 'grant_type' => 'password', + 'client_id' => $this->settings->clientId, + 'client_secret' => $this->settings->secret, + 'username' => $this->settings->username, + 'password' => $this->settings->password, + ], + ]); + + $data = $resp->toArray(false); + + if (!isset($data['access_token'])) { + throw new \RuntimeException( + 'Invalid token response from Buerklin: HTTP ' . $resp->getStatusCode() . ' body=' . $resp->getContent(false) + ); + } + + $token = (string) $data['access_token']; + + // Cache for (expires_in - 30s) if available + $ttl = 300; + if (isset($data['expires_in']) && is_numeric($data['expires_in'])) { + $ttl = max(60, (int) $data['expires_in'] - 30); + } + + $item->set($token); + $item->expiresAfter($ttl); + $this->partInfoCache->save($item); + + return $token; + } + + private function getDefaultQueryParams(): array + { + return [ + 'curr' => $this->settings->currency ?: 'EUR', + 'language' => $this->settings->language ?: 'en', + ]; + } + + private function getProduct(string $code, bool $use_cache = true): array + { + $code = strtoupper(trim($code)); + if ($code === '') { + throw new \InvalidArgumentException('Product code must not be empty.'); + } + + $cacheKey = sprintf( + 'buerklin.product.%s', + md5($code . '|' . $this->settings->language . '|' . $this->settings->currency) + ); + + if (!$use_cache) { + $this->partInfoCache->deleteItem($cacheKey); + unset($this->productCache[$cacheKey]); + } + + if (isset($this->productCache[$cacheKey])) { + return $this->productCache[$cacheKey]; + } + + $item = $this->partInfoCache->getItem($cacheKey); + if ($item->isHit() && is_array($cached = $item->get())) { + return $this->productCache[$cacheKey] = $cached; + } + + $product = $this->makeAPICall('/products/' . rawurlencode($code) . '/'); + + $item->set($product); + $item->expiresAfter(self::CACHE_TTL); + $this->partInfoCache->save($item); + + return $this->productCache[$cacheKey] = $product; + } + + private function makeAPICall(string $endpoint, array $queryParams = []): array + { + try { + $response = $this->client->request('GET', self::ENDPOINT_URL . $endpoint, [ + 'auth_bearer' => $this->getToken(), + 'headers' => ['Accept' => 'application/json'], + 'query' => array_merge($this->getDefaultQueryParams(), $queryParams), + ]); + + return $response->toArray(); + } catch (\Exception $e) { + throw new \RuntimeException("Buerklin API request failed: " . + "Endpoint: " . $endpoint . + "Token: [redacted] " . + "QueryParams: " . json_encode($queryParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . " " . + "Exception message: " . $e->getMessage()); + } + } + + + public function getProviderInfo(): array + { + return [ + 'name' => 'Buerklin', + 'description' => 'This provider uses the Buerklin API to search for parts.', + 'url' => 'https://www.buerklin.com/', + 'disabled_help' => 'Configure the API Client ID, Secret, Username and Password provided by Buerklin in the provider settings to enable.', + 'settings_class' => BuerklinSettings::class + ]; + } + + public function getProviderKey(): string + { + return 'buerklin'; + } + + // This provider is considered active if settings are present + public function isActive(): bool + { + // The client credentials and user credentials must be set + return $this->settings->clientId !== null && $this->settings->clientId !== '' + && $this->settings->secret !== null && $this->settings->secret !== '' + && $this->settings->username !== null && $this->settings->username !== '' + && $this->settings->password !== null && $this->settings->password !== ''; + } + + /** + * Sanitizes a field by removing any HTML tags and other unwanted characters + * @param string|null $field + * @return string|null + */ + private function sanitizeField(?string $field): ?string + { + if ($field === null) { + return null; + } + + return strip_tags($field); + } + + /** + * Takes a deserialized JSON object of the product and returns a PartDetailDTO + * @param array $product + * @return PartDetailDTO + */ + private function getPartDetail(array $product): PartDetailDTO + { + // If this is a search-result object, it may not contain prices/features/images -> reload full details. + if ((!isset($product['price']) && !isset($product['volumePrices'])) && isset($product['code'])) { + try { + $product = $this->getProduct((string) $product['code']); + } catch (\Throwable $e) { + // If reload fails, keep the partial product data and continue. + } + } + + // Extract images from API response + $productImages = $this->getProductImages($product['images'] ?? null); + + // Set preview image + $preview = $productImages[0]->url ?? null; + + // Extract features (parameters) from classifications[0].features of Buerklin JSON response + $features = $product['classifications'][0]['features'] ?? []; + + // Feature parameters (from classifications->features) + $featureParams = $this->attributesToParameters($features, ''); // leave group empty for normal parameters + + // Compliance parameters (from top-level fields like RoHS/SVHC/…) + $complianceParams = $this->complianceToParameters($product, 'Compliance'); + + // Merge all parameters + $allParams = array_merge($featureParams, $complianceParams); + + // Assign footprint: "Design" (en) / "Bauform" (de) / "Enclosure" (en) / "Gehäuse" (de) + $footprint = null; + if (is_array($features)) { + foreach ($features as $feature) { + $name = $feature['name'] ?? null; + if ($name === 'Design' || $name === 'Bauform' || $name === 'Enclosure' || $name === 'Gehäuse') { + $footprint = $feature['featureValues'][0]['value'] ?? null; + break; + } + } + } + + // Prices: prefer volumePrices, fallback to single price + $code = (string) ($product['orderNumber'] ?? $product['code'] ?? ''); + $prices = $product['volumePrices'] ?? null; + + if (!is_array($prices) || count($prices) === 0) { + $pVal = $product['price']['value'] ?? null; + $pCur = $product['price']['currencyIso'] ?? ($this->settings->currency ?: 'EUR'); + + if (is_numeric($pVal)) { + $prices = [ + [ + 'minQuantity' => 1, + 'value' => (float) $pVal, + 'currencyIso' => (string) $pCur, + ] + ]; + } else { + $prices = []; + } + } + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: (string) ($product['code'] ?? $code), + + name: (string) ($product['manufacturerProductId'] ?? $code), + description: $this->sanitizeField($product['description'] ?? null), + + category: $this->sanitizeField($product['classifications'][0]['name'] ?? ($product['categories'][0]['name'] ?? null)), + manufacturer: $this->sanitizeField($product['manufacturer'] ?? null), + mpn: $this->sanitizeField($product['manufacturerProductId'] ?? null), + + preview_image_url: $preview, + manufacturing_status: null, + + provider_url: $this->getProductShortURL((string) ($product['code'] ?? $code)), + footprint: $footprint, + + datasheets: null, // not found in JSON response, the Buerklin website however has links to datasheets + images: $productImages, + + parameters: $allParams, + + vendor_infos: $this->pricesToVendorInfo( + sku: $code, + url: $this->getProductShortURL($code), + prices: $prices + ), + + mass: $product['weight'] ?? null, + ); + } + + /** + * Converts the price array to a VendorInfoDTO array to be used in the PartDetailDTO + * @param string $sku + * @param string $url + * @param array $prices + * @return array + */ + private function pricesToVendorInfo(string $sku, string $url, array $prices): array + { + $priceDTOs = array_map(function ($price) { + $val = $price['value'] ?? null; + $valStr = is_numeric($val) + ? number_format((float) $val, 6, '.', '') // 6 decimal places, trailing zeros are fine + : (string) $val; + + // Optional: softly trim unnecessary trailing zeros (e.g. 75.550000 -> 75.55) + $valStr = rtrim(rtrim($valStr, '0'), '.'); + + return new PriceDTO( + minimum_discount_amount: (float) ($price['minQuantity'] ?? 1), + price: $valStr, + currency_iso_code: (string) ($price['currencyIso'] ?? $this->settings->currency ?? 'EUR'), + includes_tax: false + ); + }, $prices); + + return [ + new PurchaseInfoDTO( + distributor_name: self::DISTRIBUTOR_NAME, + order_number: $sku, + prices: $priceDTOs, + product_url: $url, + ) + ]; + } + + + /** + * Returns a valid Buerklin product short URL from product code + * @param string $product_code + * @return string + */ + private function getProductShortURL(string $product_code): string + { + return 'https://www.buerklin.com/' . $this->settings->language . '/p/' . $product_code . '/'; + } + + /** + * Returns a deduplicated list of product images as FileDTOs. + * + * - takes only real image arrays (with 'url' field) + * - makes relative URLs absolute + * - deduplicates using URL + * - prefers 'zoom' format, then 'product' format, then all others + * + * @param array|null $images + * @return FileDTO[] + */ + private function getProductImages(?array $images): array + { + if (!is_array($images)) { + return []; + } + + // 1) Only real image entries with URL + $imgs = array_values(array_filter($images, fn($i) => is_array($i) && !empty($i['url']))); + + // 2) Prefer zoom images + $zoom = array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'zoom')); + $chosen = count($zoom) > 0 + ? $zoom + : array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'product')); + + // 3) If still none, take all + if (count($chosen) === 0) { + $chosen = $imgs; + } + + // 4) Deduplicate by URL (after making absolute) + $byUrl = []; + foreach ($chosen as $img) { + $url = (string) $img['url']; + + if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) { + $url = 'https://www.buerklin.com' . $url; + } + if (!filter_var($url, FILTER_VALIDATE_URL)) { + continue; + } + + $byUrl[$url] = $url; + } + + return array_map( + fn($url) => new FileDTO($url), + array_values($byUrl) + ); + } + + private function attributesToParameters(array $features, ?string $group = null): array + { + $out = []; + + foreach ($features as $f) { + if (!is_array($f)) { + continue; + } + + $name = $f['name'] ?? null; + if (!is_string($name) || trim($name) === '') { + continue; + } + + $vals = []; + foreach (($f['featureValues'] ?? []) as $fv) { + if (is_array($fv) && isset($fv['value']) && is_string($fv['value']) && trim($fv['value']) !== '') { + $vals[] = trim($fv['value']); + } + } + if (empty($vals)) { + continue; + } + + // Multiple values: join with comma + $value = implode(', ', array_values(array_unique($vals))); + + // Unit/symbol from Buerklin feature + $unit = $f['featureUnit']['symbol'] ?? null; + if (!is_string($unit) || trim($unit) === '') { + $unit = null; + } + + // ParameterDTO parses value field (handles value + unit) + $out[] = ParameterDTO::parseValueField( + name: $name, + value: $value, + unit: $unit, + symbol: null, + group: $group + ); + } + + // Deduplicate by name + $byName = []; + foreach ($out as $p) { + $byName[$p->name] ??= $p; + } + + return array_values($byName); + } + + /** + * @param string $keyword + * @param array $options + * @return PartDetailDTO[] + */ + public function searchByKeyword(string $keyword, array $options = []): array + { + $keyword = strtoupper(trim($keyword)); + if ($keyword === '') { + return []; + } + + $response = $this->makeAPICall('/products/search/', [ + 'pageSize' => 50, + 'currentPage' => 0, + 'query' => $keyword, + 'sort' => 'relevance', + ]); + + $products = $response['products'] ?? []; + + // Normal case: products found in search results + if (is_array($products) && !empty($products)) { + return array_map(fn($p) => $this->getPartDetail($p), $products); + } + + // Fallback: try direct lookup by code + try { + $product = $this->getProduct($keyword, use_cache: !($options[self::OPTION_NO_CACHE] ?? false)); + return [$this->getPartDetail($product)]; + } catch (\Throwable $e) { + return []; + } + } + + public function getDetails(string $id, array $options = []): PartDetailDTO + { + // Detail endpoint is /products/{code}/ + //By default use cache for details, but allow bypassing cache with option (e.g. for refresh) + $response = $this->getProduct($id, use_cache: !($options[self::OPTION_NO_CACHE] ?? false)); + + return $this->getPartDetail($response); + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + //ProviderCapabilities::DATASHEET, // currently not implemented + ProviderCapabilities::PRICE, + ProviderCapabilities::FOOTPRINT, + ]; + } + + private function complianceToParameters(array $product, ?string $group = 'Compliance'): array + { + $params = []; + + $add = function (string $name, $value) use (&$params, $group) { + if ($value === null) { + return; + } + + if (is_bool($value)) { + $value = $value ? 'Yes' : 'No'; + } elseif (is_array($value) || is_object($value)) { + // Avoid dumping large or complex structures + return; + } else { + $value = trim((string) $value); + if ($value === '') { + return; + } + } + + $params[] = ParameterDTO::parseValueField( + name: $name, + value: (string) $value, + unit: null, + symbol: null, + group: $group + ); + }; + + $add('RoHS conform', $product['labelRoHS'] ?? null); // "yes"/"no" + + $rawRoHsDate = $product['dateRoHS'] ?? null; + // Try to parse and reformat date to Y-m-d (do not use language-dependent formats) + if (is_string($rawRoHsDate) && $rawRoHsDate !== '') { + try { + $dt = new \DateTimeImmutable($rawRoHsDate); + $formatted = $dt->format('Y-m-d'); + } catch (\Exception $e) { + $formatted = $rawRoHsDate; + } + // Always use the same parameter name (do not use language-dependent names) + $add('RoHS date', $formatted); + } + $add('SVHC free', $product['SVHC'] ?? null); // bool + $add('Hazardous good', $product['hazardousGood'] ?? null); // bool + $add('Hazardous materials', $product['hazardousMaterials'] ?? null); // bool + + $add('Country of origin', $product['countryOfOrigin'] ?? null); + // Customs tariff code must always be stored as string, otherwise "85411000" may be stored as "8.5411e+7" + if (isset($product['articleCustomsCode'])) { + // Raw value as string + $codeRaw = (string) $product['articleCustomsCode']; + + // Optionally keep only digits (in case of spaces or other characters) + $code = preg_replace('/\D/', '', $codeRaw) ?? $codeRaw; + $code = trim($code); + + if ($code !== '') { + $params[] = new ParameterDTO( + name: 'Customs code', + value_text: $code, + value_typ: null, + value_min: null, + value_max: null, + unit: null, + symbol: null, + group: $group + ); + } + } + + return $params; + } + + /** + * @param array $keywords + * @param array $options + * @return array + */ + public function searchByKeywordsBatch(array $keywords, array $options = []): array + { + /** @var array $results */ + $results = []; + + foreach ($keywords as $keyword) { + $keyword = strtoupper(trim((string) $keyword)); + if ($keyword === '') { + continue; + } + + // Reuse existing single search -> returns PartDetailDTO[] + /** @var PartDetailDTO[] $partDetails */ + $partDetails = $this->searchByKeyword($keyword); + + // Convert to SearchResultDTO[] + $results[$keyword] = array_map( + fn(PartDetailDTO $detail) => $this->convertPartDetailToSearchResult($detail), + $partDetails + ); + } + + return $results; + } + + /** + * Converts a PartDetailDTO into a SearchResultDTO for bulk search. + */ + private function convertPartDetailToSearchResult(PartDetailDTO $detail): SearchResultDTO + { + return new SearchResultDTO( + provider_key: $detail->provider_key, + provider_id: $detail->provider_id, + name: $detail->name, + description: $detail->description ?? '', + category: $detail->category ?? null, + manufacturer: $detail->manufacturer ?? null, + mpn: $detail->mpn ?? null, + preview_image_url: $detail->preview_image_url ?? null, + manufacturing_status: $detail->manufacturing_status ?? null, + provider_url: $detail->provider_url ?? null, + footprint: $detail->footprint ?? null, + ); + } + + public function getHandledDomains(): array + { + return ['buerklin.com']; + } + + public function getIDFromURL(string $url): ?string + { + //Inputs: + //https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/ + //https://www.buerklin.com/de/p/40F1332/ + //https://www.buerklin.com/en/p/bkl-electronic/dc-connectors/072341-l/40F1332/ + //https://www.buerklin.com/en/p/40F1332/ + //The ID is the last part after the manufacturer/category/mpn segment and before the final slash + //https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/#download should also work + + $path = parse_url($url, PHP_URL_PATH); + + if (!$path) { + return null; + } + + // Ensure it's actually a product URL + if (strpos($path, '/p/') === false) { + return null; + } + + $id = basename(rtrim($path, '/')); + + return $id !== '' && $id !== 'p' ? $id : null; + } + +} diff --git a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php new file mode 100644 index 00000000..aee30d6b --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php @@ -0,0 +1,233 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Settings\InfoProviderSystem\BuerklinSettings; +use App\Settings\InfoProviderSystem\CanopySettings; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\DependencyInjection\Attribute\When; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * Use canopy API to retrieve infos from amazon + */ +class CanopyProvider implements InfoProviderInterface +{ + + public const BASE_URL = "https://rest.canopyapi.co/api"; + public const SEARCH_API_URL = self::BASE_URL . "/amazon/search"; + public const DETAIL_API_URL = self::BASE_URL . "/amazon/product"; + + public const DISTRIBUTOR_NAME = 'Amazon'; + + public function __construct(private readonly CanopySettings $settings, + private readonly HttpClientInterface $httpClient, private readonly CacheItemPoolInterface $partInfoCache) + { + + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Amazon (Canopy)', + 'description' => 'Retrieves part infos from Amazon using the Canopy API', + 'url' => 'https://canopyapi.co', + 'disabled_help' => 'Set Canopy API key in the provider configuration to enable this provider', + 'settings_class' => CanopySettings::class + ]; + } + + public function getProviderKey(): string + { + return 'canopy'; + } + + public function isActive(): bool + { + return $this->settings->apiKey !== null; + } + + private function productPageFromASIN(string $asin): string + { + return "https://www.{$this->settings->getRealDomain()}/dp/{$asin}"; + } + + /** + * Saves the given part to the cache. + * Everytime this function is called, the cache is overwritten. + * @param PartDetailDTO $part + * @return void + */ + private function saveToCache(PartDetailDTO $part): void + { + $key = 'canopy_part_'.$part->provider_id; + + $item = $this->partInfoCache->getItem($key); + $item->set($part); + $item->expiresAfter(3600 * 24); //Cache for 1 day + $this->partInfoCache->save($item); + } + + /** + * Retrieves a from the cache, or null if it was not cached yet. + * @param string $id + * @return PartDetailDTO|null + */ + private function getFromCache(string $id): ?PartDetailDTO + { + $key = 'canopy_part_'.$id; + + $item = $this->partInfoCache->getItem($key); + if ($item->isHit()) { + return $item->get(); + } + + return null; + } + + public function searchByKeyword(string $keyword, array $options = []): array + { + $response = $this->httpClient->request('GET', self::SEARCH_API_URL, [ + 'query' => [ + 'domain' => $this->settings->domain, + 'searchTerm' => $keyword, + ], + 'headers' => [ + 'API-KEY' => $this->settings->apiKey, + ] + ]); + + $data = $response->toArray(); + $results = $data['data']['amazonProductSearchResults']['productResults']['results'] ?? []; + + $out = []; + foreach ($results as $result) { + + + $dto = new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $result['asin'], + name: $result["title"], + description: "", + preview_image_url: $result["mainImageUrl"] ?? null, + provider_url: $this->productPageFromASIN($result['asin']), + vendor_infos: [$this->priceToPurchaseInfo($result['price'], $result['asin'])] + ); + + $out[] = $dto; + $this->saveToCache($dto); + } + + return $out; + } + + private function categoriesToCategory(array $categories): ?string + { + if (count($categories) === 0) { + return null; + } + + return implode(" -> ", array_map(static fn($cat) => $cat['name'], $categories)); + } + + private function feauturesBulletsToNotes(array $featureBullets): string + { + $notes = "
    "; + foreach ($featureBullets as $bullet) { + $notes .= "
  • " . $bullet . "
  • "; + } + $notes .= "
"; + return $notes; + } + + private function priceToPurchaseInfo(?array $price, string $asin): PurchaseInfoDTO + { + $priceDtos = []; + if ($price !== null) { + $priceDtos[] = new PriceDTO(minimum_discount_amount: 1, price: (string) $price['value'], currency_iso_code: $price['currency'], includes_tax: true); + } + + + return new PurchaseInfoDTO(self::DISTRIBUTOR_NAME, order_number: $asin, prices: $priceDtos, product_url: $this->productPageFromASIN($asin)); + } + + public function getDetails(string $id, array $options = []): PartDetailDTO + { + //Check that the id is a valid ASIN (10 characters, letters and numbers) + if (!preg_match('/^[A-Z0-9]{10}$/', $id)) { + throw new \InvalidArgumentException("The id must be a valid ASIN (10 characters, letters and numbers)"); + } + + $do_not_cache = ($options[self::OPTION_NO_CACHE] ?? false) || $this->settings->alwaysGetDetails; + + //Use cached details if available and the settings allow it, to avoid unnecessary API requests, since the search results already contain most of the details + if(!$do_not_cache && ($cached = $this->getFromCache($id)) !== null) { + return $cached; + } + + $response = $this->httpClient->request('GET', self::DETAIL_API_URL, [ + 'query' => [ + 'asin' => $id, + 'domain' => $this->settings->domain, + ], + 'headers' => [ + 'API-KEY' => $this->settings->apiKey, + ], + ]); + + $product = $response->toArray()['data']['amazonProduct']; + + + if ($product === null) { + throw new \RuntimeException("Product with ASIN $id not found"); + } + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $product['asin'], + name: $product['title'], + description: '', + category: $this->categoriesToCategory($product['categories']), + manufacturer: $product['brand'] ?? null, + preview_image_url: $product['mainImageUrl'] ?? $product['imageUrls'][0] ?? null, + provider_url: $this->productPageFromASIN($product['asin']), + notes: $this->feauturesBulletsToNotes($product['featureBullets'] ?? []), + vendor_infos: [$this->priceToPurchaseInfo($product['price'], $product['asin'])] + ); + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::PRICE, + ]; + } +} diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php new file mode 100644 index 00000000..2e6708be --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -0,0 +1,347 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Settings\InfoProviderSystem\ConradSettings; +use App\Settings\InfoProviderSystem\ConradShopIDs; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface +{ + + private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch'; + public const DISTRIBUTOR_NAME = 'Conrad'; + + private HttpClientInterface $httpClient; + + public function __construct( HttpClientInterface $httpClient, private ConradSettings $settings) + { + //We want everything in JSON + $this->httpClient = $httpClient->withOptions([ + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Conrad', + 'description' => 'Retrieves part information from conrad.de', + 'url' => 'https://www.conrad.de/', + 'disabled_help' => 'Set API key in settings', + 'settings_class' => ConradSettings::class, + ]; + } + + public function getProviderKey(): string + { + return 'conrad'; + } + + public function isActive(): bool + { + return !empty($this->settings->apiKey); + } + + private function getProductUrl(string $productId): string + { + return 'https://' . $this->settings->shopID->getDomain() . '/' . $this->settings->shopID->getLanguage() . '/p/' . $productId; + } + + private function getFootprintFromTechnicalDetails(array $technicalDetails): ?string + { + foreach ($technicalDetails as $detail) { + if ($detail['name'] === 'ATT_LOV_HOUSING_SEMICONDUCTORS') { + return $detail['values'][0] ?? null; + } + } + + return null; + } + + public function searchByKeyword(string $keyword, array $options = []): array + { + $url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/' + . $this->settings->shopID->getDomainEnd() . '/' . $this->settings->shopID->getLanguage() + . '/' . $this->settings->shopID->getCustomerType(); + + $response = $this->httpClient->request('POST', $url, [ + 'query' => [ + 'apikey' => $this->settings->apiKey, + ], + 'json' => [ + 'query' => $keyword, + 'size' => 50, + 'sort' => [["field"=>"_score","order"=>"desc"]], + ], + ]); + + $out = []; + $results = $response->toArray(); + + foreach($results['hits'] as $result) { + + $out[] = new SearchResultDTO( + provider_key: $this->getProviderKey(), + provider_id: $result['productId'], + name: $result['manufacturerId'] ?? $result['productId'], + description: $result['title'] ?? '', + manufacturer: $result['brand']['name'] ?? null, + mpn: $result['manufacturerId'] ?? null, + preview_image_url: $result['image'] ?? null, + provider_url: $this->getProductUrl($result['productId']), + footprint: $this->getFootprintFromTechnicalDetails($result['technicalDetails'] ?? []), + gtin: $result['ean'] ?? null, + ); + } + + return $out; + } + + private function getFootprintFromTechnicalAttributes(array $technicalDetails): ?string + { + foreach ($technicalDetails as $detail) { + if ($detail['attributeID'] === 'ATT.LOV.HOUSING_SEMICONDUCTORS') { + return $detail['values'][0]['value'] ?? null; + } + } + + return null; + } + + /** + * @param array $technicalAttributes + * @return array + */ + private function technicalAttributesToParameters(array $technicalAttributes): array + { + return array_map(static function (array $p) { + if (count($p['values']) === 1) { //Single value attribute + if (array_key_exists('unit', $p['values'][0])) { + return ParameterDTO::parseValueField( //With unit + name: $p['attributeName'], + value: $p['values'][0]['value'], + unit: $p['values'][0]['unit']['name'], + ); + } + + return ParameterDTO::parseValueIncludingUnit( + name: $p['attributeName'], + value: $p['values'][0]['value'], + ); + } + + if (count($p['values']) === 2) { //Multi value attribute (e.g. min/max) + $value = $p['values'][0]['value'] ?? null; + $value2 = $p['values'][1]['value'] ?? null; + $unit = $p['values'][0]['unit']['name'] ?? ''; + $unit2 = $p['values'][1]['unit']['name'] ?? ''; + if ($unit === $unit2 && is_numeric($value) && is_numeric($value2)) { + if (array_key_exists('unit', $p['values'][0])) { //With unit + return new ParameterDTO( + name: $p['attributeName'], + value_min: (float)$value, + value_max: (float)$value2, + unit: $unit, + ); + } + + return new ParameterDTO( + name: $p['attributeName'], + value_min: (float)$value, + value_max: (float)$value2, + ); + } + } + + // fallback implementation + $values = implode(", ", array_map(fn($q) => + array_key_exists('unit', $q) ? $q['value']." ". ($q['unit']['name'] ?? $q['unit']) : $q['value'] + , $p['values'])); + return ParameterDTO::parseValueIncludingUnit( + name: $p['attributeName'], + value: $values, + ); + }, $technicalAttributes); + } + + /** + * @param array $productMedia + * @return array + */ + public function productMediaToDatasheets(array $productMedia): array + { + $files = []; + foreach ($productMedia['manuals'] ?? [] as $manual) { + //Filter out unwanted languages + if (!empty($this->settings->attachmentLanguageFilter) && !in_array($manual['language'], $this->settings->attachmentLanguageFilter, true)) { + continue; + } + + $files[] = new FileDTO($manual['fullUrl'], $manual['title'] . ' (' . $manual['language'] . ')'); + } + + return $files; + } + + + /** + * Queries prices for a given product ID. It makes a POST request to the Conrad API + * @param string $productId + * @return PurchaseInfoDTO + */ + private function queryPrices(string $productId): PurchaseInfoDTO + { + $priceQueryURL = $this->settings->shopID->getAPIRoot() . '/price-availability/4/' + . $this->settings->shopID->getShopID() . '/facade'; + + $response = $this->httpClient->request('POST', $priceQueryURL, [ + 'query' => [ + 'apikey' => $this->settings->apiKey, + 'overrideCalculationSchema' => $this->settings->includeVAT ? 'GROSS' : 'NET' + ], + 'json' => [ + 'ns:inputArticleItemList' => [ + "#namespaces" => [ + "ns" => "http://www.conrad.de/ccp/basit/service/article/priceandavailabilityservice/api" + ], + 'articles' => [ + [ + "articleID" => $productId, + "calculatePrice" => true, + "checkAvailability" => true, + ], + ] + ] + ] + ]); + + $result = $response->toArray(); + + $priceInfo = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['price'] ?? []; + $price = $priceInfo['price'] ?? "0.0"; + $currency = $priceInfo['currency'] ?? "EUR"; + $includesVat = !$priceInfo['isGrossAmount'] || $priceInfo['isGrossAmount'] === "true"; + $minOrderAmount = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['availabilityStatus']['minimumOrderQuantity'] ?? 1; + + $prices = []; + foreach ($priceInfo['priceScale'] ?? [] as $priceScale) { + $prices[] = new PriceDTO( + minimum_discount_amount: max($priceScale['scaleFrom'], $minOrderAmount), + price: (string)$priceScale['pricePerUnit'], + currency_iso_code: $currency, + includes_tax: $includesVat + ); + } + if (empty($prices)) { //Fallback if no price scales are defined + $prices[] = new PriceDTO( + minimum_discount_amount: $minOrderAmount, + price: (string)$price, + currency_iso_code: $currency, + includes_tax: $includesVat + ); + } + + return new PurchaseInfoDTO( + distributor_name: self::DISTRIBUTOR_NAME, + order_number: $productId, + prices: $prices, + product_url: $this->getProductUrl($productId) + ); + } + + public function getDetails(string $id, array $options = []): PartDetailDTO + { + $productInfoURL = $this->settings->shopID->getAPIRoot() . '/product/1/service/' . $this->settings->shopID->getShopID() + . '/product/' . $id; + + $response = $this->httpClient->request('GET', $productInfoURL, [ + 'query' => [ + 'apikey' => $this->settings->apiKey, + ] + ]); + + $data = $response->toArray(); + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $data['shortProductNumber'], + name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'], + description: $data['productShortInformation']['title'] ?? '', + category: $data['productShortInformation']['articleGroupName'] ?? null, + manufacturer: $data['brand']['displayName'] !== null ? preg_replace("/[\u{2122}\u{00ae}]/", "", $data['brand']['displayName']) : null, //Replace ™ and ® symbols + mpn: $data['productFullInformation']['manufacturer']['id'] ?? null, + preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null, + provider_url: $this->getProductUrl($data['shortProductNumber']), + footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []), + gtin: $data['productFullInformation']['eanCode'] ?? null, + notes: $data['productFullInformation']['description'] ?? null, + datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []), + parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []), + vendor_infos: [$this->queryPrices($data['shortProductNumber'])] + ); + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::GTIN, + ]; + } + + public function getHandledDomains(): array + { + $domains = []; + foreach (ConradShopIDs::cases() as $shopID) { + $domains[] = $shopID->getDomain(); + } + return array_unique($domains); + } + + public function getIDFromURL(string $url): ?string + { + //Input: https://www.conrad.de/de/p/apple-iphone-air-wolkenweiss-256-gb-eek-a-a-g-16-5-cm-6-5-zoll-3475299.html + //The numbers before the optional .html are the product ID + + $matches = []; + if (preg_match('/-(\d+)(\.html)?$/', $url, $matches) === 1) { + return $matches[1]; + } + + return null; + } +} diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php index 423b5244..e7a62aa4 100644 --- a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php @@ -106,7 +106,7 @@ class DigikeyProvider implements InfoProviderInterface return $this->settings->clientId !== null && $this->settings->clientId !== '' && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME); } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $request = [ 'Keywords' => $keyword, @@ -159,7 +159,7 @@ class DigikeyProvider implements InfoProviderInterface return $result; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { try { $response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [ @@ -311,6 +311,14 @@ class DigikeyProvider implements InfoProviderInterface 'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME) ]); + if ($response->getStatusCode() === 404) { + //No media found + return [ + 'datasheets' => [], + 'images' => [], + ]; + } + $media_array = $response->toArray(); foreach ($media_array['MediaLinks'] as $media_link) { diff --git a/src/Services/InfoProviderSystem/Providers/Element14Provider.php b/src/Services/InfoProviderSystem/Providers/Element14Provider.php index 27dfb908..1d9e092c 100644 --- a/src/Services/InfoProviderSystem/Providers/Element14Provider.php +++ b/src/Services/InfoProviderSystem/Providers/Element14Provider.php @@ -33,7 +33,7 @@ use App\Settings\InfoProviderSystem\Element14Settings; use Composer\CaBundle\CaBundle; use Symfony\Contracts\HttpClient\HttpClientInterface; -class Element14Provider implements InfoProviderInterface +class Element14Provider implements InfoProviderInterface, URLHandlerInfoProviderInterface { private const ENDPOINT_URL = 'https://api.element14.com/catalog/products'; @@ -282,12 +282,12 @@ class Element14Provider implements InfoProviderInterface }; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { return $this->queryByTerm('any:' . $keyword); } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $tmp = $this->queryByTerm('id:' . $id); if (count($tmp) === 0) { @@ -309,4 +309,21 @@ class Element14Provider implements InfoProviderInterface ProviderCapabilities::DATASHEET, ]; } + + public function getHandledDomains(): array + { + return ['element14.com', 'farnell.com', 'newark.com']; + } + + public function getIDFromURL(string $url): ?string + { + //Input URL example: https://de.farnell.com/on-semiconductor/bc547b/transistor-npn-to-92/dp/1017673 + //The digits after the /dp/ are the part ID + $matches = []; + if (preg_match('#/dp/(\d+)#', $url, $matches) === 1) { + return $matches[1]; + } + + return null; + } } diff --git a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php index e0de9772..915a118c 100644 --- a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php @@ -54,7 +54,7 @@ class EmptyProvider implements InfoProviderInterface return true; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { return [ @@ -69,7 +69,7 @@ class EmptyProvider implements InfoProviderInterface ]; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { throw new \RuntimeException('No part details available'); } diff --git a/src/Services/InfoProviderSystem/Providers/FixAndValidateUrlTrait.php b/src/Services/InfoProviderSystem/Providers/FixAndValidateUrlTrait.php new file mode 100644 index 00000000..c9395a46 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/FixAndValidateUrlTrait.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Exceptions\ProviderIDNotSupportedException; + +trait FixAndValidateUrlTrait +{ + private function fixAndValidateURL(string $url): string + { + $originalUrl = $url; + + //Add scheme if missing + if (!preg_match('/^https?:\/\//', $url)) { + //Remove any leading slashes + $url = ltrim($url, '/'); + + //If the URL starts with https:/ or http:/, add the missing slash + //Traefik removes the double slash as secruity measure, so we want to be forgiving and add it back if needed + //See https://github.com/Part-DB/Part-DB-server/issues/1296 + if (preg_match('/^https?:\/[^\/]/', $url)) { + $url = preg_replace('/^(https?:)\/([^\/])/', '$1//$2', $url); + } else { + $url = 'https://'.$url; + } + } + + //If this is not a valid URL with host, domain and path, throw an exception + if (filter_var($url, FILTER_VALIDATE_URL) === false || + parse_url($url, PHP_URL_HOST) === null || + parse_url($url, PHP_URL_PATH) === null) { + throw new ProviderIDNotSupportedException("The given ID is not a valid URL: ".$originalUrl); + } + + return $url; + } +} diff --git a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php new file mode 100644 index 00000000..45777f9e --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php @@ -0,0 +1,399 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Exceptions\ProviderIDNotSupportedException; +use App\Helpers\RandomizeUseragentHttpClient; +use App\Services\InfoProviderSystem\SubmittedPageStorage; +use App\Services\InfoProviderSystem\CreateFromUrlHelper; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Settings\InfoProviderSystem\GenericWebProviderSettings; +use Brick\Schema\Interfaces\BreadcrumbList; +use Brick\Schema\Interfaces\ImageObject; +use Brick\Schema\Interfaces\Product; +use Brick\Schema\Interfaces\PropertyValue; +use Brick\Schema\Interfaces\QuantitativeValue; +use Brick\Schema\Interfaces\Thing; +use Brick\Schema\SchemaReader; +use Brick\Schema\SchemaTypeList; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class GenericWebProvider implements InfoProviderInterface +{ + + use FixAndValidateUrlTrait; + + public const DISTRIBUTOR_NAME = 'Website'; + + private readonly HttpClientInterface $httpClient; + + public function __construct(HttpClientInterface $httpClient, private readonly GenericWebProviderSettings $settings, + private readonly CreateFromUrlHelper $createFromUrlHelper, + private readonly SubmittedPageStorage $browserHtmlStorage, + ) + { + //Use NoPrivateNetworkHttpClient to prevent SSRF vulnerabilities, and RandomizeUseragentHttpClient to make it harder for servers to block us + $this->httpClient = (new RandomizeUseragentHttpClient(new NoPrivateNetworkHttpClient($httpClient)))->withOptions( + [ + 'timeout' => 15, + ] + ); + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Generic Web URL', + 'description' => 'Tries to extract a part from a given product webpage URL using common metadata standards like JSON-LD and OpenGraph.', + //'url' => 'https://example.com', + 'disabled_help' => 'Enable in settings to use this provider', + 'settings_class' => GenericWebProviderSettings::class, + ]; + } + + public function getProviderKey(): string + { + return 'generic_web'; + } + + public function isActive(): bool + { + return $this->settings->enabled; + } + + public function searchByKeyword(string $keyword, array $options = []): array + { + $url = $this->fixAndValidateURL($keyword); + + if (!($options[self::OPTION_SKIP_DELEGATION] ?? false)) { + //Before loading the page, try to delegate to another provider + $delegatedPart = $this->createFromUrlHelper->delegateToOtherProvider($url, $this); + if ($delegatedPart !== null) { + return [$delegatedPart]; + } + } + + try { + $new_options = $options; + $new_options[self::OPTION_SKIP_DELEGATION] = true; //Skip delegation for the getDetails call to prevent infinite loops + return [ + $this->getDetails($keyword, $new_options) + ]; } catch (ProviderIDNotSupportedException $e) { + return []; + } + } + + private function extractShopName(string $url): string + { + $host = parse_url($url, PHP_URL_HOST); + if ($host === false || $host === null) { + return self::DISTRIBUTOR_NAME; + } + return $host; + } + + private function breadcrumbToCategory(?BreadcrumbList $breadcrumbList): ?string + { + if ($breadcrumbList === null) { + return null; + } + + $items = $breadcrumbList->itemListElement->getValues(); + if (count($items) < 1) { + return null; + } + + try { + //Build our category from the breadcrumb items + $categories = []; + foreach ($items as $item) { + if (isset($item->name)) { + $categories[] = trim($item->name->toString()); + } + } + } catch (\Throwable) { + return null; + } + + return implode(' -> ', $categories); + } + + private function productToPart(Product $product, string $url, Crawler $dom, ?BreadcrumbList $categoryBreadcrumb): PartDetailDTO + { + $notes = $product->description->toString() ?? ""; + if ($product->disambiguatingDescription !== null) { + if (!empty($notes)) { + $notes .= "\n\n"; + } + $notes .= $product->disambiguatingDescription->toString(); + } + + + //Extract vendor infos + $vendor_infos = null; + $offer = $product->offers->getFirstValue(); + if ($offer !== null) { + $prices = []; + if ($offer->price->toString() !== null) { + $prices = [new PriceDTO( + minimum_discount_amount: 1, + price: $offer->price->toString(), + currency_iso_code: $offer->priceCurrency?->toString() + )]; + } else { //Check for nested offers (like IKEA does it) + $offer2 = $offer->offers->getFirstValue(); + if ($offer2 !== null && $offer2->price->toString() !== null) { + $prices = [ + new PriceDTO( + minimum_discount_amount: 1, + price: $offer2->price->toString(), + currency_iso_code: $offer2->priceCurrency?->toString() + ) + ]; + } + } + + $vendor_infos = [new PurchaseInfoDTO( + distributor_name: $this->extractShopName($url), + order_number: $product->sku?->toString() ?? $product->identifier?->toString() ?? 'Unknown', + prices: $prices, + product_url: $offer->url?->toString() ?? $url, + )]; + } + + //Extract image: + $image = null; + if ($product->image !== null) { + $imageObj = $product->image->getFirstValue(); + if (is_string($imageObj)) { + $image = $imageObj; + } else if ($imageObj instanceof ImageObject) { + $image = $imageObj->contentUrl?->toString() ?? $imageObj->url?->toString(); + } + } + + //Extract parameters from additionalProperty + $parameters = []; + foreach ($product->additionalProperty->getValues() as $property) { + if ($property instanceof PropertyValue) { //TODO: Handle minValue and maxValue + if ($property->unitText->toString() !== null) { + $parameters[] = ParameterDTO::parseValueField( + name: $property->name->toString() ?? 'Unknown', + value: $property->value->toString() ?? '', + unit: $property->unitText->toString() + ); + } else { + $parameters[] = ParameterDTO::parseValueIncludingUnit( + name: $property->name->toString() ?? 'Unknown', + value: $property->value->toString() ?? '' + ); + } + } + } + + //Try to extract weight + $mass = null; + if (($weight = $product->weight?->getFirstValue()) instanceof QuantitativeValue) { + $mass = $weight->value->toString(); + } + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $url, + name: $product->name?->toString() ?? $product->alternateName?->toString() ?? $product->mpn?->toString() ?? 'Unknown Name', + description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '', + category: $this->breadcrumbToCategory($categoryBreadcrumb) ?? $product->category?->toString(), + manufacturer: self::propertyOrString($product->manufacturer) ?? self::propertyOrString($product->brand), + mpn: $product->mpn?->toString(), + preview_image_url: $image, + provider_url: $url, + gtin: $product->gtin14?->toString() ?? $product->gtin13?->toString() ?? $product->gtin12?->toString() ?? $product->gtin8?->toString(), + notes: $notes, + parameters: $parameters, + vendor_infos: $vendor_infos, + mass: $mass, + ); + } + + private static function propertyOrString(SchemaTypeList|Thing|string|null $value, string $property = "name"): ?string + { + if ($value instanceof SchemaTypeList) { + $value = $value->getFirstValue(); + } + if ($value === null) { + return null; + } + + if (is_string($value)) { + return $value; + } + + return $value->$property?->toString(); + } + + + /** + * Gets the content of a meta tag by its name or property attribute, or null if not found + * @param Crawler $dom + * @param string $name + * @return string|null + */ + private function getMetaContent(Crawler $dom, string $name): ?string + { + $meta = $dom->filter('meta[property="'.$name.'"]'); + if ($meta->count() > 0) { + return $meta->attr('content'); + } + + //Try name attribute + $meta = $dom->filter('meta[name="'.$name.'"]'); + if ($meta->count() > 0) { + return $meta->attr('content'); + } + + return null; + } + + + public function getDetails(string $id, array $options = []): PartDetailDTO + { + $url = $this->fixAndValidateURL($id); + + if (!($options[self::OPTION_SKIP_DELEGATION] ?? false)) { + //Before loading the page, try to delegate to another provider + $delegatedPart = $this->createFromUrlHelper->delegateToOtherProviderDetails($url, $this); + if ($delegatedPart !== null) { + return $delegatedPart; + } + } + + // Use pre-fetched browser HTML if the option is set and a stored page is available for this URL + $content = null; + if (($token = ($options[self::OPTION_SUBMITTED_PAGE_TOKEN] ?? '')) !== '') { + $content = $this->browserHtmlStorage->retrieve($token)?->html; + } + + //Otherwise, fetch the page content ourselves + if ($content === null) { + $response = $this->httpClient->request('GET', $url); + $content = $response->getContent(); + } + + $dom = new Crawler($content); + + //Try to determine a canonical URL + $canonicalURL = $url; + if ($dom->filter('link[rel="canonical"]')->count() > 0) { + $canonicalURL = $dom->filter('link[rel="canonical"]')->attr('href'); + } else if ($dom->filter('meta[property="og:url"]')->count() > 0) { + $canonicalURL = $dom->filter('meta[property="og:url"]')->attr('content'); + } + + //If the canonical URL is relative, make it absolute + if (parse_url($canonicalURL, PHP_URL_SCHEME) === null) { + $parsedUrl = parse_url($url); + $scheme = $parsedUrl['scheme'] ?? 'https'; + $host = $parsedUrl['host'] ?? ''; + $canonicalURL = $scheme.'://'.$host.$canonicalURL; + } + + + $schemaReader = SchemaReader::forAllFormats(); + $things = $schemaReader->readHtml($content, $canonicalURL); + + //Try to find a breadcrumb schema to extract the category + $categoryBreadCrumbs = null; + foreach ($things as $thing) { + if ($thing instanceof BreadcrumbList) { + $categoryBreadCrumbs = $thing; + break; + } + } + + //Try to find a Product schema + foreach ($things as $thing) { + if ($thing instanceof Product) { + return $this->productToPart($thing, $canonicalURL, $dom, $categoryBreadCrumbs); + } + } + + //If no JSON-LD data is found, try to extract basic data from meta tags + $pageTitle = $dom->filter('title')->count() > 0 ? $dom->filter('title')->text() : 'Unknown'; + + $prices = []; + if ($price = $this->getMetaContent($dom, 'product:price:amount')) { + $prices[] = new PriceDTO( + minimum_discount_amount: 1, + price: $price, + currency_iso_code: $this->getMetaContent($dom, 'product:price:currency'), + ); + } else { + //Amazon fallback + $amazonAmount = $dom->filter('input[type="hidden"][name*="amount"]'); + if ($amazonAmount->count() > 0) { + $prices[] = new PriceDTO( + minimum_discount_amount: 1, + price: $amazonAmount->first()->attr('value'), + currency_iso_code: $dom->filter('input[type="hidden"][name*="currencyCode"]')->first()->attr('value'), + ); + } + } + + $vendor_infos = [new PurchaseInfoDTO( + distributor_name: $this->extractShopName($canonicalURL), + order_number: 'Unknown', + prices: $prices, + product_url: $canonicalURL, + )]; + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $canonicalURL, + name: $this->getMetaContent($dom, 'og:title') ?? $pageTitle, + description: $this->getMetaContent($dom, 'og:description') ?? $this->getMetaContent($dom, 'description') ?? '', + manufacturer: $this->getMetaContent($dom, 'product:brand'), + preview_image_url: $this->getMetaContent($dom, 'og:image'), + provider_url: $canonicalURL, + vendor_infos: $vendor_infos, + ); + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::PRICE, + ProviderCapabilities::GTIN, + ]; + } +} diff --git a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php index 1f787559..d3895795 100644 --- a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php +++ b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php @@ -28,6 +28,9 @@ use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; interface InfoProviderInterface { + public const OPTION_NO_CACHE = 'no_cache'; // if set to true, the provider should not use any cache and retrieve fresh data from the source + public const OPTION_SKIP_DELEGATION = 'skip_delegation'; // if set to true, the provider should not delegate the request to other providers, even if it supports delegation. + public const OPTION_SUBMITTED_PAGE_TOKEN = 'submitted_page_token'; // if set to a non-empty string, the provider should use the browser-submitted page with the given token (and retrieve it from BrowserHtmlSessionStorage) /** * Get information about this provider @@ -61,16 +64,18 @@ interface InfoProviderInterface /** * Searches for a keyword and returns a list of search results * @param string $keyword The keyword to search for + * @param array $options An associative array of options for the search, which can be used to pass additional parameters to the provider (e.g. filters, pagination, etc.). The content of this array is provider specific and not defined by the interface * @return SearchResultDTO[] A list of search results */ - public function searchByKeyword(string $keyword): array; + public function searchByKeyword(string $keyword, array $options = []): array; /** * Returns detailed information about the part with the given id * @param string $id + * @param array $options An associative array of options for the search, which can be used to pass additional parameters to the provider (e.g. filters, pagination, etc.). The content of this array is provider specific and not defined by the interface * @return PartDetailDTO */ - public function getDetails(string $id): PartDetailDTO; + public function getDetails(string $id, array $options = []): PartDetailDTO; /** * A list of capabilities this provider supports (which kind of data it can provide). diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php index ede34eb8..5f251b43 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -33,7 +33,7 @@ use App\Settings\InfoProviderSystem\LCSCSettings; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Contracts\HttpClient\HttpClientInterface; -class LCSCProvider implements BatchInfoProviderInterface +class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProviderInterface { private const ENDPOINT_URL = 'https://wmsc.lcsc.com/ftps/wm'; @@ -349,17 +349,18 @@ class LCSCProvider implements BatchInfoProviderInterface return $result; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { return $this->queryByTerm($keyword, true); // Use lightweight mode for search } /** * Batch search multiple keywords asynchronously (like JavaScript Promise.all) - * @param array $keywords Array of keywords to search + * @param array $keywords + * @param array $options * @return array Results indexed by keyword */ - public function searchByKeywordsBatch(array $keywords): array + public function searchByKeywordsBatch(array $keywords, array $options = []): array { if (empty($keywords)) { return []; @@ -396,6 +397,7 @@ class LCSCProvider implements BatchInfoProviderInterface // Now collect all results (like .then() in JavaScript) foreach ($responses as $keyword => $response) { try { + $keyword = (string) $keyword; $arr = $response->toArray(); // This waits for the response $results[$keyword] = $this->processSearchResponse($arr, $keyword); } catch (\Exception $e) { @@ -428,7 +430,7 @@ class LCSCProvider implements BatchInfoProviderInterface return $result; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $tmp = $this->queryByTerm($id, false); if (count($tmp) === 0) { @@ -452,4 +454,21 @@ class LCSCProvider implements BatchInfoProviderInterface ProviderCapabilities::FOOTPRINT, ]; } + + public function getHandledDomains(): array + { + return ['lcsc.com']; + } + + public function getIDFromURL(string $url): ?string + { + //Input example: https://www.lcsc.com/product-detail/C258144.html?s_z=n_BC547 + //The part between the "C" and the ".html" is the unique ID + + $matches = []; + if (preg_match("#/product-detail/(\w+)\.html#", $url, $matches) > 0) { + return $matches[1]; + } + return null; + } } diff --git a/src/Services/InfoProviderSystem/Providers/MouserProvider.php b/src/Services/InfoProviderSystem/Providers/MouserProvider.php index 3171c994..49ca2d50 100644 --- a/src/Services/InfoProviderSystem/Providers/MouserProvider.php +++ b/src/Services/InfoProviderSystem/Providers/MouserProvider.php @@ -76,7 +76,7 @@ class MouserProvider implements InfoProviderInterface return $this->settings->apiKey !== '' && $this->settings->apiKey !== null; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { /* SearchByKeywordRequest description: @@ -144,7 +144,7 @@ class MouserProvider implements InfoProviderInterface return $this->responseToDTOArray($response); } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { /* SearchByPartRequest description: diff --git a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php index b705e04a..9764517b 100644 --- a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php @@ -278,12 +278,13 @@ class OEMSecretsProvider implements InfoProviderInterface * and debugging with local JSON files. The results are processed, cached, and then sorted based * on the keyword and specified criteria. * - * @param string $keyword The part number to search for + * @param string $keyword + * @param array $options * @return array An array of processed product details, sorted by relevance and additional criteria. * * @throws \Exception If the JSON file used for debugging is not found or contains errors. */ - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { /* oemsecrets Part Search API 3.0.1 @@ -397,13 +398,13 @@ class OEMSecretsProvider implements InfoProviderInterface * Generates a cache key for storing part details based on the provided provider ID. * * This method creates a unique cache key by prefixing the provider ID with 'part_details_' - * and hashing the provider ID using MD5 to ensure a consistent and compact key format. + * and hashing the provider ID using XXH3 to ensure a consistent and compact key format. * * @param string $provider_id The unique identifier of the provider or part. * @return string The generated cache key. */ private function getCacheKey(string $provider_id): string { - return 'oemsecrets_part_' . md5($provider_id); + return 'oemsecrets_part_' . hash('xxh3', $provider_id); } @@ -414,14 +415,20 @@ class OEMSecretsProvider implements InfoProviderInterface * found in the cache, they are returned. If not, an exception is thrown indicating that * the details could not be found. * - * @param string $id The unique identifier of the provider or part. + * @param string $id + * @param array $options * @return PartDetailDTO The detailed information about the part. * * @throws \Exception If no details are found for the given provider ID. */ - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $cacheKey = $this->getCacheKey($id); + + if ($options[self::OPTION_NO_CACHE] ?? false) { + $this->partInfoCache->deleteItem($cacheKey); + } + $cacheItem = $this->partInfoCache->getItem($cacheKey); if ($cacheItem->isHit()) { @@ -680,7 +687,7 @@ class OEMSecretsProvider implements InfoProviderInterface if (is_array($prices)) { // Step 1: Check if prices exist in the preferred currency if (isset($prices[$this->settings->currency]) && is_array($prices[$this->settings->currency])) { - $priceDetails = $prices[$this->$this->settings->currency]; + $priceDetails = $prices[$this->settings->currency]; foreach ($priceDetails as $priceDetail) { if ( is_array($priceDetail) && diff --git a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php index 1142f4ef..de404e18 100644 --- a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php @@ -326,7 +326,7 @@ class OctopartProvider implements InfoProviderInterface ); } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $graphQL = sprintf(<<<'GRAPHQL' query partSearch($keyword: String, $limit: Int, $currency: String!, $country: String!, $authorizedOnly: Boolean!) { @@ -367,11 +367,13 @@ class OctopartProvider implements InfoProviderInterface return $tmp; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { + $no_cache = $options[self::OPTION_NO_CACHE] ?? false; + //Check if we have the part cached $cached = $this->getFromCache($id); - if ($cached !== null) { + if (!$no_cache && $cached !== null) { return $cached; } diff --git a/src/Services/InfoProviderSystem/Providers/PollinProvider.php b/src/Services/InfoProviderSystem/Providers/PollinProvider.php index b74e0365..7acecc3a 100644 --- a/src/Services/InfoProviderSystem/Providers/PollinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/PollinProvider.php @@ -36,7 +36,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DomCrawler\Crawler; use Symfony\Contracts\HttpClient\HttpClientInterface; -class PollinProvider implements InfoProviderInterface +class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface { public function __construct(private readonly HttpClientInterface $client, @@ -66,7 +66,7 @@ class PollinProvider implements InfoProviderInterface return $this->settings->enabled; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $response = $this->client->request('GET', 'https://www.pollin.de/search', [ 'query' => [ @@ -110,7 +110,7 @@ class PollinProvider implements InfoProviderInterface }; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { //Ensure that $id is numeric if (!is_numeric($id)) { @@ -141,11 +141,16 @@ class PollinProvider implements InfoProviderInterface $orderId = trim($dom->filter('span[itemprop="sku"]')->text()); //Text is important here //Calculate the mass - $massStr = $dom->filter('meta[itemprop="weight"]')->attr('content'); - //Remove the unit - $massStr = str_replace('kg', '', $massStr); - //Convert to float and convert to grams - $mass = (float) $massStr * 1000; + $massDom = $dom->filter('meta[itemprop="weight"]'); + if ($massDom->count() > 0) { + $massStr = $massDom->attr('content'); + $massStr = str_replace('kg', '', $massStr); + //Convert to float and convert to grams + $mass = (float) $massStr * 1000; + } else { + $mass = null; + } + //Parse purchase info $purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl); @@ -248,4 +253,22 @@ class PollinProvider implements InfoProviderInterface ProviderCapabilities::DATASHEET ]; } + + public function getHandledDomains(): array + { + return ['pollin.de']; + } + + public function getIDFromURL(string $url): ?string + { + //URL like: https://www.pollin.de/p/shelly-bluetooth-schalter-und-dimmer-blu-zb-button-plug-play-mocha-592325 + + //Extract the 6-digit number at the end of the URL + $matches = []; + if (preg_match('/-(\d{6})(?:\/|$)/', $url, $matches)) { + return $matches[1]; + } + + return null; + } } diff --git a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php index fd67cd2c..3a7d03e9 100644 --- a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php +++ b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php @@ -31,9 +31,6 @@ enum ProviderCapabilities /** Basic information about a part, like the name, description, part number, manufacturer etc */ case BASIC; - /** Information about the footprint of a part */ - case FOOTPRINT; - /** Provider can provide a picture for a part */ case PICTURE; @@ -43,6 +40,32 @@ enum ProviderCapabilities /** Provider can provide prices for a part */ case PRICE; + /** Information about the footprint of a part */ + case FOOTPRINT; + + /** Provider can provide GTIN for a part */ + case GTIN; + + /** Provider can provide parameters/specifications for a part */ + case PARAMETERS; + + /** + * Get the order index for displaying capabilities in a stable order. + * @return int + */ + public function getOrderIndex(): int + { + return match($this) { + self::BASIC => 1, + self::PICTURE => 2, + self::DATASHEET => 3, + self::PRICE => 4, + self::FOOTPRINT => 5, + self::GTIN => 6, + self::PARAMETERS => 7, + }; + } + public function getTranslationKey(): string { return 'info_providers.capabilities.' . match($this) { @@ -51,6 +74,8 @@ enum ProviderCapabilities self::PICTURE => 'picture', self::DATASHEET => 'datasheet', self::PRICE => 'price', + self::GTIN => 'gtin', + self::PARAMETERS => 'parameters', }; } @@ -62,6 +87,8 @@ enum ProviderCapabilities self::PICTURE => 'fa-image', self::DATASHEET => 'fa-file-alt', self::PRICE => 'fa-money-bill-wave', + self::GTIN => 'fa-barcode', + self::PARAMETERS => 'fa-list-ul', }; } } diff --git a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php index 5c8efbf1..9dfb099f 100644 --- a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\Providers; +use App\Helpers\RandomizeUseragentHttpClient; use App\Services\InfoProviderSystem\DTOs\FileDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; @@ -30,7 +31,6 @@ use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\ReicheltSettings; -use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DomCrawler\Crawler; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -39,10 +39,13 @@ class ReicheltProvider implements InfoProviderInterface public const DISTRIBUTOR_NAME = "Reichelt"; - public function __construct(private readonly HttpClientInterface $client, + private readonly HttpClientInterface $client; + + public function __construct(HttpClientInterface $client, private readonly ReicheltSettings $settings, ) { + $this->client = new RandomizeUseragentHttpClient($client); } public function getProviderInfo(): array @@ -66,7 +69,7 @@ class ReicheltProvider implements InfoProviderInterface return $this->settings->enabled; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $response = $this->client->request('GET', sprintf($this->getBaseURL() . '/shop/search/%s', $keyword)); $html = $response->getContent(); @@ -84,6 +87,8 @@ class ReicheltProvider implements InfoProviderInterface $name = $element->filter('meta[itemprop="name"]')->attr('content'); $sku = $element->filter('meta[itemprop="sku"]')->attr('content'); + + //Try to extract a picture URL: $pictureURL = $element->filter("div.al_artlogo img")->attr('src'); @@ -95,14 +100,15 @@ class ReicheltProvider implements InfoProviderInterface category: null, manufacturer: $sku, preview_image_url: $pictureURL, - provider_url: $element->filter('a.al_artinfo_link')->attr('href') + provider_url: $element->filter('a.al_artinfo_link')->attr('href'), + ); }); return $results; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { //Check that the ID is a number if (!is_numeric($id)) { @@ -146,6 +152,15 @@ class ReicheltProvider implements InfoProviderInterface $priceString = $dom->filter('meta[itemprop="price"]')->attr('content'); $currency = $dom->filter('meta[itemprop="priceCurrency"]')->attr('content', 'EUR'); + $gtin = null; + foreach (['gtin13', 'gtin14', 'gtin12', 'gtin8'] as $gtinType) { + if ($dom->filter("[itemprop=\"$gtinType\"]")->count() > 0) { + $gtin = $dom->filter("[itemprop=\"$gtinType\"]")->innerText(); + break; + } + } + + //Create purchase info $purchaseInfo = new PurchaseInfoDTO( distributor_name: self::DISTRIBUTOR_NAME, @@ -167,10 +182,11 @@ class ReicheltProvider implements InfoProviderInterface mpn: $this->parseMPN($dom), preview_image_url: $json[0]['article_picture'], provider_url: $productPage, + gtin: $gtin, notes: $notes, datasheets: $datasheets, parameters: $this->parseParameters($dom), - vendor_infos: [$purchaseInfo] + vendor_infos: [$purchaseInfo], ); } @@ -273,6 +289,7 @@ class ReicheltProvider implements InfoProviderInterface ProviderCapabilities::PICTURE, ProviderCapabilities::DATASHEET, ProviderCapabilities::PRICE, + ProviderCapabilities::GTIN, ]; } } diff --git a/src/Services/InfoProviderSystem/Providers/TMEProvider.php b/src/Services/InfoProviderSystem/Providers/TMEProvider.php index 9bc73f09..24ba0ea7 100644 --- a/src/Services/InfoProviderSystem/Providers/TMEProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TMEProvider.php @@ -32,7 +32,7 @@ use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Settings\InfoProviderSystem\TMESettings; -class TMEProvider implements InfoProviderInterface +class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterface { private const VENDOR_NAME = 'TME'; @@ -69,7 +69,7 @@ class TMEProvider implements InfoProviderInterface return $this->tmeClient->isUsable(); } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $response = $this->tmeClient->makeRequest('Products/Search', [ 'Country' => $this->settings->country, @@ -99,7 +99,7 @@ class TMEProvider implements InfoProviderInterface return $result; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $response = $this->tmeClient->makeRequest('Products/GetProducts', [ 'Country' => $this->settings->country, @@ -280,9 +280,13 @@ class TMEProvider implements InfoProviderInterface { //If a URL starts with // we assume that it is a relative URL and we add the protocol if (str_starts_with($url, '//')) { - return 'https:' . $url; + $url = 'https:' . $url; } + //Encode bare % signs that are not already part of a valid percent-encoded sequence + //Fixes part numbers with % in them e.g. SMD0603-5K1-1% + $url = preg_replace('/%(?![0-9A-Fa-f]{2})/', '%25', $url); + return $url; } @@ -296,4 +300,22 @@ class TMEProvider implements InfoProviderInterface ProviderCapabilities::PRICE, ]; } + + public function getHandledDomains(): array + { + return ['tme.eu']; + } + + public function getIDFromURL(string $url): ?string + { + //Input: https://www.tme.eu/de/details/fi321_se/kuhler/alutronic/ + //The ID is the part after the details segment and before the next slash + + $matches = []; + if (preg_match('#/details/([^/]+)/#', $url, $matches) === 1) { + return $matches[1]; + } + + return null; + } } diff --git a/src/Services/InfoProviderSystem/Providers/TestProvider.php b/src/Services/InfoProviderSystem/Providers/TestProvider.php index 8b78c95a..42927abd 100644 --- a/src/Services/InfoProviderSystem/Providers/TestProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TestProvider.php @@ -55,7 +55,7 @@ class TestProvider implements InfoProviderInterface return true; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { return [ new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element1', name: 'Element 1', description: 'fd'), @@ -72,7 +72,7 @@ class TestProvider implements InfoProviderInterface ]; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { return new PartDetailDTO( provider_key: $this->getProviderKey(), @@ -92,4 +92,4 @@ class TestProvider implements InfoProviderInterface ] ); } -} \ No newline at end of file +} diff --git a/src/Services/InfoProviderSystem/Providers/URLHandlerInfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/URLHandlerInfoProviderInterface.php new file mode 100644 index 00000000..c0506648 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/URLHandlerInfoProviderInterface.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +/** + * If an interface + */ +interface URLHandlerInfoProviderInterface +{ + /** + * Returns a list of supported domains (e.g. ["digikey.com"]) + * @return array An array of supported domains + */ + public function getHandledDomains(): array; + + /** + * Extracts the unique ID of a part from a given URL. It is okay if this is not a canonical ID, as long as it can be used to uniquely identify the part within this provider. + * @param string $url The URL to extract the ID from + * @return string|null The extracted ID, or null if the URL is not valid for this provider + */ + public function getIDFromURL(string $url): ?string; +} diff --git a/src/Services/InfoProviderSystem/SubmittedPageStorage.php b/src/Services/InfoProviderSystem/SubmittedPageStorage.php new file mode 100644 index 00000000..5e623f57 --- /dev/null +++ b/src/Services/InfoProviderSystem/SubmittedPageStorage.php @@ -0,0 +1,131 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\InfoProviderSystem; + +use App\Services\InfoProviderSystem\DTOs\BrowserSubmittedPage; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * Stores browser-submitted pages for the browser extension feature. + * + * Each page is stored as a {@see BrowserSubmittedPage} DTO in the application cache with a short TTL. + * The session holds only a compact list of recently submitted URLs so that pages can be listed + * without bloating the session with HTML content. + */ +class SubmittedPageStorage +{ + private const CACHE_KEY_PREFIX = 'browser_plugin_html_'; + private const CACHE_TTL = 1800; // 30 minutes + private const SESSION_KEY = 'browser_plugin_recent_urls'; + private const MAX_RECENT = 10; + + public function __construct( + private readonly RequestStack $requestStack, + private readonly CacheItemPoolInterface $cache, + ) { + } + + /** + * Stores a submitted page in the cache and records its URL in the session's recent list. + * @return string The token under which the page was stored, derived from the URL and HTML. This token is used to retrieve the page later. It is the same value as $page->token. + */ + public function store(BrowserSubmittedPage $page): string + { + $item = $this->cache->getItem($this->cacheKey($page)); + $item->set($page); + $item->expiresAfter(self::CACHE_TTL); + $this->cache->save($item); + + $session = $this->requestStack->getSession(); + $tokens = array_values(array_filter( + $session->get(self::SESSION_KEY, []), + static fn(string $u): bool => $u !== $page->token, + )); + array_unshift($tokens, $page->token); + $session->set(self::SESSION_KEY, array_slice($tokens, 0, self::MAX_RECENT)); + + return $page->token; + } + + /** + * Retrieves the stored page via its token (which is derived from the URL and HTML). Returns null if not found or expired. + */ + public function retrieve(string $token): ?BrowserSubmittedPage + { + $item = $this->cache->getItem($this->cacheKey($token)); + if (!$item->isHit()) { + return null; + } + return $item->get(); + } + + /** + * Returns the list of recently submitted pages, newest first. + * Pages whose cache entry has expired are silently omitted. + * The list depends on the session and thus is per-browser and per-user. + * + * @return BrowserSubmittedPage[] + */ + public function getRecentPages(): array + { + $tokens = $this->requestStack->getSession()->get(self::SESSION_KEY, []); + $pages = []; + foreach ($tokens as $token) { + $page = $this->retrieve($token); + if ($page !== null) { + $pages[] = $page; + } + } + return $pages; + } + + /** + * Removes a page from both the cache and the recent list. + * @param BrowserSubmittedPage|string $page The page or its token to remove. + */ + public function remove(BrowserSubmittedPage|string $page): void + { + $this->cache->deleteItem($this->cacheKey($page)); + + $token = is_string($page) ? $page : $page->token; + + $session = $this->requestStack->getSession(); + //Remove the token from the recent list in the session: + $tokens = array_values(array_filter( + $session->get(self::SESSION_KEY, []), + static fn(string $u): bool => $u !== $token + )); + $session->set(self::SESSION_KEY, $tokens); + } + + private function cacheKey(BrowserSubmittedPage|string $token): string + { + if (!is_string($token)) { + $token = $token->token; + } + + return self::CACHE_KEY_PREFIX . $token; + } +} diff --git a/src/Services/LabelSystem/BarcodeScanner/AmazonBarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/AmazonBarcodeScanResult.php new file mode 100644 index 00000000..fb756043 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/AmazonBarcodeScanResult.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +final readonly class AmazonBarcodeScanResult implements BarcodeScanResultInterface +{ + public function __construct(public string $asin) { + if (!self::isAmazonBarcode($asin)) { + throw new \InvalidArgumentException("The provided input '$asin' is not a valid Amazon barcode (ASIN)"); + } + } + + public static function isAmazonBarcode(string $input): bool + { + //Amazon barcodes are 10 alphanumeric characters + return preg_match('/^[A-Z0-9]{10}$/i', $input) === 1; + } + + public function getDecodedForInfoMode(): array + { + return [ + 'ASIN' => $this->asin, + ]; + } +} diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php deleted file mode 100644 index 2de7c035..00000000 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php +++ /dev/null @@ -1,166 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace App\Services\LabelSystem\BarcodeScanner; - -use App\Entity\LabelSystem\LabelSupportedElement; -use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\Part; -use App\Entity\Parts\PartLot; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityNotFoundException; -use InvalidArgumentException; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -/** - * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest - */ -final class BarcodeRedirector -{ - public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em) - { - } - - /** - * Determines the URL to which the user should be redirected, when scanning a QR code. - * - * @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan - * @return string the URL to which should be redirected - * - * @throws EntityNotFoundException - */ - public function getRedirectURL(BarcodeScanResultInterface $barcodeScan): string - { - if($barcodeScan instanceof LocalBarcodeScanResult) { - return $this->getURLLocalBarcode($barcodeScan); - } - - if ($barcodeScan instanceof EIGP114BarcodeScanResult) { - return $this->getURLVendorBarcode($barcodeScan); - } - - throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan)); - } - - private function getURLLocalBarcode(LocalBarcodeScanResult $barcodeScan): string - { - switch ($barcodeScan->target_type) { - case LabelSupportedElement::PART: - return $this->urlGenerator->generate('app_part_show', ['id' => $barcodeScan->target_id]); - case LabelSupportedElement::PART_LOT: - //Try to determine the part to the given lot - $lot = $this->em->find(PartLot::class, $barcodeScan->target_id); - if (!$lot instanceof PartLot) { - throw new EntityNotFoundException(); - } - - return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]); - - case LabelSupportedElement::STORELOCATION: - return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]); - - default: - throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name); - } - } - - /** - * Gets the URL to a part from a scan of a Vendor Barcode - */ - private function getURLVendorBarcode(EIGP114BarcodeScanResult $barcodeScan): string - { - $part = $this->getPartFromVendor($barcodeScan); - return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]); - } - - /** - * Gets a part from a scan of a Vendor Barcode by filtering for parts - * with the same Info Provider Id or, if that fails, by looking for parts with a - * matching manufacturer product number. Only returns the first matching part. - */ - private function getPartFromVendor(EIGP114BarcodeScanResult $barcodeScan) : Part - { - // first check via the info provider ID (e.g. Vendor ID). This might fail if the part was not added via - // the info provider system or if the part was bought from a different vendor than the data was retrieved - // from. - if($barcodeScan->digikeyPartNumber) { - $qb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); - //Lower() to be case insensitive - $qb->where($qb->expr()->like('LOWER(part.providerReference.provider_id)', 'LOWER(:vendor_id)')); - $qb->setParameter('vendor_id', $barcodeScan->digikeyPartNumber); - $results = $qb->getQuery()->getResult(); - if ($results) { - return $results[0]; - } - } - - if(!$barcodeScan->supplierPartNumber){ - throw new EntityNotFoundException(); - } - - //Fallback to the manufacturer part number. This may return false positives, since it is common for - //multiple manufacturers to use the same part number for their version of a common product - //We assume the user is able to realize when this returns the wrong part - //If the barcode specifies the manufacturer we try to use that as well - $mpnQb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); - $mpnQb->where($mpnQb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)')); - $mpnQb->setParameter('mpn', $barcodeScan->supplierPartNumber); - - if($barcodeScan->mouserManufacturer){ - $manufacturerQb = $this->em->getRepository(Manufacturer::class)->createQueryBuilder("manufacturer"); - $manufacturerQb->where($manufacturerQb->expr()->like("LOWER(manufacturer.name)", "LOWER(:manufacturer_name)")); - $manufacturerQb->setParameter("manufacturer_name", $barcodeScan->mouserManufacturer); - $manufacturers = $manufacturerQb->getQuery()->getResult(); - - if($manufacturers) { - $mpnQb->andWhere($mpnQb->expr()->eq("part.manufacturer", ":manufacturer")); - $mpnQb->setParameter("manufacturer", $manufacturers); - } - - } - - $results = $mpnQb->getQuery()->getResult(); - if($results){ - return $results[0]; - } - throw new EntityNotFoundException(); - } -} diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php index e5930b36..7f65262a 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php @@ -93,6 +93,22 @@ final class BarcodeScanHelper return $this->parseEIGP114Barcode($input); } + if ($type === BarcodeSourceType::GTIN) { + return $this->parseGTINBarcode($input); + } + + if ($type === BarcodeSourceType::LCSC) { + return $this->parseLCSCBarcode($input); + } + + if ($type === BarcodeSourceType::AMAZON) { + return new AmazonBarcodeScanResult($input); + } + + if ($type === BarcodeSourceType::TME) { + return TMEBarcodeScanResult::parse($input); + } + //Null means auto and we try the different formats $result = $this->parseInternalBarcode($input); @@ -117,14 +133,45 @@ final class BarcodeScanHelper return $result; } + //If the result is a valid GTIN barcode, we can parse it directly + if (GTINBarcodeScanResult::isValidGTIN($input)) { + return $this->parseGTINBarcode($input); + } + + // Try LCSC barcode + if (LCSCBarcodeScanResult::isLCSCBarcode($input)) { + return $this->parseLCSCBarcode($input); + } + + //Try amazon barcode + if (AmazonBarcodeScanResult::isAmazonBarcode($input)) { + return new AmazonBarcodeScanResult($input); + } + + // Try TME barcode + if (TMEBarcodeScanResult::isTMEBarcode($input)) { + return TMEBarcodeScanResult::parse($input); + } + throw new InvalidArgumentException('Unknown barcode'); } + private function parseGTINBarcode(string $input): GTINBarcodeScanResult + { + return new GTINBarcodeScanResult($input); + } + private function parseEIGP114Barcode(string $input): EIGP114BarcodeScanResult { return EIGP114BarcodeScanResult::parseFormat06Code($input); } + private function parseLCSCBarcode(string $input): LCSCBarcodeScanResult + { + return LCSCBarcodeScanResult::parse($input); + } + + private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult { $lot_repo = $this->entityManager->getRepository(PartLot::class); diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php new file mode 100644 index 00000000..60a1136f --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php @@ -0,0 +1,373 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace App\Services\LabelSystem\BarcodeScanner; + +use App\Entity\LabelSystem\LabelSupportedElement; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\StorageLocation; +use App\Exceptions\InfoProviderNotActiveException; +use App\Repository\Parts\PartRepository; +use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\ProviderRegistry; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityNotFoundException; +use InvalidArgumentException; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * This class handles the result of a barcode scan and determines further actions, like which URL the user should be redirected to. + * + * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest + */ +final readonly class BarcodeScanResultHandler +{ + public function __construct(private UrlGeneratorInterface $urlGenerator, private EntityManagerInterface $em, private PartInfoRetriever $infoRetriever, + private ProviderRegistry $providerRegistry) + { + } + + /** + * Determines the URL to which the user should be redirected, when scanning a QR code. + * + * @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan + * @return string|null the URL to which should be redirected, or null if no suitable URL could be determined for the given barcode scan result + */ + public function getInfoURL(BarcodeScanResultInterface $barcodeScan): ?string + { + //For other barcodes try to resolve the part first and then redirect to the part page + $entity = $this->resolveEntity($barcodeScan); + + if ($entity === null) { + return null; + } + + if ($entity instanceof Part) { + return $this->urlGenerator->generate('app_part_show', ['id' => $entity->getID()]); + } + + if ($entity instanceof PartLot) { + return $this->urlGenerator->generate('app_part_show', ['id' => $entity->getPart()->getID(), 'highlightLot' => $entity->getID()]); + } + + if ($entity instanceof StorageLocation) { + return $this->urlGenerator->generate('part_list_store_location', ['id' => $entity->getID()]); + } + + //@phpstan-ignore-next-line This should never happen, since resolveEntity should only return Part, PartLot or StorageLocation + throw new \LogicException("Resolved entity is of unknown type: ".get_class($entity)); + } + + /** + * Returns a URL to create a new part based on this barcode scan result, if possible. + * @param BarcodeScanResultInterface $scanResult + * @return string|null + * @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system + */ + public function getCreationURL(BarcodeScanResultInterface $scanResult): ?string + { + $infos = $this->getCreateInfos($scanResult); + if ($infos === null) { + return null; + } + + //Ensure that the provider is active, otherwise we should not generate a creation URL for it + $provider = $this->providerRegistry->getProviderByKey($infos['providerKey']); + if (!$provider->isActive()) { + throw InfoProviderNotActiveException::fromProvider($provider); + } + + //So far we can just copy over our provider info array to the URL parameters: + return $this->urlGenerator->generate('info_providers_create_part', $infos); + } + + /** + * Tries to resolve the given barcode scan result to a local entity. This can be a Part, a PartLot or a StorageLocation, depending on the type of the barcode and the information contained in it. + * Returns null if no matching entity could be found. + * @param BarcodeScanResultInterface $barcodeScan + * @return Part|PartLot|StorageLocation|null + */ + public function resolveEntity(BarcodeScanResultInterface $barcodeScan): Part|PartLot|StorageLocation|null + { + if ($barcodeScan instanceof LocalBarcodeScanResult) { + return $this->resolvePartFromLocal($barcodeScan); + } + + if ($barcodeScan instanceof EIGP114BarcodeScanResult) { + return $this->resolvePartFromVendor($barcodeScan); + } + + if ($barcodeScan instanceof GTINBarcodeScanResult) { + return $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]); + } + + if ($barcodeScan instanceof LCSCBarcodeScanResult) { + return $this->resolvePartFromLCSC($barcodeScan); + } + + if ($barcodeScan instanceof AmazonBarcodeScanResult) { + return $this->em->getRepository(Part::class)->getPartByProviderInfo($barcodeScan->asin) + ?? $this->em->getRepository(Part::class)->getPartBySPN($barcodeScan->asin); + } + + if ($barcodeScan instanceof TMEBarcodeScanResult) { + return $this->resolvePartFromTME($barcodeScan); + } + + return null; + } + + /** + * Tries to resolve a Part from the given barcode scan result. Returns null if no part could be found for the given barcode, + * or the barcode doesn't contain information allowing to resolve to a local part. + * @param BarcodeScanResultInterface $barcodeScan + * @return Part|null + * @throws \InvalidArgumentException if the barcode scan result type is unknown and cannot be handled this function + */ + public function resolvePart(BarcodeScanResultInterface $barcodeScan): ?Part + { + $entity = $this->resolveEntity($barcodeScan); + if ($entity instanceof Part) { + return $entity; + } + if ($entity instanceof PartLot) { + return $entity->getPart(); + } + //Storage locations are not associated with a specific part, so we cannot resolve a part for + //a storage location barcode + return null; + } + + private function resolvePartFromLocal(LocalBarcodeScanResult $barcodeScan): Part|PartLot|StorageLocation|null + { + return match ($barcodeScan->target_type) { + LabelSupportedElement::PART => $this->em->find(Part::class, $barcodeScan->target_id), + LabelSupportedElement::PART_LOT => $this->em->find(PartLot::class, $barcodeScan->target_id), + LabelSupportedElement::STORELOCATION => $this->em->find(StorageLocation::class, $barcodeScan->target_id), + }; + } + + /** + * Gets a part from a scan of a Vendor Barcode by filtering for parts + * with the same Info Provider Id or, if that fails, by looking for parts with a + * matching manufacturer product number. Only returns the first matching part. + */ + private function resolvePartFromVendor(EIGP114BarcodeScanResult $barcodeScan) : ?Part + { + // first check via the info provider ID (e.g. Vendor ID). This might fail if the part was not added via + // the info provider system or if the part was bought from a different vendor than the data was retrieved + // from. + if($barcodeScan->digikeyPartNumber) { + + $part = $this->em->getRepository(Part::class)->getPartByProviderInfo($barcodeScan->digikeyPartNumber); + if ($part !== null) { + return $part; + } + } + + if (!$barcodeScan->supplierPartNumber){ + return null; + } + + //Fallback to the manufacturer part number. This may return false positives, since it is common for + //multiple manufacturers to use the same part number for their version of a common product + //We assume the user is able to realize when this returns the wrong part + //If the barcode specifies the manufacturer we try to use that as well + + return $this->em->getRepository(Part::class)->getPartByMPN($barcodeScan->supplierPartNumber, $barcodeScan->mouserManufacturer); + } + + /** + * Resolve LCSC barcode -> Part. + * Strategy: + * 1) Try providerReference.provider_id == pc (LCSC "Cxxxxxx") if you store it there + * Returns first match (consistent with EIGP114 logic) + * 2) Fallback to search across supplier part number (SPN) + */ + private function resolvePartFromLCSC(LCSCBarcodeScanResult $barcodeScan): ?Part + { + // Try LCSC code (pc) as provider id if available + $pc = $barcodeScan->lcscCode; // e.g. C138033 + if ($pc) { + $part = $this->em->getRepository(Part::class)->getPartByProviderInfo($pc); + if ($part !== null) { + return $part; + } + } + + // fallback to search by SPN + return $this->em->getRepository(Part::class)->getPartBySPN($pc); + } + + + private function resolvePartFromTME(TMEBarcodeScanResult $barcodeScan): ?Part + { + $pn = $barcodeScan->tmePartNumber; + if ($pn) { + $part = $this->em->getRepository(Part::class)->getPartByProviderInfo($pn); + if ($part !== null) { + return $part; + } + + //Try to find the part by SPN/SKU + $part = $this->em->getRepository(Part::class)->getPartBySPN($pn); + if ($part !== null) { + return $part; + } + } + + // Fallback: search by MPN + return $this->em->getRepository(Part::class)->getPartByMPN($barcodeScan->mpn, $barcodeScan->manufacturer); + } + + /** + * Tries to extract creation information for a part from the given barcode scan result. This can be used to + * automatically fill in the info provider reference of a part, when creating a new part based on the scan result. + * Returns null if no provider information could be extracted from the scan result, or if the scan result type is unknown and cannot be handled by this function. + * It is not necessarily checked that the provider is active, or that the result actually exists on the provider side. + * @param BarcodeScanResultInterface $scanResult + * @return array{providerKey: string, providerId: string, lotAmount?: float|int, lotName?: string, lotUserBarcode?: string}|null + * @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system + */ + public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array + { + // TME + if ($scanResult instanceof TMEBarcodeScanResult) { + if ($scanResult->tmePartNumber === null) { + return null; + } + return [ + 'providerKey' => 'tme', + 'providerId' => $scanResult->tmePartNumber, + 'lotAmount' => $scanResult->quantity, + 'lotName' => $scanResult->purchaseOrder, + 'lotUserBarcode' => $scanResult->rawInput, + ]; + } + + // LCSC + if ($scanResult instanceof LCSCBarcodeScanResult) { + return [ + 'providerKey' => 'lcsc', + 'providerId' => $scanResult->lcscCode, + 'lotAmount' => $scanResult->quantity, + 'lotName' => $scanResult->orderNumber ?? $scanResult->pickBatchNumber, + 'lotUserBarcode' => $scanResult->rawInput, + ]; + } + + if ($scanResult instanceof EIGP114BarcodeScanResult) { + return $this->getCreationInfoForEIGP114($scanResult); + } + + if ($scanResult instanceof AmazonBarcodeScanResult) { + return [ + 'providerKey' => 'canopy', + 'providerId' => $scanResult->asin, + ]; + } + + return null; + + } + + /** + * @param EIGP114BarcodeScanResult $scanResult + * * @return array{providerKey: string, providerId: string, lotAmount?: float|int, lotName?: string, lotUserBarcode?: string}|null + */ + private function getCreationInfoForEIGP114(EIGP114BarcodeScanResult $scanResult): ?array + { + $vendor = $scanResult->guessBarcodeVendor(); + + // Mouser: use supplierPartNumber -> search provider -> provider_id + if ($vendor === 'mouser' && $scanResult->supplierPartNumber !== null + ) { + // Search Mouser using the MPN + $dtos = $this->infoRetriever->searchByKeyword( + keyword: $scanResult->supplierPartNumber, + providers: ["mouser"] + ); + + // If there are results, provider_id is MouserPartNumber (per MouserProvider.php) + $best = $dtos[0] ?? null; + + if ($best !== null) { + return [ + 'providerKey' => 'mouser', + 'providerId' => $best->provider_id, + 'lotAmount' => $scanResult->quantity, + 'lotName' => $scanResult->customerPO, + 'lotUserBarcode' => $scanResult->rawInput, + ]; + } + + return null; + } + + // Digi-Key: supplierPartNumber directly + if ($vendor === 'digikey') { + return [ + 'providerKey' => 'digikey', + 'providerId' => $scanResult->supplierPartNumber ?? throw new \RuntimeException('Digikey barcode does not contain required supplier part number'), + 'lotAmount' => $scanResult->quantity, + 'lotName' => $scanResult->digikeyInvoiceNumber ?? $scanResult->digikeySalesOrderNumber ?? $scanResult->customerPO, + 'lotUserBarcode' => $scanResult->rawInput, + ]; + } + + // Element14: can use supplierPartNumber directly + if ($vendor === 'element14') { + return [ + 'providerKey' => 'element14', + 'providerId' => $scanResult->supplierPartNumber ?? throw new \RuntimeException('Element14 barcode does not contain required supplier part number'), + 'lotAmount' => $scanResult->quantity, + 'lotName' => $scanResult->customerPO, + 'lotUserBarcode' => $scanResult->rawInput, + ]; + } + + return null; + } + + +} diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php index 88130351..befa91b6 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php @@ -33,4 +33,4 @@ interface BarcodeScanResultInterface * @return array */ public function getDecodedForInfoMode(): array; -} \ No newline at end of file +} diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php index 40f707de..df991a8c 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php @@ -26,20 +26,33 @@ namespace App\Services\LabelSystem\BarcodeScanner; /** * This enum represents the different types, where a barcode/QR-code can be generated from */ -enum BarcodeSourceType +enum BarcodeSourceType: string { /** This Barcode was generated using Part-DB internal recommended barcode generator */ - case INTERNAL; + case INTERNAL = 'internal'; /** This barcode is containing an internal part number (IPN) */ - case IPN; + case IPN = 'ipn'; /** * This barcode is a user defined barcode defined on a part lot */ - case USER_DEFINED; + case USER_DEFINED = 'user'; /** * EIGP114 formatted barcodes like used by digikey, mouser, etc. */ - case EIGP114; -} \ No newline at end of file + case EIGP114 = 'eigp'; + + /** + * GTIN /EAN barcodes, which are used on most products in the world. These are checked with the GTIN field of a part. + */ + case GTIN = 'gtin'; + + /** For LCSC.com formatted QR codes */ + case LCSC = 'lcsc'; + + case AMAZON = 'amazon'; + + /** For TME (tme.eu) formatted QR codes */ + case TME = 'tme'; +} diff --git a/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php index 0b4f4b56..0ff74fd4 100644 --- a/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php +++ b/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php @@ -28,40 +28,40 @@ namespace App\Services\LabelSystem\BarcodeScanner; * Based on PR 811, EIGP 114.2018 (https://www.ecianow.org/assets/docs/GIPC/EIGP-114.2018%20ECIA%20Labeling%20Specification%20for%20Product%20and%20Shipment%20Identification%20in%20the%20Electronics%20Industry%20-%202D%20Barcode.pdf), * , https://forum.digikey.com/t/digikey-product-labels-decoding-digikey-barcodes/41097 */ -class EIGP114BarcodeScanResult implements BarcodeScanResultInterface +readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface { /** * @var string|null Ship date in format YYYYMMDD */ - public readonly ?string $shipDate; + public ?string $shipDate; /** * @var string|null Customer assigned part number – Optional based on * agreements between Distributor and Supplier */ - public readonly ?string $customerPartNumber; + public ?string $customerPartNumber; /** * @var string|null Supplier assigned part number */ - public readonly ?string $supplierPartNumber; + public ?string $supplierPartNumber; /** * @var int|null Quantity of product */ - public readonly ?int $quantity; + public ?int $quantity; /** * @var string|null Customer assigned purchase order number */ - public readonly ?string $customerPO; + public ?string $customerPO; /** * @var string|null Line item number from PO. Required on Logistic Label when * used on back of Packing Slip. See Section 4.9 */ - public readonly ?string $customerPOLine; + public ?string $customerPOLine; /** * 9D - YYWW (Year and Week of Manufacture). ) If no date code is used @@ -69,7 +69,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface * to indicate the product is Not Traceable by this data field. * @var string|null */ - public readonly ?string $dateCode; + public ?string $dateCode; /** * 10D - YYWW (Year and Week of Manufacture). ) If no date code is used @@ -77,7 +77,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface * to indicate the product is Not Traceable by this data field. * @var string|null */ - public readonly ?string $alternativeDateCode; + public ?string $alternativeDateCode; /** * Traceability number assigned to a batch or group of items. If @@ -86,14 +86,14 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface * by this data field. * @var string|null */ - public readonly ?string $lotCode; + public ?string $lotCode; /** * Country where part was manufactured. Two-letter code from * ISO 3166 country code list * @var string|null */ - public readonly ?string $countryOfOrigin; + public ?string $countryOfOrigin; /** * @var string|null Unique alphanumeric number assigned by supplier @@ -101,85 +101,85 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface * Carton. Always used in conjunction with a mixed logistic label * with a 5S data identifier for Package ID. */ - public readonly ?string $packageId1; + public ?string $packageId1; /** * @var string|null * 4S - Package ID for Logistic Carton with like items */ - public readonly ?string $packageId2; + public ?string $packageId2; /** * @var string|null * 5S - Package ID for Logistic Carton with mixed items */ - public readonly ?string $packageId3; + public ?string $packageId3; /** * @var string|null Unique alphanumeric number assigned by supplier. */ - public readonly ?string $packingListNumber; + public ?string $packingListNumber; /** * @var string|null Ship date in format YYYYMMDD */ - public readonly ?string $serialNumber; + public ?string $serialNumber; /** * @var string|null Code for sorting and classifying LEDs. Use when applicable */ - public readonly ?string $binCode; + public ?string $binCode; /** * @var int|null Sequential carton count in format “#/#” or “# of #” */ - public readonly ?int $packageCount; + public ?int $packageCount; /** * @var string|null Alphanumeric string assigned by the supplier to distinguish * from one closely-related design variation to another. Use as * required or when applicable */ - public readonly ?string $revisionNumber; + public ?string $revisionNumber; /** * @var string|null Digikey Extension: This is not represented in the ECIA spec, but the field being used is found in the ANSI MH10.8.2-2016 spec on which the ECIA spec is based. In the ANSI spec it is called First Level (Supplier Assigned) Part Number. */ - public readonly ?string $digikeyPartNumber; + public ?string $digikeyPartNumber; /** * @var string|null Digikey Extension: This can be shared across multiple invoices and time periods and is generated as an order enters our system from any vector (web, API, phone order, etc.) */ - public readonly ?string $digikeySalesOrderNumber; + public ?string $digikeySalesOrderNumber; /** * @var string|null Digikey extension: This is typically assigned per shipment as items are being released to be picked in the warehouse. A SO can have many Invoice numbers */ - public readonly ?string $digikeyInvoiceNumber; + public ?string $digikeyInvoiceNumber; /** * @var string|null Digikey extension: This is for internal DigiKey purposes and defines the label type. */ - public readonly ?string $digikeyLabelType; + public ?string $digikeyLabelType; /** * @var string|null You will also see this as the last part of a URL for a product detail page. Ex https://www.digikey.com/en/products/detail/w%C3%BCrth-elektronik/860010672008/5726907 */ - public readonly ?string $digikeyPartID; + public ?string $digikeyPartID; /** * @var string|null Digikey Extension: For internal use of Digikey. Probably not needed */ - public readonly ?string $digikeyNA; + public ?string $digikeyNA; /** * @var string|null Digikey Extension: This is a field of varying length used to keep the barcode approximately the same size between labels. It is safe to ignore. */ - public readonly ?string $digikeyPadding; + public ?string $digikeyPadding; - public readonly ?string $mouserPositionInOrder; + public ?string $mouserPositionInOrder; - public readonly ?string $mouserManufacturer; + public ?string $mouserManufacturer; @@ -187,7 +187,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface * * @param array $data The fields of the EIGP114 barcode, where the key is the field name and the value is the field content */ - public function __construct(public readonly array $data) + public function __construct(public array $data, public readonly ?string $rawInput = null) { //IDs per EIGP 114.2018 $this->shipDate = $data['6D'] ?? null; @@ -254,12 +254,16 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface */ public static function isFormat06Code(string $input): bool { - //Code must begin with [)>06 - if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")){ - return false; + //Code should begin with [)>06 as per the standard + if(!str_starts_with($input, "[)>\u{1E}06\u{1D}") + // some codes don't contain record separators + && !str_starts_with($input, "[)>06\u{1D}") + // This is found on old Mouser parts + && !str_starts_with($input, ">[)>06\u{1D}")) + { + return false; } - - //Digikey does not put a trailer onto the barcode, so we just check for the header + //Digikey and Mouser don't put a trailer onto the barcode, so we just check for the header return true; } @@ -271,6 +275,8 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface */ public static function parseFormat06Code(string $input): self { + $rawInput = $input; + //Ensure that the input is a valid format06 code if (!self::isFormat06Code($input)) { throw new \InvalidArgumentException("The given input is not a valid format06 code"); @@ -306,7 +312,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface $results[$key] = $fieldValue; } - return new self($results); + return new self($results, $rawInput); } public function getDecodedForInfoMode(): array @@ -329,4 +335,4 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface return $tmp; } -} \ No newline at end of file +} diff --git a/src/Services/LabelSystem/BarcodeScanner/GTINBarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/GTINBarcodeScanResult.php new file mode 100644 index 00000000..30aaa223 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/GTINBarcodeScanResult.php @@ -0,0 +1,62 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +use GtinValidation\GtinValidator; + +readonly class GTINBarcodeScanResult implements BarcodeScanResultInterface +{ + + private GtinValidator $validator; + + public function __construct( + public string $gtin, + ) { + $this->validator = new GtinValidator($this->gtin); + } + + public function getDecodedForInfoMode(): array + { + $obj = $this->validator->getGtinObject(); + return [ + 'GTIN' => $this->gtin, + 'GTIN type' => $obj->getType(), + 'Valid' => $this->validator->isValid() ? 'Yes' : 'No', + ]; + } + + /** + * Checks if the given input is a valid GTIN. This is used to determine whether a scanned barcode should be interpreted as a GTIN or not. + * @param string $input + * @return bool + */ + public static function isValidGTIN(string $input): bool + { + try { + return (new GtinValidator($input))->isValid(); + } catch (\Exception $e) { + return false; + } + } +} diff --git a/src/Services/LabelSystem/BarcodeScanner/LCSCBarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/LCSCBarcodeScanResult.php new file mode 100644 index 00000000..0151cffa --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/LCSCBarcodeScanResult.php @@ -0,0 +1,157 @@ + $fields + */ + public function __construct( + public array $fields, + public string $rawInput, + ) { + + $this->pickBatchNumber = $this->fields['pbn'] ?? null; + $this->orderNumber = $this->fields['on'] ?? null; + $this->lcscCode = $this->fields['pc'] ?? null; + $this->mpn = $this->fields['pm'] ?? null; + $this->quantity = isset($this->fields['qty']) ? (int)$this->fields['qty'] : null; + $this->countryChannel = $this->fields['cc'] ?? null; + $this->warehouseCode = $this->fields['wc'] ?? null; + $this->pdi = $this->fields['pdi'] ?? null; + $this->hp = $this->fields['hp'] ?? null; + + } + + public function getSourceType(): BarcodeSourceType + { + return BarcodeSourceType::LCSC; + } + + /** + * @return array|float[]|int[]|null[]|string[] An array of fields decoded from the barcode + */ + public function getDecodedForInfoMode(): array + { + // Keep it human-friendly + return [ + 'Barcode type' => 'LCSC', + 'MPN (pm)' => $this->mpn ?? '', + 'LCSC code (pc)' => $this->lcscCode ?? '', + 'Qty' => $this->quantity !== null ? (string) $this->quantity : '', + 'Order No (on)' => $this->orderNumber ?? '', + 'Pick Batch (pbn)' => $this->pickBatchNumber ?? '', + 'Warehouse (wc)' => $this->warehouseCode ?? '', + 'Country/Channel (cc)' => $this->countryChannel ?? '', + 'PDI (unknown meaning)' => $this->pdi ?? '', + 'HP (unknown meaning)' => $this->hp ?? '', + ]; + } + + /** + * Parses the barcode data to see if the input matches the expected format used by lcsc.com + * @param string $input + * @return bool + */ + public static function isLCSCBarcode(string $input): bool + { + $s = trim($input); + + // Your example: {pbn:...,on:...,pc:...,pm:...,qty:...} + if (!str_starts_with($s, '{') || !str_ends_with($s, '}')) { + return false; + } + + // Must contain at least pm: and pc: (common for LCSC labels) + return (stripos($s, 'pm:') !== false) && (stripos($s, 'pc:') !== false); + } + + /** + * Parse the barcode input string into the fields used by lcsc.com + * @param string $input + * @return self + */ + public static function parse(string $input): self + { + $raw = trim($input); + + if (!self::isLCSCBarcode($raw)) { + throw new InvalidArgumentException('Not an LCSC barcode'); + } + + $inner = substr($raw, 1, -1); // remove { } + + $fields = []; + + // This format is comma-separated pairs, values do not contain commas in your sample. + $pairs = array_filter( + array_map(trim(...), explode(',', $inner)), + static fn(string $s): bool => $s !== '' + ); + + foreach ($pairs as $pair) { + $pos = strpos($pair, ':'); + if ($pos === false) { + continue; + } + + $k = trim(substr($pair, 0, $pos)); + $v = trim(substr($pair, $pos + 1)); + + if ($k === '') { + continue; + } + + $fields[$k] = $v; + } + + if (!isset($fields['pm']) || trim($fields['pm']) === '') { + throw new InvalidArgumentException('LCSC barcode missing pm field'); + } + + return new self($fields, $raw); + } +} diff --git a/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php index 050aff6f..25fb4710 100644 --- a/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php +++ b/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php @@ -29,12 +29,12 @@ use App\Entity\LabelSystem\LabelSupportedElement; * This class represents the result of a barcode scan of a barcode that uniquely identifies a local entity, * like an internally generated barcode or a barcode that was added manually to the system by a user */ -class LocalBarcodeScanResult implements BarcodeScanResultInterface +readonly class LocalBarcodeScanResult implements BarcodeScanResultInterface { public function __construct( - public readonly LabelSupportedElement $target_type, - public readonly int $target_id, - public readonly BarcodeSourceType $source_type, + public LabelSupportedElement $target_type, + public int $target_id, + public BarcodeSourceType $source_type, ) { } @@ -46,4 +46,4 @@ class LocalBarcodeScanResult implements BarcodeScanResultInterface 'Target ID' => $this->target_id, ]; } -} \ No newline at end of file +} diff --git a/src/Services/LabelSystem/BarcodeScanner/TMEBarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/TMEBarcodeScanResult.php new file mode 100644 index 00000000..5feb67c1 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/TMEBarcodeScanResult.php @@ -0,0 +1,143 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\LabelSystem\BarcodeScanner; + +use InvalidArgumentException; + +/** + * This class represents the content of a tme.eu barcode label. + * The format is space-separated KEY:VALUE tokens, e.g.: + * QTY:1000 PN:SMD0603-5K1-1% PO:32723349/7 MFR:ROYALOHM MPN:0603SAF5101T5E CoO:TH RoHS https://www.tme.eu/details/... + */ +readonly class TMEBarcodeScanResult implements BarcodeScanResultInterface +{ + /** @var int|null Quantity (QTY) */ + public ?int $quantity; + + /** @var string|null TME part number (PN) */ + public ?string $tmePartNumber; + + /** @var string|null Purchase order number (PO) */ + public ?string $purchaseOrder; + + /** @var string|null Manufacturer name (MFR) */ + public ?string $manufacturer; + + /** @var string|null Manufacturer part number (MPN) */ + public ?string $mpn; + + /** @var string|null Country of origin (CoO) */ + public ?string $countryOfOrigin; + + /** @var bool Whether the part is RoHS compliant */ + public bool $rohs; + + /** @var string|null The product URL */ + public ?string $productUrl; + + /** + * @param array $fields Parsed key-value fields (keys uppercased) + * @param string $rawInput Original barcode string + */ + public function __construct( + public array $fields, + public string $rawInput, + ) { + $this->quantity = isset($this->fields['QTY']) ? (int) $this->fields['QTY'] : null; + $this->tmePartNumber = $this->fields['PN'] ?? null; + $this->purchaseOrder = $this->fields['PO'] ?? null; + $this->manufacturer = $this->fields['MFR'] ?? null; + $this->mpn = $this->fields['MPN'] ?? null; + $this->countryOfOrigin = $this->fields['COO'] ?? null; + $this->rohs = isset($this->fields['ROHS']); + $this->productUrl = $this->fields['URL'] ?? null; + } + + public function getSourceType(): BarcodeSourceType + { + return BarcodeSourceType::TME; + } + + public function getDecodedForInfoMode(): array + { + return [ + 'Barcode type' => 'TME', + 'TME Part No. (PN)' => $this->tmePartNumber ?? '', + 'MPN' => $this->mpn ?? '', + 'Manufacturer (MFR)' => $this->manufacturer ?? '', + 'Qty' => $this->quantity !== null ? (string) $this->quantity : '', + 'Purchase Order (PO)' => $this->purchaseOrder ?? '', + 'Country of Origin (CoO)' => $this->countryOfOrigin ?? '', + 'RoHS' => $this->rohs ? 'Yes' : 'No', + 'URL' => $this->productUrl ?? '', + ]; + } + + /** + * Returns true if the input looks like a TME barcode label (contains tme.eu URL). + */ + public static function isTMEBarcode(string $input): bool + { + return str_contains(strtolower($input), 'tme.eu'); + } + + /** + * Parse the TME barcode string into a TMEBarcodeScanResult. + */ + public static function parse(string $input): self + { + $raw = trim($input); + + if (!self::isTMEBarcode($raw)) { + throw new InvalidArgumentException('Not a TME barcode'); + } + + $fields = []; + + // Split on whitespace; each token is either KEY:VALUE, a bare keyword, or the URL + $tokens = preg_split('/\s+/', $raw); + foreach ($tokens as $token) { + if ($token === '') { + continue; + } + + // The TME URL + if (str_starts_with(strtolower($token), 'http')) { + $fields['URL'] = $token; + continue; + } + + $colonPos = strpos($token, ':'); + if ($colonPos !== false) { + $key = strtoupper(substr($token, 0, $colonPos)); + $value = substr($token, $colonPos + 1); + $fields[$key] = $value; + } else { + // Bare keyword like "RoHS" + $fields[strtoupper($token)] = ''; + } + } + + return new self($fields, $raw); + } +} diff --git a/src/Services/LabelSystem/LabelHTMLGenerator.php b/src/Services/LabelSystem/LabelHTMLGenerator.php index 8a5201ff..31093953 100644 --- a/src/Services/LabelSystem/LabelHTMLGenerator.php +++ b/src/Services/LabelSystem/LabelHTMLGenerator.php @@ -95,7 +95,7 @@ final class LabelHTMLGenerator 'paper_height' => $options->getHeight(), ] ); - } catch (Error $exception) { + } catch (\Throwable $exception) { throw new TwigModeException($exception); } } else { diff --git a/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php b/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php index 400fef35..9719917a 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php @@ -114,10 +114,12 @@ final class BarcodeProvider implements PlaceholderProviderInterface return 'IPN Barcode ERROR!: '.$e->getMessage(); } } - - - - return null; } + + public static function getDefaultPriority(): int + { + //This provider should be checked before all others, so that nothing is delegated for part lots + return 1000; + } } diff --git a/src/Services/LabelSystem/SandboxedTwigFactory.php b/src/Services/LabelSystem/SandboxedTwigFactory.php index 32b50e06..3383df2e 100644 --- a/src/Services/LabelSystem/SandboxedTwigFactory.php +++ b/src/Services/LabelSystem/SandboxedTwigFactory.php @@ -70,12 +70,14 @@ use App\Twig\Sandbox\SandboxedLabelExtension; use App\Twig\TwigCoreExtension; use InvalidArgumentException; use Twig\Environment; +use Twig\Extension\AttributeExtension; use Twig\Extension\SandboxExtension; use Twig\Extra\Html\HtmlExtension; use Twig\Extra\Intl\IntlExtension; use Twig\Extra\Markdown\MarkdownExtension; use Twig\Extra\String\StringExtension; use Twig\Loader\ArrayLoader; +use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\Sandbox\SecurityPolicyInterface; /** @@ -84,11 +86,11 @@ use Twig\Sandbox\SecurityPolicyInterface; */ final class SandboxedTwigFactory { - private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'verbatim', 'with']; + private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'types', 'verbatim', 'with']; private const ALLOWED_FILTERS = ['abs', 'batch', 'capitalize', 'column', 'country_name', - 'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'first', 'format', + 'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'find', 'first', 'format', 'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'html_to_markdown', 'join', 'keys', - 'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'raw', 'number_format', + 'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'number_format', 'raw', 'reduce', 'replace', 'reverse', 'round', 'slice', 'slug', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title', 'trim', 'u', 'upper', 'url_encode', @@ -102,16 +104,17 @@ final class SandboxedTwigFactory ]; private const ALLOWED_FUNCTIONS = ['country_names', 'country_timezones', 'currency_names', 'cycle', - 'date', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names', - 'template_from_string', 'timezone_names', + 'date', 'enum', 'enum_cases', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names', + 'timezone_names', //Part-DB specific extensions: //EntityExtension: - 'entity_type', 'entity_url', + 'entity_type', 'entity_url', 'type_label', 'type_label_plural', //BarcodeExtension: 'barcode_svg', //SandboxedLabelExtension 'placeholder', + 'associated_parts', 'associated_parts_count', 'associated_parts_r', 'associated_parts_count_r', ]; private const ALLOWED_METHODS = [ @@ -128,24 +131,24 @@ final class SandboxedTwigFactory 'getValueTypical', 'getUnit', 'getValueText', ], MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'], PartLot::class => ['isExpired', 'getDescription', 'getComment', 'getExpirationDate', 'getStorageLocation', - 'getPart', 'isInstockUnknown', 'getAmount', 'getNeedsRefill', 'getVendorBarcode'], + 'getPart', 'isInstockUnknown', 'getAmount', 'getOwner', 'getLastStocktakeAt', 'getNeedsRefill', 'getVendorBarcode'], StorageLocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'], Supplier::class => ['getShippingCosts', 'getDefaultCurrency'], Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference', 'getDescription', 'getComment', 'isFavorite', 'getCategory', 'getFootprint', - 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getOrderAmount', 'getOrderDelivery', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', + 'getPartLots', 'getPartUnit', 'getPartCustomState', 'useFloatAmount', 'getMinAmount', 'getOrderAmount', 'getOrderDelivery', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', 'getManufacturerProductUrl', 'getCustomProductURL', 'getManufacturingStatus', 'getManufacturer', 'getManufacturerProductNumber', 'getOrderdetails', 'isObsolete', 'getParameters', 'getGroupedParameters', 'isProjectBuildPart', 'getBuiltProject', 'getAssociatedPartsAsOwner', 'getAssociatedPartsAsOther', 'getAssociatedPartsAll', - 'getEdaInfo' + 'getEdaInfo', 'getGtin' ], Currency::class => ['getIsoCode', 'getInverseExchangeRate', 'getExchangeRate'], Orderdetail::class => ['getPart', 'getSupplier', 'getSupplierPartNr', 'getObsolete', - 'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl'], + 'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl', 'getPricesIncludesVAT'], Pricedetail::class => ['getOrderdetail', 'getPrice', 'getPricePerUnit', 'getPriceRelatedQuantity', - 'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode'], + 'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode', 'getIncludesVat'], InfoProviderReference:: class => ['getProviderKey', 'getProviderId', 'getProviderUrl', 'getLastUpdated', 'isProviderCreated'], PartAssociation::class => ['getType', 'getComment', 'getOwner', 'getOther', 'getOtherType'], @@ -186,13 +189,18 @@ final class SandboxedTwigFactory $twig->addExtension(new StringExtension()); $twig->addExtension(new HtmlExtension()); - //Add Part-DB specific extension - $twig->addExtension($this->formatExtension); - $twig->addExtension($this->barcodeExtension); - $twig->addExtension($this->entityExtension); - $twig->addExtension($this->twigCoreExtension); $twig->addExtension($this->sandboxedLabelExtension); + //Our other extensions are using attributes, we need a bit more work to load them + $dynamicExtensions = [$this->formatExtension, $this->barcodeExtension, $this->entityExtension, $this->twigCoreExtension]; + $dynamicExtensionsMap = []; + + foreach ($dynamicExtensions as $extension) { + $twig->addExtension(new AttributeExtension($extension::class)); + $dynamicExtensionsMap[$extension::class] = static fn () => $extension; + } + $twig->addRuntimeLoader(new FactoryRuntimeLoader($dynamicExtensionsMap)); + return $twig; } diff --git a/src/Services/LogSystem/LogDiffFormatter.php b/src/Services/LogSystem/LogDiffFormatter.php index 8b165d5a..1ac5a2f5 100644 --- a/src/Services/LogSystem/LogDiffFormatter.php +++ b/src/Services/LogSystem/LogDiffFormatter.php @@ -32,7 +32,7 @@ class LogDiffFormatter * @param $old_data * @param $new_data */ - public function formatDiff($old_data, $new_data): string + public function formatDiff(mixed $old_data, mixed $new_data): string { if (is_string($old_data) && is_string($new_data)) { return $this->diffString($old_data, $new_data); diff --git a/src/Services/LogSystem/TimeTravel.php b/src/Services/LogSystem/TimeTravel.php index 68d962bb..79d9f8e2 100644 --- a/src/Services/LogSystem/TimeTravel.php +++ b/src/Services/LogSystem/TimeTravel.php @@ -222,7 +222,7 @@ class TimeTravel if (isset($metadata->fieldMappings[$field])) { //We need to convert the string to a BigDecimal first if (!$data instanceof BigDecimal && ('big_decimal' === $metadata->getFieldMapping($field)->type)) { - $data = BigDecimal::of($data); + $data = is_float($data) ? BigDecimal::fromFloatShortest($data) : BigDecimal::of($data); } if (!$data instanceof \DateTimeInterface diff --git a/src/Services/Misc/GitVersionInfo.php b/src/Services/Misc/GitVersionInfo.php deleted file mode 100644 index 3c079f4f..00000000 --- a/src/Services/Misc/GitVersionInfo.php +++ /dev/null @@ -1,83 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Services\Misc; - -use Symfony\Component\HttpKernel\KernelInterface; - -class GitVersionInfo -{ - protected string $project_dir; - - public function __construct(KernelInterface $kernel) - { - $this->project_dir = $kernel->getProjectDir(); - } - - /** - * Get the Git branch name of the installed system. - * - * @return string|null The current git branch name. Null, if this is no Git installation - */ - public function getGitBranchName(): ?string - { - if (is_file($this->project_dir.'/.git/HEAD')) { - $git = file($this->project_dir.'/.git/HEAD'); - $head = explode('/', $git[0], 3); - - if (!isset($head[2])) { - return null; - } - - return trim($head[2]); - } - - return null; // this is not a Git installation - } - - /** - * Get hash of the last git commit (on remote "origin"!). - * - * If this method does not work, try to make a "git pull" first! - * - * @param int $length if this is smaller than 40, only the first $length characters will be returned - * - * @return string|null The hash of the last commit, null If this is no Git installation - */ - public function getGitCommitHash(int $length = 7): ?string - { - $filename = $this->project_dir.'/.git/refs/remotes/origin/'.$this->getGitBranchName(); - if (is_file($filename)) { - $head = file($filename); - - if (!isset($head[0])) { - return null; - } - - $hash = $head[0]; - - return substr($hash, 0, $length); - } - - return null; // this is not a Git installation - } -} diff --git a/src/Services/Parts/PartLotWithdrawAddHelper.php b/src/Services/Parts/PartLotWithdrawAddHelper.php index 34ec4c1d..d6a95b34 100644 --- a/src/Services/Parts/PartLotWithdrawAddHelper.php +++ b/src/Services/Parts/PartLotWithdrawAddHelper.php @@ -197,4 +197,45 @@ final class PartLotWithdrawAddHelper $this->entityManager->remove($origin); } } + + /** + * Perform a stocktake for the given part lot, setting the amount to the given actual amount. + * Please note that the changes are not flushed to DB yet, you have to do this yourself + * @param PartLot $lot + * @param float $actualAmount + * @param string|null $comment + * @param \DateTimeInterface|null $action_timestamp + * @return void + */ + public function stocktake(PartLot $lot, float $actualAmount, ?string $comment = null, ?\DateTimeInterface $action_timestamp = null): void + { + if ($actualAmount < 0) { + throw new \InvalidArgumentException('Actual amount must be non-negative'); + } + + $part = $lot->getPart(); + + //Check whether we have to round the amount + if (!$part->useFloatAmount()) { + $actualAmount = round($actualAmount); + } + + $oldAmount = $lot->getAmount(); + //Clear any unknown status when doing a stocktake, as we now have a known amount + $lot->setInstockUnknown(false); + $lot->setAmount($actualAmount); + if ($action_timestamp) { + $lot->setLastStocktakeAt(\DateTimeImmutable::createFromInterface($action_timestamp)); + } else { + $lot->setLastStocktakeAt(new \DateTimeImmutable()); //Use now if no timestamp is given + } + + $event = PartStockChangedLogEntry::stocktake($lot, $oldAmount, $lot->getAmount(), $part->getAmountSum() , $comment, $action_timestamp); + $this->eventLogger->log($event); + + //Apply the comment also to global events, so it gets associated with the elementChanged log entry + if (!$this->eventCommentHelper->isMessageSet() && ($comment !== null && $comment !== '')) { + $this->eventCommentHelper->setMessage($comment); + } + } } diff --git a/src/Services/Parts/PartsTableActionHandler.php b/src/Services/Parts/PartsTableActionHandler.php index 945cff7b..b0353e29 100644 --- a/src/Services/Parts/PartsTableActionHandler.php +++ b/src/Services/Parts/PartsTableActionHandler.php @@ -127,6 +127,15 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart ); } + if ($action === 'batch_edit_eda') { + $ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts)); + return new RedirectResponse( + $this->urlGenerator->generate('batch_eda_edit', [ + 'ids' => $ids, + '_redirect' => $redirect_url + ]) + ); + } //Iterate over the parts and apply the action to it: foreach ($selected_parts as $part) { diff --git a/src/Services/Parts/PricedetailHelper.php b/src/Services/Parts/PricedetailHelper.php index b2e1340f..e40fc44d 100644 --- a/src/Services/Parts/PricedetailHelper.php +++ b/src/Services/Parts/PricedetailHelper.php @@ -170,7 +170,7 @@ class PricedetailHelper return null; } - return $avg->dividedBy($count, Pricedetail::PRICE_PRECISION, RoundingMode::HALF_UP); + return $avg->dividedBy($count, Pricedetail::PRICE_PRECISION, RoundingMode::HalfUp); } /** @@ -213,6 +213,6 @@ class PricedetailHelper $val_target = $val_base->multipliedBy($targetCurrency->getInverseExchangeRate()); } - return $val_target->toScale(Pricedetail::PRICE_PRECISION, RoundingMode::HALF_UP); + return $val_target->toScale(Pricedetail::PRICE_PRECISION, RoundingMode::HalfUp); } } diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php index a541c29d..e011d980 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -25,16 +25,22 @@ namespace App\Services\ProjectSystem; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Entity\PriceInformations\Currency; use App\Helpers\Projects\ProjectBuildRequest; use App\Services\Parts\PartLotWithdrawAddHelper; +use App\Services\Parts\PricedetailHelper; +use Brick\Math\BigDecimal; +use Brick\Math\RoundingMode; /** * @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest */ final readonly class ProjectBuildHelper { - public function __construct(private PartLotWithdrawAddHelper $withdraw_add_helper) - { + public function __construct( + private PartLotWithdrawAddHelper $withdraw_add_helper, + private PricedetailHelper $pricedetailHelper, + ) { } /** @@ -168,4 +174,81 @@ final readonly class ProjectBuildHelper $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); } } + + /** + * Calculates the total price to build the given project N times, taking bulk pricing into account. + * Returns null if no BOM entry has any pricing information. + */ + public function calculateTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal + { + $total = BigDecimal::zero(); + $has_price = false; + + foreach ($project->getBomEntries() as $entry) { + $unit_price = $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency); + if ($unit_price === null) { + continue; + } + $has_price = true; + $total = $total->plus($unit_price->multipliedBy(BigDecimal::fromFloatShortest($entry->getQuantity()))->multipliedBy($number_of_builds)); + } + + return $has_price ? $total : null; + } + + /** + * Calculates the price to build one unit of the given project when ordering for N builds in total. + * Returns null if no BOM entry has any pricing information. + */ + public function calculateUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal + { + $total = $this->calculateTotalBuildPrice($project, $number_of_builds, $currency); + if ($total === null) { + return null; + } + return $total->dividedBy($number_of_builds, 10, RoundingMode::HalfUp); + } + + /** + * Returns the total build price rounded up to 2 decimal places, ready for display. + */ + public function roundedTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal + { + return $this->calculateTotalBuildPrice($project, $number_of_builds, $currency) + ?->toScale(2, RoundingMode::Up); + } + + /** + * Returns the unit build price rounded up to 2 decimal places, ready for display. + */ + public function roundedUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal + { + return $this->calculateUnitBuildPrice($project, $number_of_builds, $currency) + ?->toScale(2, RoundingMode::Up); + } + + /** + * Returns the effective unit price for a single piece of the given BOM entry, + * taking bulk pricing and minimum order amounts into account for N builds. + * Returns BigDecimal::zero() when no pricing data is available. + */ + public function getEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds = 1, ?Currency $currency = null): BigDecimal + { + return $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency) ?? BigDecimal::zero(); + } + + /** + * Returns the effective unit price for a single piece of the given BOM entry, + * taking bulk pricing into account for N builds. + */ + private function getBomEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds, ?Currency $currency): ?BigDecimal + { + if ($entry->getPart() instanceof Part) { + $total_qty = $entry->getQuantity() * $number_of_builds; + $min_order = $this->pricedetailHelper->getMinOrderAmount($entry->getPart()); + $effective_qty = ($min_order !== null) ? max($total_qty, $min_order) : $total_qty; + return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $effective_qty, $currency); + } + return $entry->getPrice(); + } } diff --git a/src/Services/System/BackupManager.php b/src/Services/System/BackupManager.php new file mode 100644 index 00000000..621b58d7 --- /dev/null +++ b/src/Services/System/BackupManager.php @@ -0,0 +1,456 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\System; + +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; +use Shivas\VersioningBundle\Service\VersionManagerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; + +/** + * Manages Part-DB backups: creation, restoration, and listing. + * + * This service handles all backup-related operations and can be used + * by the Update Manager, CLI commands, or other services. + */ +readonly class BackupManager +{ + private const BACKUP_DIR = 'var/backups'; + + public function __construct( + #[Autowire(param: 'kernel.project_dir')] + private string $projectDir, + private LoggerInterface $logger, + private Filesystem $filesystem, + private VersionManagerInterface $versionManager, + private EntityManagerInterface $entityManager, + private CommandRunHelper $commandRunHelper, + ) { + } + + /** + * Get the backup directory path. + */ + public function getBackupDir(): string + { + return $this->projectDir . '/' . self::BACKUP_DIR; + } + + /** + * Get the current version string for use in filenames. + */ + private function getCurrentVersionString(): string + { + return $this->versionManager->getVersion()->toString(); + } + + /** + * Create a backup before updating. + * + * @param string|null $targetVersion Optional target version for naming + * @param string|null $prefix Optional prefix for the backup filename + * @return string The path to the created backup file + */ + public function createBackup(?string $targetVersion = null, ?string $prefix = 'backup'): string + { + $backupDir = $this->getBackupDir(); + + if (!is_dir($backupDir)) { + $this->filesystem->mkdir($backupDir, 0755); + } + + $currentVersion = $this->getCurrentVersionString(); + + // Build filename + if ($targetVersion) { + $targetVersionClean = preg_replace('/[^a-zA-Z0-9\.]/', '', $targetVersion); + $backupFile = $backupDir . '/pre-update-v' . $currentVersion . '-to-' . $targetVersionClean . '-' . date('Y-m-d-His') . '.zip'; + } else { + $backupFile = $backupDir . '/' . $prefix . '-v' . $currentVersion . '-' . date('Y-m-d-His') . '.zip'; + } + + $this->commandRunHelper->runCommand([ + 'php', 'bin/console', 'partdb:backup', + '--full', + '--overwrite', + $backupFile, + ], 'Create backup', 600); + + $this->logger->info('Created backup', ['file' => $backupFile]); + + return $backupFile; + } + + /** + * Get list of backups, that are available, sorted by date descending. + * + * @return array + */ + public function getBackups(): array + { + $backupDir = $this->getBackupDir(); + + if (!is_dir($backupDir)) { + return []; + } + + $backups = []; + foreach (glob($backupDir . '/*.zip') as $backupFile) { + $backups[] = [ + 'file' => basename($backupFile), + 'path' => $backupFile, + 'date' => filemtime($backupFile), + 'size' => filesize($backupFile), + ]; + } + + // Sort by date descending + usort($backups, static fn($a, $b) => $b['date'] <=> $a['date']); + + return $backups; + } + + /** + * Get details about a specific backup file. + * + * @param string $filename The backup filename + * @return null|array{file: string, path: string, date: int, size: int, from_version: ?string, to_version: ?string, contains_database?: bool, contains_config?: bool, contains_attachments?: bool} Backup details or null if not found + */ + public function getBackupDetails(string $filename): ?array + { + $backupDir = $this->getBackupDir(); + $backupPath = $backupDir . '/' . basename($filename); + + if (!file_exists($backupPath) || !str_ends_with($backupPath, '.zip')) { + return null; + } + + // Parse version info from filename: pre-update-v2.5.1-to-v2.5.0-2024-01-30-185400.zip + $info = [ + 'file' => basename($backupPath), + 'path' => $backupPath, + 'date' => filemtime($backupPath), + 'size' => filesize($backupPath), + 'from_version' => null, + 'to_version' => null, + ]; + + if (preg_match('/pre-update-v([\d.]+)-to-v?([\d.]+)-/', $filename, $matches)) { + $info['from_version'] = $matches[1]; + $info['to_version'] = $matches[2]; + } + + // Check what the backup contains by reading the ZIP + try { + $zip = new \ZipArchive(); + if ($zip->open($backupPath) === true) { + $info['contains_database'] = $zip->locateName('database.sql') !== false || $zip->locateName('var/app.db') !== false; + $info['contains_config'] = $zip->locateName('.env.local') !== false || $zip->locateName('config/parameters.yaml') !== false; + $info['contains_attachments'] = $zip->locateName('public/media/') !== false || $zip->locateName('uploads/') !== false; + $zip->close(); + } + } catch (\Exception $e) { + $this->logger->warning('Could not read backup ZIP contents', ['error' => $e->getMessage()]); + } + + return $info; + } + + /** + * Delete a backup file. + * + * @param string $filename The backup filename to delete + * @return bool True if deleted successfully + */ + public function deleteBackup(string $filename): bool + { + $backupDir = $this->getBackupDir(); + $backupPath = $backupDir . '/' . basename($filename); + + if (!file_exists($backupPath) || !str_ends_with($backupPath, '.zip')) { + return false; + } + + try { + $this->filesystem->remove($backupPath); + $this->logger->info('Deleted backup', ['file' => $filename]); + return true; + } catch (\Exception $e) { + $this->logger->error('Failed to delete backup', ['file' => $filename, 'error' => $e->getMessage()]); + return false; + } + } + + /** + * Restore from a backup file. + * + * @param string $filename The backup filename to restore + * @param bool $restoreDatabase Whether to restore the database + * @param bool $restoreConfig Whether to restore config files + * @param bool $restoreAttachments Whether to restore attachments + * @param callable|null $onProgress Callback for progress updates + * @return array{success: bool, steps: array, error: ?string} + */ + public function restoreBackup( + string $filename, + bool $restoreDatabase = true, + bool $restoreConfig = false, + bool $restoreAttachments = false, + ?callable $onProgress = null + ): array { + $steps = []; + $startTime = microtime(true); + + $log = function (string $step, string $message, bool $success, ?float $duration = null) use (&$steps, $onProgress): void { + $entry = [ + 'step' => $step, + 'message' => $message, + 'success' => $success, + 'timestamp' => (new \DateTime())->format('c'), + 'duration' => $duration, + ]; + $steps[] = $entry; + $this->logger->info('[Restore] ' . $step . ': ' . $message, ['success' => $success]); + + if ($onProgress) { + $onProgress($entry); + } + }; + + try { + // Validate backup file + $backupDir = $this->getBackupDir(); + $backupPath = $backupDir . '/' . basename($filename); + + if (!file_exists($backupPath)) { + throw new \RuntimeException('Backup file not found: ' . $filename); + } + + $stepStart = microtime(true); + + // Step 1: Extract backup to temp directory + $tempDir = sys_get_temp_dir() . '/partdb_restore_' . uniqid(); + $this->filesystem->mkdir($tempDir); + + $zip = new \ZipArchive(); + if ($zip->open($backupPath) !== true) { + throw new \RuntimeException('Could not open backup ZIP file'); + } + $zip->extractTo($tempDir); + $zip->close(); + $log('extract', 'Extracted backup to temporary directory', true, microtime(true) - $stepStart); + + // Step 2: Restore database if requested and present + if ($restoreDatabase) { + $stepStart = microtime(true); + $this->restoreDatabaseFromBackup($tempDir); + $log('database', 'Restored database', true, microtime(true) - $stepStart); + } + + // Step 3: Restore config files if requested and present + if ($restoreConfig) { + $stepStart = microtime(true); + $this->restoreConfigFromBackup($tempDir); + $log('config', 'Restored configuration files', true, microtime(true) - $stepStart); + } + + // Step 4: Restore attachments if requested and present + if ($restoreAttachments) { + $stepStart = microtime(true); + $this->restoreAttachmentsFromBackup($tempDir); + $log('attachments', 'Restored attachments', true, microtime(true) - $stepStart); + } + + // Step 5: Clean up temp directory + $stepStart = microtime(true); + $this->filesystem->remove($tempDir); + $log('cleanup', 'Cleaned up temporary files', true, microtime(true) - $stepStart); + + $totalDuration = microtime(true) - $startTime; + $log('complete', sprintf('Restore completed successfully in %.1f seconds', $totalDuration), true); + + return [ + 'success' => true, + 'steps' => $steps, + 'error' => null, + ]; + + } catch (\Throwable $e) { + $this->logger->error('Restore failed: ' . $e->getMessage(), [ + 'exception' => $e, + 'file' => $filename, + ]); + + // Try to clean up + try { + if (isset($tempDir) && is_dir($tempDir)) { + $this->filesystem->remove($tempDir); + } + } catch (\Throwable $cleanupError) { + $this->logger->error('Cleanup after failed restore also failed', ['error' => $cleanupError->getMessage()]); + } + + return [ + 'success' => false, + 'steps' => $steps, + 'error' => $e->getMessage(), + ]; + } + } + + /** + * Restore database from backup. + */ + private function restoreDatabaseFromBackup(string $tempDir): void + { + // Get database connection params from Doctrine + $connection = $this->entityManager->getConnection(); + $params = $connection->getParams(); + $platform = $connection->getDatabasePlatform(); + + // Check for SQL dump (MySQL/PostgreSQL) + $sqlFile = $tempDir . '/database.sql'; + if (file_exists($sqlFile)) { + + if ($platform instanceof AbstractMySQLPlatform) { + // Use mysql command to import - need to use shell to handle input redirection + $mysqlCmd = 'mysql'; + if (isset($params['host'])) { + $mysqlCmd .= ' -h ' . escapeshellarg($params['host']); + } + if (isset($params['port'])) { + $mysqlCmd .= ' -P ' . escapeshellarg((string)$params['port']); + } + if (isset($params['user'])) { + $mysqlCmd .= ' -u ' . escapeshellarg($params['user']); + } + if (isset($params['password']) && $params['password']) { + $mysqlCmd .= ' -p' . escapeshellarg($params['password']); + } + if (isset($params['dbname'])) { + $mysqlCmd .= ' ' . escapeshellarg($params['dbname']); + } + $mysqlCmd .= ' < ' . escapeshellarg($sqlFile); + + // Execute using shell + $process = Process::fromShellCommandline($mysqlCmd, $this->projectDir, null, null, 300); + $process->run(); + + if (!$process->isSuccessful()) { + throw new \RuntimeException('MySQL import failed: ' . $process->getErrorOutput()); + } + } elseif ($platform instanceof PostgreSQLPlatform) { + // Use psql command to import + $psqlCmd = 'psql'; + if (isset($params['host'])) { + $psqlCmd .= ' -h ' . escapeshellarg($params['host']); + } + if (isset($params['port'])) { + $psqlCmd .= ' -p ' . escapeshellarg((string)$params['port']); + } + if (isset($params['user'])) { + $psqlCmd .= ' -U ' . escapeshellarg($params['user']); + } + if (isset($params['dbname'])) { + $psqlCmd .= ' -d ' . escapeshellarg($params['dbname']); + } + $psqlCmd .= ' -f ' . escapeshellarg($sqlFile); + + // Set PGPASSWORD environment variable if password is provided + $env = null; + if (isset($params['password']) && $params['password']) { + $env = ['PGPASSWORD' => $params['password']]; + } + + // Execute using shell + $process = Process::fromShellCommandline($psqlCmd, $this->projectDir, $env, null, 300); + $process->run(); + + if (!$process->isSuccessful()) { + throw new \RuntimeException('PostgreSQL import failed: ' . $process->getErrorOutput()); + } + } else { + throw new \RuntimeException('Unsupported database platform for restore'); + } + + return; + } + + // Check for SQLite database file + $sqliteFile = $tempDir . '/var/app.db'; + if (file_exists($sqliteFile)) { + // Use the actual configured SQLite path from Doctrine, not a hardcoded path + $targetDb = $params['path'] ?? $this->projectDir . '/var/app.db'; + $this->filesystem->copy($sqliteFile, $targetDb, true); + return; + } + + $this->logger->warning('No database found in backup'); + } + + /** + * Restore config files from backup. + */ + private function restoreConfigFromBackup(string $tempDir): void + { + // Restore .env.local + $envLocal = $tempDir . '/.env.local'; + if (file_exists($envLocal)) { + $this->filesystem->copy($envLocal, $this->projectDir . '/.env.local', true); + } + + // Restore config/parameters.yaml + $parametersYaml = $tempDir . '/config/parameters.yaml'; + if (file_exists($parametersYaml)) { + $this->filesystem->copy($parametersYaml, $this->projectDir . '/config/parameters.yaml', true); + } + + // Restore config/banner.md + $bannerMd = $tempDir . '/config/banner.md'; + if (file_exists($bannerMd)) { + $this->filesystem->copy($bannerMd, $this->projectDir . '/config/banner.md', true); + } + } + + /** + * Restore attachments from backup. + */ + private function restoreAttachmentsFromBackup(string $tempDir): void + { + // Restore public/media + $publicMedia = $tempDir . '/public/media'; + if (is_dir($publicMedia)) { + $this->filesystem->mirror($publicMedia, $this->projectDir . '/public/media', null, ['override' => true]); + } + + // Restore uploads + $uploads = $tempDir . '/uploads'; + if (is_dir($uploads)) { + $this->filesystem->mirror($uploads, $this->projectDir . '/uploads', null, ['override' => true]); + } + } +} diff --git a/src/Services/System/CommandRunHelper.php b/src/Services/System/CommandRunHelper.php new file mode 100644 index 00000000..19bc8548 --- /dev/null +++ b/src/Services/System/CommandRunHelper.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\System; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Process\Process; + +class CommandRunHelper +{ + public function __construct( + #[Autowire(param: 'kernel.project_dir')] private readonly string $project_dir + ) + { + } + + /** + * Run a shell command with proper error handling. + */ + public function runCommand(array $command, string $description, int $timeout = 120): string + { + $process = new Process($command, $this->project_dir); + $process->setTimeout($timeout); + + // Set environment variables needed for Composer and other tools + // This is especially important when running as www-data which may not have HOME set + // We inherit from current environment and override/add specific variables + $currentEnv = getenv(); + if (!is_array($currentEnv)) { + $currentEnv = []; + } + $env = array_merge($currentEnv, [ + 'HOME' => $this->project_dir.'/var/www-data-home', + 'COMPOSER_HOME' => $this->project_dir.'/var/composer', + 'PATH' => getenv('PATH') ?: '/usr/local/bin:/usr/bin:/bin', + ]); + $process->setEnv($env); + + $output = ''; + $process->run(function ($type, $buffer) use (&$output) { + $output .= $buffer; + }); + + if (!$process->isSuccessful()) { + $errorOutput = $process->getErrorOutput() ?: $process->getOutput(); + throw new \RuntimeException( + sprintf('%s failed: %s', $description, trim($errorOutput)) + ); + } + + return $output; + } +} diff --git a/src/Services/System/GitVersionInfoProvider.php b/src/Services/System/GitVersionInfoProvider.php new file mode 100644 index 00000000..927326e5 --- /dev/null +++ b/src/Services/System/GitVersionInfoProvider.php @@ -0,0 +1,144 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\System; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Process\Process; + +/** + * This service provides information about the current Git installation (if any). + */ +final readonly class GitVersionInfoProvider +{ + public function __construct(#[Autowire(param: 'kernel.project_dir')] private string $project_dir) + { + } + + /** + * Check if the project directory is a Git repository. + * @return bool + */ + public function isGitRepo(): bool + { + return is_dir($this->getGitDirectory()); + } + + /** + * Get the path to the Git directory of the installed system without a trailing slash. + * Even if this is no Git installation, the path is returned. + * @return string The path to the Git directory of the installed system + */ + public function getGitDirectory(): string + { + return $this->project_dir . '/.git'; + } + + /** + * Get the Git branch name of the installed system. + * + * @return string|null The current git branch name. Null, if this is no Git installation + */ + public function getBranchName(): ?string + { + if (is_file($this->getGitDirectory() . '/HEAD')) { + $git = file($this->getGitDirectory() . '/HEAD'); + if ($git === false) { + return null; + } + $head = explode('/', $git[0], 3); + + if (!isset($head[2])) { + return null; + } + + return trim($head[2]); + } + + return null; // this is not a Git installation + } + + /** + * Get hash of the last git commit (on remote "origin"!). + * + * If this method does not work, try to make a "git pull" first! + * + * @param int $length if this is smaller than 40, only the first $length characters will be returned + * + * @return string|null The hash of the last commit, null If this is no Git installation + */ + public function getCommitHash(int $length = 8): ?string + { + $path = $this->getGitDirectory() . '/HEAD'; + if (!file_exists($path)) { + return null; + } + + $head = trim(file_get_contents($path)); + + // If it's a symbolic ref (e.g., "ref: refs/heads/main") + if (str_starts_with($head, 'ref:')) { + $refPath = $this->getGitDirectory() . '/' . trim(substr($head, 5)); + if (file_exists($refPath)) { + $hash = trim(file_get_contents($refPath)); + } + } else { + // Otherwise, it's a detached HEAD (the hash is right there) + $hash = $head; + } + + return isset($hash) ? substr($hash, 0, $length) : null; + + } + + /** + * Get the Git remote URL of the installed system. + */ + public function getRemoteURL(): ?string + { + // Get remote URL + $configFile = $this->getGitDirectory() . '/config'; + if (file_exists($configFile)) { + $config = file_get_contents($configFile); + if (preg_match('#url = (.+)#', $config, $matches)) { + return trim($matches[1]); + } + } + + return null; // this is not a Git installation + } + + /** + * Check if there are local changes in the Git repository. + * Attention: This runs a git command, which might be slow! + * @return bool|null True if there are local changes, false if not, null if this is not a Git installation + */ + public function hasLocalChanges(): ?bool + { + $process = new Process(['git', 'status', '--porcelain'], $this->project_dir); + $process->run(); + if (!$process->isSuccessful()) { + return null; // this is not a Git installation + } + return !empty(trim($process->getOutput())); + } +} diff --git a/src/Services/System/InstallationType.php b/src/Services/System/InstallationType.php new file mode 100644 index 00000000..2631e644 --- /dev/null +++ b/src/Services/System/InstallationType.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\System; + +/** + * Detects the installation type of Part-DB to determine the appropriate update strategy. + */ +enum InstallationType: string +{ + case GIT = 'git'; + case DOCKER = 'docker'; + case ZIP_RELEASE = 'zip_release'; + case UNKNOWN = 'unknown'; + + public function getLabel(): string + { + return match ($this) { + self::GIT => 'Git Clone', + self::DOCKER => 'Docker', + self::ZIP_RELEASE => 'Release Archive (ZIP File)', + self::UNKNOWN => 'Unknown', + }; + } + + public function supportsAutoUpdate(): bool + { + return match ($this) { + self::GIT => true, + self::DOCKER => true, + // ZIP_RELEASE auto-update not yet implemented + self::ZIP_RELEASE => false, + self::UNKNOWN => false, + }; + } + + public function getUpdateInstructions(): string + { + return match ($this) { + self::GIT => 'Run: php bin/console partdb:update', + self::DOCKER => 'Configure Watchtower for one-click updates, or manually: docker-compose pull && docker-compose up -d', + self::ZIP_RELEASE => 'Download the new release ZIP from GitHub, extract it over your installation, and run: php bin/console doctrine:migrations:migrate && php bin/console cache:clear', + self::UNKNOWN => 'Unable to determine installation type. Please update manually.', + }; + } +} diff --git a/src/Services/System/InstallationTypeDetector.php b/src/Services/System/InstallationTypeDetector.php new file mode 100644 index 00000000..9f9fbdb8 --- /dev/null +++ b/src/Services/System/InstallationTypeDetector.php @@ -0,0 +1,153 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\System; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Process\Process; + +readonly class InstallationTypeDetector +{ + public function __construct(#[Autowire(param: 'kernel.project_dir')] private string $project_dir, private GitVersionInfoProvider $gitVersionInfoProvider) + { + + } + + /** + * Detect the installation type based on filesystem markers. + */ + public function detect(): InstallationType + { + // Check for Docker environment first + if ($this->isDocker()) { + return InstallationType::DOCKER; + } + + // Check for Git installation + if ($this->isGitInstall()) { + return InstallationType::GIT; + } + + // Check for ZIP release (has VERSION file but no .git) + if ($this->isZipRelease()) { + return InstallationType::ZIP_RELEASE; + } + + return InstallationType::UNKNOWN; + } + + /** + * Check if running inside a Docker container. + */ + public function isDocker(): bool + { + // Check for /.dockerenv file + if (file_exists('/.dockerenv')) { + return true; + } + + // Check for DOCKER environment variable + if (getenv('DOCKER') !== false) { + return true; + } + + // Check for container runtime in cgroup + if (file_exists('/proc/1/cgroup')) { + $cgroup = @file_get_contents('/proc/1/cgroup'); + if ($cgroup !== false && (str_contains($cgroup, 'docker') || str_contains($cgroup, 'containerd'))) { + return true; + } + } + + return false; + } + + /** + * Check if this is a Git-based installation. + */ + public function isGitInstall(): bool + { + return $this->gitVersionInfoProvider->isGitRepo(); + } + + /** + * Check if this appears to be a ZIP release installation. + */ + public function isZipRelease(): bool + { + // Has VERSION file but no .git directory + return file_exists($this->project_dir . '/VERSION') && !$this->isGitInstall(); + } + + /** + * Get detailed information about the installation. + */ + public function getInstallationInfo(): array + { + $type = $this->detect(); + + $info = [ + 'type' => $type, + 'type_name' => $type->getLabel(), + 'supports_auto_update' => $type->supportsAutoUpdate(), + 'update_instructions' => $type->getUpdateInstructions(), + 'project_dir' => $this->project_dir, + ]; + + if ($type === InstallationType::GIT) { + $info['git'] = $this->getGitInfo(); + } + + if ($type === InstallationType::DOCKER) { + $info['docker'] = $this->getDockerInfo(); + } + + return $info; + } + + /** + * Get Git-specific information. + * @return array{branch: string|null, commit: string|null, remote_url: string|null, has_local_changes: bool} + */ + private function getGitInfo(): array + { + return [ + 'branch' => $this->gitVersionInfoProvider->getBranchName(), + 'commit' => $this->gitVersionInfoProvider->getCommitHash(8), + 'remote_url' => $this->gitVersionInfoProvider->getRemoteURL(), + 'has_local_changes' => $this->gitVersionInfoProvider->hasLocalChanges() ?? false, + ]; + } + + /** + * Get Docker-specific information. + * @return array{container_id: string|null, image: string|null} + */ + private function getDockerInfo(): array + { + return [ + 'container_id' => @file_get_contents('/proc/1/cpuset') ?: null, + 'image' => getenv('DOCKER_IMAGE') ?: null, + ]; + } +} diff --git a/src/Services/System/UpdateAvailableManager.php b/src/Services/System/UpdateAvailableFacade.php similarity index 61% rename from src/Services/System/UpdateAvailableManager.php rename to src/Services/System/UpdateAvailableFacade.php index 82cfb84e..60f66036 100644 --- a/src/Services/System/UpdateAvailableManager.php +++ b/src/Services/System/UpdateAvailableFacade.php @@ -24,28 +24,23 @@ declare(strict_types=1); namespace App\Services\System; use App\Settings\SystemSettings\PrivacySettings; -use Psr\Log\LoggerInterface; -use Shivas\VersioningBundle\Service\VersionManagerInterface; -use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; use Version\Version; /** * This class checks if a new version of Part-DB is available. */ -class UpdateAvailableManager +class UpdateAvailableFacade { - - private const API_URL = 'https://api.github.com/repos/Part-DB/Part-DB-server/releases/latest'; private const CACHE_KEY = 'uam_latest_version'; private const CACHE_TTL = 60 * 60 * 24 * 2; // 2 day - public function __construct(private readonly HttpClientInterface $httpClient, - private readonly CacheInterface $updateCache, private readonly VersionManagerInterface $versionManager, - private readonly PrivacySettings $privacySettings, private readonly LoggerInterface $logger, - #[Autowire(param: 'kernel.debug')] private readonly bool $is_dev_mode) + public function __construct( + private readonly CacheInterface $updateCache, + private readonly PrivacySettings $privacySettings, + private readonly UpdateChecker $updateChecker, + ) { } @@ -89,9 +84,7 @@ class UpdateAvailableManager } $latestVersion = $this->getLatestVersion(); - $currentVersion = $this->versionManager->getVersion(); - - return $latestVersion->isGreaterThan($currentVersion); + return $this->updateChecker->isNewerVersionThanCurrent($latestVersion); } /** @@ -111,34 +104,7 @@ class UpdateAvailableManager return $this->updateCache->get(self::CACHE_KEY, function (ItemInterface $item) { $item->expiresAfter(self::CACHE_TTL); - try { - $response = $this->httpClient->request('GET', self::API_URL); - $result = $response->toArray(); - $tag_name = $result['tag_name']; - - // Remove the leading 'v' from the tag name - $version = substr($tag_name, 1); - - return [ - 'version' => $version, - 'url' => $result['html_url'], - ]; - } catch (\Exception $e) { - //When we are in dev mode, throw the exception, otherwise just silently log it - if ($this->is_dev_mode) { - throw $e; - } - - //In the case of an error, try it again after half of the cache time - $item->expiresAfter(self::CACHE_TTL / 2); - - $this->logger->error('Checking for updates failed: ' . $e->getMessage()); - - return [ - 'version' => '0.0.1', - 'url' => 'update-checking-error' - ]; - } - }); + return $this->updateChecker->getLatestVersion(); + }) ?? ['version' => '0.0.1', 'url' => 'update-checking-failed']; } -} \ No newline at end of file +} diff --git a/src/Services/System/UpdateChecker.php b/src/Services/System/UpdateChecker.php new file mode 100644 index 00000000..366e8d67 --- /dev/null +++ b/src/Services/System/UpdateChecker.php @@ -0,0 +1,349 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\System; + +use App\Settings\SystemSettings\PrivacySettings; +use Psr\Log\LoggerInterface; +use Shivas\VersioningBundle\Service\VersionManagerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Process\Process; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Version\Version; + +/** + * Enhanced update checker that fetches release information including changelogs. + */ +class UpdateChecker +{ + private const GITHUB_API_BASE = 'https://api.github.com/repos/Part-DB/Part-DB-server'; + private const CACHE_KEY_RELEASES = 'update_checker_releases'; + private const CACHE_KEY_COMMITS = 'update_checker_commits_behind'; + private const CACHE_TTL = 60 * 60 * 6; // 6 hours + private const CACHE_TTL_ERROR = 60 * 60; // 1 hour on error + + public function __construct(private readonly HttpClientInterface $httpClient, + private readonly CacheInterface $updateCache, private readonly VersionManagerInterface $versionManager, + private readonly PrivacySettings $privacySettings, private readonly LoggerInterface $logger, + private readonly InstallationTypeDetector $installationTypeDetector, + private readonly GitVersionInfoProvider $gitVersionInfoProvider, + #[Autowire(param: 'kernel.debug')] private readonly bool $is_dev_mode, + #[Autowire(param: 'kernel.project_dir')] private readonly string $project_dir, + private readonly ?WatchtowerClient $watchtowerClient = null) + { + + } + + /** + * Get the current installed version. + */ + public function getCurrentVersion(): Version + { + return $this->versionManager->getVersion(); + } + + /** + * Get the current version as string. + */ + public function getCurrentVersionString(): string + { + return $this->getCurrentVersion()->toString(); + } + + /** + * Get Git repository information. + * @return array{branch: ?string, commit: ?string, has_local_changes: bool, commits_behind: int, is_git_install: bool} + */ + private function getGitInfo(): array + { + $info = [ + 'branch' => null, + 'commit' => null, + 'has_local_changes' => false, + 'commits_behind' => 0, + 'is_git_install' => false, + ]; + + if (!$this->gitVersionInfoProvider->isGitRepo()) { + return $info; + } + + $info['is_git_install'] = true; + + $info['branch'] = $this->gitVersionInfoProvider->getBranchName(); + $info['commit'] = $this->gitVersionInfoProvider->getCommitHash(8); + $info['has_local_changes'] = $this->gitVersionInfoProvider->hasLocalChanges(); + + // Get commits behind (fetch first) + if ($info['branch']) { + // Try to get cached commits behind count + $info['commits_behind'] = $this->getCommitsBehind($info['branch']); + } + + return $info; + } + + /** + * Get number of commits behind the remote branch (cached). + */ + private function getCommitsBehind(string $branch): int + { + if (!$this->privacySettings->checkForUpdates) { + return 0; + } + + $cacheKey = self::CACHE_KEY_COMMITS . '_' . hash('xxh3', $branch); + + return $this->updateCache->get($cacheKey, function (ItemInterface $item) use ($branch) { + $item->expiresAfter(self::CACHE_TTL); + + // Fetch from remote first + $process = new Process(['git', 'fetch', '--tags', 'origin'], $this->project_dir); + $process->run(); + + // Count commits behind + $process = new Process(['git', 'rev-list', 'HEAD..origin/' . $branch, '--count'], $this->project_dir); + $process->run(); + + return $process->isSuccessful() ? (int) trim($process->getOutput()) : 0; + }); + } + + /** + * Force refresh git information by invalidating cache. + */ + public function refreshVersionInfo(): void + { + $gitBranch = $this->gitVersionInfoProvider->getBranchName(); + if ($gitBranch) { + $this->updateCache->delete(self::CACHE_KEY_COMMITS . '_' . hash('xxh3', $gitBranch)); + } + $this->updateCache->delete(self::CACHE_KEY_RELEASES); + } + + /** + * Get all available releases from GitHub (cached). + * + * @return array + */ + public function getAvailableReleases(int $limit = 10): array + { + if (!$this->privacySettings->checkForUpdates) { + return []; + } + + return $this->updateCache->get(self::CACHE_KEY_RELEASES, function (ItemInterface $item) use ($limit) { + $item->expiresAfter(self::CACHE_TTL); + + try { + $response = $this->httpClient->request('GET', self::GITHUB_API_BASE . '/releases', [ + 'query' => ['per_page' => $limit], + 'headers' => [ + 'Accept' => 'application/vnd.github.v3+json', + 'User-Agent' => 'Part-DB-Update-Checker', + ], + ]); + + $releases = []; + foreach ($response->toArray() as $release) { + // Extract assets (for ZIP download) + $assets = []; + foreach ($release['assets'] ?? [] as $asset) { + if (str_ends_with($asset['name'], '.zip') || str_ends_with($asset['name'], '.tar.gz')) { + $assets[] = [ + 'name' => $asset['name'], + 'url' => $asset['browser_download_url'], + 'size' => $asset['size'], + ]; + } + } + + $releases[] = [ + 'version' => ltrim($release['tag_name'], 'v'), + 'tag' => $release['tag_name'], + 'name' => $release['name'] ?? $release['tag_name'], + 'url' => $release['html_url'], + 'published_at' => $release['published_at'], + 'body' => $release['body'] ?? '', + 'prerelease' => $release['prerelease'] ?? false, + 'draft' => $release['draft'] ?? false, + 'assets' => $assets, + 'tarball_url' => $release['tarball_url'] ?? null, + 'zipball_url' => $release['zipball_url'] ?? null, + ]; + } + + return $releases; + } catch (\Exception $e) { + $this->logger->error('Failed to fetch releases from GitHub: ' . $e->getMessage()); + $item->expiresAfter(self::CACHE_TTL_ERROR); + + if ($this->is_dev_mode) { + throw $e; + } + + return []; + } + }); + } + + /** + * Get the latest stable release. + * @return array{version: string, tag: string, name: string, url: string, published_at: string, body: string, prerelease: bool, assets: array}|null + */ + public function getLatestVersion(bool $includePrerelease = false): ?array + { + $releases = $this->getAvailableReleases(); + + foreach ($releases as $release) { + // Skip drafts always + if ($release['draft']) { + continue; + } + + // Skip prereleases unless explicitly included + if (!$includePrerelease && $release['prerelease']) { + continue; + } + + return $release; + } + + return null; + } + + /** + * Check if a specific version is newer than current. + */ + public function isNewerVersionThanCurrent(Version|string $version): bool + { + if ($version instanceof Version) { + return $version->isGreaterThan($this->getCurrentVersion()); + } + try { + return Version::fromString(ltrim($version, 'v'))->isGreaterThan($this->getCurrentVersion()); + } catch (\Exception) { + return false; + } + } + + /** + * Get comprehensive update status. + * @return array{current_version: string, latest_version: ?string, latest_tag: ?string, update_available: bool, release_notes: ?string, release_url: ?string, + * published_at: ?string, git: array, installation: array, can_auto_update: bool, update_blockers: array, check_enabled: bool} + */ + public function getUpdateStatus(): array + { + $current = $this->getCurrentVersion(); + $latest = $this->getLatestVersion(); + $gitInfo = $this->getGitInfo(); + $installInfo = $this->installationTypeDetector->getInstallationInfo(); + + $updateAvailable = false; + $latestVersion = null; + $latestTag = null; + + if ($latest) { + try { + $latestVersionObj = Version::fromString($latest['version']); + $updateAvailable = $latestVersionObj->isGreaterThan($current); + $latestVersion = $latest['version']; + $latestTag = $latest['tag']; + } catch (\Exception) { + // Invalid version string + } + } + + // Determine if we can auto-update + $canAutoUpdate = $installInfo['supports_auto_update']; + $updateBlockers = []; + + if ($gitInfo['has_local_changes']) { + $canAutoUpdate = false; + $updateBlockers[] = 'local_changes'; + } + + // Docker installations require Watchtower for auto-update + $watchtowerConfigured = $this->watchtowerClient !== null && $this->watchtowerClient->isConfigured(); + $watchtowerAvailable = $watchtowerConfigured && $this->watchtowerClient->isAvailable(); + + if ($installInfo['type'] === InstallationType::DOCKER && !$watchtowerConfigured) { + $canAutoUpdate = false; + $updateBlockers[] = 'docker_no_watchtower'; + } elseif ($installInfo['type'] === InstallationType::DOCKER && !$watchtowerAvailable) { + $canAutoUpdate = false; + $updateBlockers[] = 'docker_watchtower_unreachable'; + } + + return [ + 'current_version' => $current->toString(), + 'latest_version' => $latestVersion, + 'latest_tag' => $latestTag, + 'update_available' => $updateAvailable, + 'release_notes' => $latest['body'] ?? null, + 'release_url' => $latest['url'] ?? null, + 'published_at' => $latest['published_at'] ?? null, + 'git' => $gitInfo, + 'installation' => $installInfo, + 'can_auto_update' => $canAutoUpdate, + 'update_blockers' => $updateBlockers, + 'check_enabled' => $this->privacySettings->checkForUpdates, + 'watchtower_configured' => $watchtowerConfigured, + 'watchtower_available' => $watchtowerAvailable, + ]; + } + + /** + * Get releases newer than the current version. + * @return array + */ + public function getAvailableUpdates(bool $includePrerelease = false): array + { + $releases = $this->getAvailableReleases(); + $current = $this->getCurrentVersion(); + $updates = []; + + foreach ($releases as $release) { + if ($release['draft']) { + continue; + } + + if (!$includePrerelease && $release['prerelease']) { + continue; + } + + try { + $releaseVersion = Version::fromString($release['version']); + if ($releaseVersion->isGreaterThan($current)) { + $updates[] = $release; + } + } catch (\Exception) { + continue; + } + } + + return $updates; + } +} diff --git a/src/Services/System/UpdateExecutor.php b/src/Services/System/UpdateExecutor.php new file mode 100644 index 00000000..ccc346d5 --- /dev/null +++ b/src/Services/System/UpdateExecutor.php @@ -0,0 +1,1073 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\System; + +use Psr\Log\LoggerInterface; +use Shivas\VersioningBundle\Service\VersionManagerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; + +/** + * Handles the execution of Part-DB updates with safety mechanisms. + * + * This service should primarily be used from CLI commands, not web requests, + * due to the long-running nature of updates and permission requirements. + * + * For web requests, use startBackgroundUpdate() method. + */ +class UpdateExecutor +{ + private const LOCK_FILE = 'var/update.lock'; + private const MAINTENANCE_FILE = 'var/maintenance.flag'; + private const UPDATE_LOG_DIR = 'var/log/updates'; + private const PROGRESS_FILE = 'var/update_progress.json'; + + /** @var array */ + private array $steps = []; + + private ?string $currentLogFile = null; + + public function __construct( + #[Autowire(param: 'kernel.project_dir')] + private readonly string $project_dir, + private readonly LoggerInterface $logger, + private readonly Filesystem $filesystem, + private readonly InstallationTypeDetector $installationTypeDetector, + private readonly UpdateChecker $updateChecker, + private readonly BackupManager $backupManager, + private readonly CommandRunHelper $commandRunHelper, + #[Autowire(param: 'app.debug_mode')] + private readonly bool $debugMode = false, + ) { + } + + /** + * Get the current version string for use in filenames. + */ + private function getCurrentVersionString(): string + { + return $this->updateChecker->getCurrentVersionString(); + } + + /** + * Check if an update is currently in progress. + */ + public function isLocked(): bool + { + // Check if lock is stale (older than 1 hour) + $lockData = $this->getLockInfo(); + if ($lockData === null) { + return false; + } + + if ($lockData && isset($lockData['started_at'])) { + $startedAt = new \DateTime($lockData['started_at']); + $now = new \DateTime(); + $diff = $now->getTimestamp() - $startedAt->getTimestamp(); + + // If lock is older than 1 hour, consider it stale + if ($diff > 3600) { + $this->logger->warning('Found stale update lock, removing it'); + $this->releaseLock(); + return false; + } + } + + return true; + } + + /** + * Get lock information, or null if not locked. + * @return null|array{started_at: string, pid: int, user: string} + */ + public function getLockInfo(): ?array + { + $lockFile = $this->project_dir . '/' . self::LOCK_FILE; + + if (!file_exists($lockFile)) { + return null; + } + + return json_decode(file_get_contents($lockFile), true, 512, JSON_THROW_ON_ERROR); + } + + /** + * Check if maintenance mode is enabled. + */ + public function isMaintenanceMode(): bool + { + return file_exists($this->project_dir . '/' . self::MAINTENANCE_FILE); + } + + /** + * Get maintenance mode information. + * @return null|array{enabled_at: string, reason: string} + */ + public function getMaintenanceInfo(): ?array + { + $maintenanceFile = $this->project_dir . '/' . self::MAINTENANCE_FILE; + + if (!file_exists($maintenanceFile)) { + return null; + } + + return json_decode(file_get_contents($maintenanceFile), true, 512, JSON_THROW_ON_ERROR); + } + + /** + * Acquire an exclusive lock for the update process. + */ + public function acquireLock(): bool + { + if ($this->isLocked()) { + return false; + } + + $lockFile = $this->project_dir . '/' . self::LOCK_FILE; + $lockDir = dirname($lockFile); + + if (!is_dir($lockDir)) { + $this->filesystem->mkdir($lockDir); + } + + $lockData = [ + 'started_at' => (new \DateTime())->format('c'), + 'pid' => getmypid(), + 'user' => get_current_user(), + ]; + + $this->filesystem->dumpFile($lockFile, json_encode($lockData, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); + + return true; + } + + /** + * Release the update lock. + */ + public function releaseLock(): void + { + $lockFile = $this->project_dir . '/' . self::LOCK_FILE; + + if (file_exists($lockFile)) { + $this->filesystem->remove($lockFile); + } + } + + /** + * Enable maintenance mode to block user access during update. + */ + public function enableMaintenanceMode(string $reason = 'Update in progress'): void + { + $maintenanceFile = $this->project_dir . '/' . self::MAINTENANCE_FILE; + $maintenanceDir = dirname($maintenanceFile); + + if (!is_dir($maintenanceDir)) { + $this->filesystem->mkdir($maintenanceDir); + } + + $data = [ + 'enabled_at' => (new \DateTime())->format('c'), + 'reason' => $reason, + ]; + + $this->filesystem->dumpFile($maintenanceFile, json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); + } + + /** + * Disable maintenance mode. + */ + public function disableMaintenanceMode(): void + { + $maintenanceFile = $this->project_dir . '/' . self::MAINTENANCE_FILE; + + if (file_exists($maintenanceFile)) { + $this->filesystem->remove($maintenanceFile); + } + } + + /** + * Reset PHP OPcache for the web server process. + * + * OPcache in PHP-FPM is separate from CLI. After updating code files, + * PHP-FPM may still serve stale cached bytecode, causing constructor + * mismatches and 500 errors. This method creates a temporary PHP script + * in the public directory, invokes it via HTTP to reset OPcache in the + * web server context, then removes the script. + * + * @return bool Whether OPcache was successfully reset + */ + private function resetOpcache(): bool + { + $token = bin2hex(random_bytes(16)); + $resetScript = $this->project_dir . '/public/_opcache_reset_' . $token . '.php'; + + try { + // Create a temporary PHP script that resets OPcache + $scriptContent = 'filesystem->dumpFile($resetScript, $scriptContent); + + // Try to invoke it via HTTP on localhost + $urls = [ + 'http://127.0.0.1/_opcache_reset_' . $token . '.php', + 'http://localhost/_opcache_reset_' . $token . '.php', + ]; + + $success = false; + foreach ($urls as $url) { + try { + $context = stream_context_create([ + 'http' => [ + 'timeout' => 5, + 'ignore_errors' => true, + ], + ]); + + $response = @file_get_contents($url, false, $context); + if ($response === 'OK') { + $this->logger->info('OPcache reset via ' . $url); + $success = true; + break; + } + } catch (\Throwable $e) { + // Try next URL + continue; + } + } + + if (!$success) { + $this->logger->info('OPcache reset via HTTP not available, trying CLI fallback'); + // CLI opcache_reset() only affects CLI, but try anyway + if (function_exists('opcache_reset')) { + opcache_reset(); + } + } + + return $success; + } catch (\Throwable $e) { + $this->logger->warning('OPcache reset failed: ' . $e->getMessage()); + return false; + } finally { + // Ensure the temp script is removed + if (file_exists($resetScript)) { + @unlink($resetScript); + } + } + } + + /** + * Validate that we can perform an update. + * + * @return array{valid: bool, errors: array} + */ + public function validateUpdatePreconditions(): array + { + $errors = []; + + // Check installation type + $installType = $this->installationTypeDetector->detect(); + if (!$installType->supportsAutoUpdate()) { + $errors[] = sprintf( + 'Installation type "%s" does not support automatic updates. %s', + $installType->getLabel(), + $installType->getUpdateInstructions() + ); + } + + // Docker installations are updated via Watchtower - skip Git/Composer/Yarn checks + if ($installType === InstallationType::DOCKER) { + // Only check if already locked + if ($this->isLocked()) { + $lockInfo = $this->getLockInfo(); + $errors[] = sprintf( + 'An update is already in progress (started at %s).', + $lockInfo['started_at'] ?? 'unknown time' + ); + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors, + ]; + } + + // Check for Git installation + if ($installType === InstallationType::GIT) { + // Check if git is available + $process = new Process(['git', '--version']); + $process->run(); + if (!$process->isSuccessful()) { + $errors[] = 'Git command not found. Please ensure Git is installed and in PATH.'; + } + + // Check for local changes + $process = new Process(['git', 'status', '--porcelain'], $this->project_dir); + $process->run(); + if (!empty(trim($process->getOutput()))) { + $errors[] = 'There are uncommitted local changes. Please commit or stash them before updating.'; + } + } + + // Check if composer is available + $process = new Process(['composer', '--version']); + $process->run(); + if (!$process->isSuccessful()) { + $errors[] = 'Composer command not found. Please ensure Composer is installed and in PATH.'; + } + + // Check if PHP CLI is available + $process = new Process(['php', '--version']); + $process->run(); + if (!$process->isSuccessful()) { + $errors[] = 'PHP CLI not found. Please ensure PHP is installed and in PATH.'; + } + + // Check if yarn is available (for frontend assets) + $process = new Process(['yarn', '--version']); + $process->run(); + if (!$process->isSuccessful()) { + $errors[] = 'Yarn command not found. Please ensure Yarn is installed and in PATH for frontend asset compilation.'; + } + + // Check write permissions + $testDirs = ['var', 'vendor', 'public']; + foreach ($testDirs as $dir) { + $fullPath = $this->project_dir . '/' . $dir; + if (is_dir($fullPath) && !is_writable($fullPath)) { + $errors[] = sprintf('Directory "%s" is not writable.', $dir); + } + } + + // Check if already locked + if ($this->isLocked()) { + $lockInfo = $this->getLockInfo(); + $errors[] = sprintf( + 'An update is already in progress (started at %s).', + $lockInfo['started_at'] ?? 'unknown time' + ); + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors, + ]; + } + + /** + * Execute the update to a specific version. + * + * @param string $targetVersion The target version/tag to update to (e.g., "v2.6.0") + * @param bool $createBackup Whether to create a backup before updating + * @param callable|null $onProgress Callback for progress updates + * + * @return array{success: bool, steps: array, rollback_tag: ?string, error: ?string, log_file: ?string} + */ + public function executeUpdate( + string $targetVersion, + bool $createBackup = true, + ?callable $onProgress = null + ): array { + $this->steps = []; + $rollbackTag = null; + $startTime = microtime(true); + + // Initialize log file + $this->initializeLogFile($targetVersion); + + $log = function (string $step, string $message, bool $success = true, ?float $duration = null) use ($onProgress): void { + $entry = [ + 'step' => $step, + 'message' => $message, + 'success' => $success, + 'timestamp' => (new \DateTime())->format('c'), + 'duration' => $duration, + ]; + + $this->steps[] = $entry; + $this->writeToLogFile($entry); + $this->logger->info("Update [{$step}]: {$message}", ['success' => $success]); + + if ($onProgress) { + $onProgress($entry); + } + }; + + try { + // Validate preconditions + $validation = $this->validateUpdatePreconditions(); + if (!$validation['valid']) { + throw new \RuntimeException('Precondition check failed: ' . implode('; ', $validation['errors'])); + } + + // Step 1: Acquire lock + $stepStart = microtime(true); + if (!$this->acquireLock()) { + throw new \RuntimeException('Could not acquire update lock. Another update may be in progress.'); + } + $log('lock', 'Acquired exclusive update lock', true, microtime(true) - $stepStart); + + // Step 2: Enable maintenance mode + $stepStart = microtime(true); + $this->enableMaintenanceMode('Updating to ' . $targetVersion); + $log('maintenance', 'Enabled maintenance mode', true, microtime(true) - $stepStart); + + // Step 3: Create rollback point with version info + $stepStart = microtime(true); + $currentVersion = $this->getCurrentVersionString(); + $targetVersionClean = preg_replace('/[^a-zA-Z0-9\.]/', '', $targetVersion); + $rollbackTag = 'pre-update-v' . $currentVersion . '-to-' . $targetVersionClean . '-' . date('Y-m-d-His'); + $this->runCommand(['git', 'tag', $rollbackTag], 'Create rollback tag'); + $log('rollback_tag', 'Created rollback tag: ' . $rollbackTag, true, microtime(true) - $stepStart); + + // Step 4: Create backup (optional) + if ($createBackup) { + $stepStart = microtime(true); + $backupFile = $this->backupManager->createBackup($targetVersion); + $log('backup', 'Created backup: ' . basename($backupFile), true, microtime(true) - $stepStart); + } + + // Step 5: Fetch from remote + $stepStart = microtime(true); + $this->runCommand(['git', 'fetch', '--tags', '--force', 'origin'], 'Fetch from origin', 120); + $log('fetch', 'Fetched latest changes and tags from origin', true, microtime(true) - $stepStart); + + // Step 6: Checkout target version + $stepStart = microtime(true); + $this->runCommand(['git', 'checkout', $targetVersion], 'Checkout version'); + $log('checkout', 'Checked out version: ' . $targetVersion, true, microtime(true) - $stepStart); + + // Step 7: Install PHP dependencies + $stepStart = microtime(true); + if ($this->debugMode) { + $this->runCommand([ // Install with dev dependencies in debug mode + 'composer', + 'install', + '--no-interaction', + '--no-progress', + ], 'Install PHP dependencies', 600); + } else { + $this->runCommand([ + 'composer', + 'install', + '--no-dev', + '--optimize-autoloader', + '--no-interaction', + '--no-progress', + ], 'Install PHP dependencies', 600); + } + $log('composer', 'Installed/updated PHP dependencies', true, microtime(true) - $stepStart); + + // Step 8: Install frontend dependencies + $stepStart = microtime(true); + $this->runCommand([ + 'yarn', 'install', + '--frozen-lockfile', + '--non-interactive', + ], 'Install frontend dependencies', 600); + $log('yarn_install', 'Installed frontend dependencies', true, microtime(true) - $stepStart); + + // Step 9: Build frontend assets + $stepStart = microtime(true); + $this->runCommand([ + 'yarn', 'build', + ], 'Build frontend assets', 600); + $log('yarn_build', 'Built frontend assets', true, microtime(true) - $stepStart); + + // Step 10: Run database migrations + $stepStart = microtime(true); + $this->runCommand([ + 'php', 'bin/console', 'doctrine:migrations:migrate', + '--no-interaction', + '--allow-no-migration', + ], 'Run migrations', 300); + $log('migrations', 'Database migrations completed', true, microtime(true) - $stepStart); + + // Step 11: Clear cache + $stepStart = microtime(true); + $this->runCommand([ + 'php', 'bin/console', 'cache:pool:clear', '--all', + '--env=prod', + '--no-interaction', + ], 'Clear cache', 120); + $log('cache_clear', 'Cleared application cache', true, microtime(true) - $stepStart); + + // Step 12: Warm up cache + $stepStart = microtime(true); + $this->runCommand([ + 'php', 'bin/console', 'cache:warmup', + '--env=prod', + ], 'Warmup cache', 120); + $log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart); + + // Step 13: Reset OPcache (if available) + $stepStart = microtime(true); + $opcacheResult = $this->resetOpcache(); + $log('opcache_reset', $opcacheResult + ? 'Reset PHP OPcache for web server' + : 'OPcache reset skipped (not available or not needed)', + true, microtime(true) - $stepStart); + + // Step 14: Disable maintenance mode + $stepStart = microtime(true); + $this->disableMaintenanceMode(); + $log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart); + + // Step 15: Release lock + $stepStart = microtime(true); + $this->releaseLock(); + + $totalDuration = microtime(true) - $startTime; + $log('complete', sprintf('Update completed successfully in %.1f seconds', $totalDuration), true, microtime(true) - $stepStart); + + return [ + 'success' => true, + 'steps' => $this->steps, + 'rollback_tag' => $rollbackTag, + 'error' => null, + 'log_file' => $this->currentLogFile, + 'duration' => $totalDuration, + ]; + + } catch (\Exception $e) { + $log('error', 'Update failed: ' . $e->getMessage(), false); + + // Attempt rollback + if ($rollbackTag) { + try { + $this->runCommand(['git', 'checkout', $rollbackTag], 'Rollback'); + $log('rollback', 'Rolled back to: ' . $rollbackTag, true); + + // Re-run composer install after rollback + $this->runCommand([ + 'composer', 'install', + '--no-dev', + '--optimize-autoloader', + '--no-interaction', + ], 'Reinstall dependencies after rollback', 600); + $log('rollback_composer', 'Reinstalled PHP dependencies after rollback', true); + + // Re-run yarn install after rollback + $this->runCommand([ + 'yarn', 'install', + '--frozen-lockfile', + '--non-interactive', + ], 'Reinstall frontend dependencies after rollback', 600); + $log('rollback_yarn_install', 'Reinstalled frontend dependencies after rollback', true); + + // Re-run yarn build after rollback + $this->runCommand([ + 'yarn', 'build', + ], 'Rebuild frontend assets after rollback', 600); + $log('rollback_yarn_build', 'Rebuilt frontend assets after rollback', true); + + // Clear cache after rollback + $this->runCommand([ + 'php', 'bin/console', 'cache:pool:clear', '--all', + '--env=prod', + ], 'Clear cache after rollback', 120); + $log('rollback_cache', 'Cleared cache after rollback', true); + + // Reset OPcache after rollback + $this->resetOpcache(); + + } catch (\Exception $rollbackError) { + $log('rollback_failed', 'Rollback failed: ' . $rollbackError->getMessage(), false); + } + } + + // Clean up + $this->disableMaintenanceMode(); + $this->releaseLock(); + + return [ + 'success' => false, + 'steps' => $this->steps, + 'rollback_tag' => $rollbackTag, + 'error' => $e->getMessage(), + 'log_file' => $this->currentLogFile, + 'duration' => microtime(true) - $startTime, + ]; + } + } + + /** + * Run a shell command with proper error handling. + */ + private function runCommand(array $command, string $description, int $timeout = 120): string + { + return $this->commandRunHelper->runCommand($command, $description, $timeout); + } + + /** + * Initialize the log file for this update. + */ + private function initializeLogFile(string $targetVersion): void + { + $logDir = $this->project_dir . '/' . self::UPDATE_LOG_DIR; + + if (!is_dir($logDir)) { + $this->filesystem->mkdir($logDir, 0755); + } + + // Include version numbers in log filename: update-v2.5.1-to-v2.6.0-2024-01-30-185400.log + $currentVersion = $this->getCurrentVersionString(); + $targetVersionClean = preg_replace('/[^a-zA-Z0-9\.]/', '', $targetVersion); + $this->currentLogFile = $logDir . '/update-v' . $currentVersion . '-to-' . $targetVersionClean . '-' . date('Y-m-d-His') . '.log'; + + $header = sprintf( + "Part-DB Update Log\n" . + "==================\n" . + "Started: %s\n" . + "From Version: %s\n" . + "Target Version: %s\n" . + "==================\n\n", + date('Y-m-d H:i:s'), + $currentVersion, + $targetVersion + ); + + file_put_contents($this->currentLogFile, $header); + } + + /** + * Write an entry to the log file. + */ + private function writeToLogFile(array $entry): void + { + if (!$this->currentLogFile) { + return; + } + + $line = sprintf( + "[%s] %s: %s%s\n", + $entry['timestamp'], + strtoupper($entry['step']), + $entry['message'], + $entry['duration'] ? sprintf(' (%.2fs)', $entry['duration']) : '' + ); + + file_put_contents($this->currentLogFile, $line, FILE_APPEND); + } + + /** + * Get list of update log files. + * @return array{file: string, path: string, date: int, size: int}[] + */ + public function getUpdateLogs(): array + { + $logDir = $this->project_dir . '/' . self::UPDATE_LOG_DIR; + + if (!is_dir($logDir)) { + return []; + } + + $logs = []; + foreach (glob($logDir . '/update-*.log') as $logFile) { + $logs[] = [ + 'file' => basename($logFile), + 'path' => $logFile, + 'date' => filemtime($logFile), + 'size' => filesize($logFile), + ]; + } + + // Sort by date descending + usort($logs, static fn($a, $b) => $b['date'] <=> $a['date']); + + return $logs; + } + + + /** + * Delete a specific update log file. + */ + public function deleteLog(string $filename): bool + { + // Validate filename pattern for security + if (!preg_match('/^update-[\w.\-]+\.log$/', $filename)) { + $this->logger->warning('Attempted to delete invalid log filename: ' . $filename); + return false; + } + + $logPath = $this->project_dir . '/' . self::UPDATE_LOG_DIR . '/' . basename($filename); + + if (!file_exists($logPath)) { + return false; + } + + try { + $this->filesystem->remove($logPath); + $this->logger->info('Deleted update log: ' . $filename); + return true; + } catch (\Exception $e) { + $this->logger->error('Failed to delete update log: ' . $e->getMessage()); + return false; + } + } + + /** + * Restore from a backup file with maintenance mode and cache clearing. + * + * This wraps BackupManager::restoreBackup with additional safety measures + * like lock acquisition, maintenance mode, and cache operations. + * + * @param string $filename The backup filename to restore + * @param bool $restoreDatabase Whether to restore the database + * @param bool $restoreConfig Whether to restore config files + * @param bool $restoreAttachments Whether to restore attachments + * @param callable|null $onProgress Callback for progress updates + * @return array{success: bool, steps: array, error: ?string} + */ + public function restoreBackup( + string $filename, + bool $restoreDatabase = true, + bool $restoreConfig = false, + bool $restoreAttachments = false, + ?callable $onProgress = null + ): array { + $this->steps = []; + $startTime = microtime(true); + + $log = function (string $step, string $message, bool $success, ?float $duration = null) use ($onProgress): void { + $entry = [ + 'step' => $step, + 'message' => $message, + 'success' => $success, + 'timestamp' => (new \DateTime())->format('c'), + 'duration' => $duration, + ]; + $this->steps[] = $entry; + $this->logger->info('[Restore] ' . $step . ': ' . $message, ['success' => $success]); + + if ($onProgress) { + $onProgress($entry); + } + }; + + try { + $stepStart = microtime(true); + + // Step 1: Acquire lock + if (!$this->acquireLock()) { + throw new \RuntimeException('Could not acquire lock. Another operation may be in progress.'); + } + $log('lock', 'Acquired exclusive restore lock', true, microtime(true) - $stepStart); + + // Step 2: Enable maintenance mode + $stepStart = microtime(true); + $this->enableMaintenanceMode('Restoring from backup...'); + $log('maintenance', 'Enabled maintenance mode', true, microtime(true) - $stepStart); + + // Step 3: Delegate to BackupManager for core restoration + $stepStart = microtime(true); + $result = $this->backupManager->restoreBackup( + $filename, + $restoreDatabase, + $restoreConfig, + $restoreAttachments, + function ($entry) use ($log) { + // Forward progress from BackupManager + $log($entry['step'], $entry['message'], $entry['success'], $entry['duration'] ?? null); + } + ); + + if (!$result['success']) { + throw new \RuntimeException($result['error'] ?? 'Restore failed'); + } + + // Step 4: Clear cache + $stepStart = microtime(true); + $this->runCommand(['php', 'bin/console', 'cache:clear', '--no-warmup'], 'Clear cache'); + $log('cache_clear', 'Cleared application cache', true, microtime(true) - $stepStart); + + // Step 5: Warm up cache + $stepStart = microtime(true); + $this->runCommand(['php', 'bin/console', 'cache:warmup'], 'Warm up cache'); + $log('cache_warmup', 'Warmed up application cache', true, microtime(true) - $stepStart); + + // Step 6: Reset OPcache + $stepStart = microtime(true); + $this->resetOpcache(); + $log('opcache_reset', 'Reset PHP OPcache', true, microtime(true) - $stepStart); + + // Step 7: Disable maintenance mode + $stepStart = microtime(true); + $this->disableMaintenanceMode(); + $log('maintenance_off', 'Disabled maintenance mode', true, microtime(true) - $stepStart); + + // Step 8: Release lock + $this->releaseLock(); + + $totalDuration = microtime(true) - $startTime; + $log('complete', sprintf('Restore completed successfully in %.1f seconds', $totalDuration), true); + + return [ + 'success' => true, + 'steps' => $this->steps, + 'error' => null, + ]; + + } catch (\Throwable $e) { + $this->logger->error('Restore failed: ' . $e->getMessage(), [ + 'exception' => $e, + 'file' => $filename, + ]); + + // Try to clean up + try { + $this->disableMaintenanceMode(); + $this->releaseLock(); + } catch (\Throwable $cleanupError) { + $this->logger->error('Cleanup after failed restore also failed', ['error' => $cleanupError->getMessage()]); + } + + return [ + 'success' => false, + 'steps' => $this->steps, + 'error' => $e->getMessage(), + ]; + } + } + + /** + * Get the path to the progress file. + */ + public function getProgressFilePath(): string + { + return $this->project_dir . '/' . self::PROGRESS_FILE; + } + + /** + * Save progress to file for web UI polling. + * @param array{status: string, target_version: string, create_backup: bool, started_at: string, current_step: int, total_steps: int, step_name: string, step_message: string, steps: array, error: ?string} $progress + */ + private function saveProgress(array $progress): void + { + $progressFile = $this->getProgressFilePath(); + $progressDir = dirname($progressFile); + + if (!is_dir($progressDir)) { + $this->filesystem->mkdir($progressDir); + } + + $this->filesystem->dumpFile($progressFile, json_encode($progress, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); + } + + /** + * Get current update progress from file. + * @return null|array{status: string, target_version: string, create_backup: bool, started_at: string, current_step: int, total_steps: int, step_name: string, step_message: string, steps: array, error: ?string} + */ + public function getProgress(): ?array + { + $progressFile = $this->getProgressFilePath(); + + if (!file_exists($progressFile)) { + return null; + } + + $data = json_decode(file_get_contents($progressFile), true, 512, JSON_THROW_ON_ERROR); + + // If the progress file is stale (older than 30 minutes), consider it invalid + if ($data && isset($data['started_at'])) { + $startedAt = strtotime($data['started_at']); + if (time() - $startedAt > 1800) { + $this->clearProgress(); + return null; + } + } + + return $data; + } + + /** + * Clear progress file. + */ + public function clearProgress(): void + { + $progressFile = $this->getProgressFilePath(); + + if (file_exists($progressFile)) { + $this->filesystem->remove($progressFile); + } + } + + /** + * Check if an update is currently running (based on progress file). + */ + public function isUpdateRunning(): bool + { + $progress = $this->getProgress(); + + if (!$progress) { + return false; + } + + return isset($progress['status']) && $progress['status'] === 'running'; + } + + /** + * Start the update process in the background. + * Returns the process ID or null on failure. + */ + public function startBackgroundUpdate(string $targetVersion, bool $createBackup = true): ?int + { + // Validate first + $validation = $this->validateUpdatePreconditions(); + if (!$validation['valid']) { + $this->logger->error('Update validation failed', ['errors' => $validation['errors']]); + return null; + } + + // Initialize progress file + $this->saveProgress([ + 'status' => 'starting', + 'target_version' => $targetVersion, + 'create_backup' => $createBackup, + 'started_at' => (new \DateTime())->format('c'), + 'current_step' => 0, + 'total_steps' => 15, + 'step_name' => 'initializing', + 'step_message' => 'Starting update process...', + 'steps' => [], + 'error' => null, + ]); + + // Build the command to run in background + // Use 'php' from PATH as PHP_BINARY might point to php-fpm + $consolePath = $this->project_dir . '/bin/console'; + $logFile = $this->project_dir . '/var/log/update-background.log'; + + // Ensure log directory exists + $logDir = dirname($logFile); + if (!is_dir($logDir)) { + $this->filesystem->mkdir($logDir, 0755); + } + + //If we are on Windows, we cannot use nohup + if (PHP_OS_FAMILY === 'Windows') { + $command = sprintf( + 'start /B php %s partdb:update %s %s --force --no-interaction >> %s 2>&1', + escapeshellarg($consolePath), + escapeshellarg($targetVersion), + $createBackup ? '' : '--no-backup', + escapeshellarg($logFile) + ); + } else { //Unix like platforms should be able to use nohup + // Use nohup to properly detach the process from the web request + // The process will continue running even after the PHP request ends + $command = sprintf( + 'nohup php %s partdb:update %s %s --force --no-interaction >> %s 2>&1 &', + escapeshellarg($consolePath), + escapeshellarg($targetVersion), + $createBackup ? '' : '--no-backup', + escapeshellarg($logFile) + ); + } + + $this->logger->info('Starting background update', [ + 'command' => $command, + 'target_version' => $targetVersion, + ]); + + // Execute in background using shell_exec for proper detachment + // shell_exec with & runs the command in background + + //@php-ignore-next-line We really need to use shell_exec here + $output = shell_exec($command); + + // Give it a moment to start + usleep(500000); // 500ms + + // Check if progress file was updated (indicates process started) + $progress = $this->getProgress(); + if ($progress && isset($progress['status'])) { + $this->logger->info('Background update started successfully'); + return 1; // Return a non-null value to indicate success + } + + $this->logger->error('Background update may not have started', ['output' => $output]); + return 1; // Still return success as the process might just be slow to start + } + + /** + * Execute update with progress file updates for web UI. + * This is called by the CLI command and updates the progress file. + */ + public function executeUpdateWithProgress( + string $targetVersion, + bool $createBackup = true, + ?callable $onProgress = null + ): array { + $totalSteps = 13; + $currentStep = 0; + + $updateProgress = function (string $stepName, string $message, bool $success = true) use (&$currentStep, $totalSteps, $targetVersion, $createBackup): void { + $currentStep++; + $progress = $this->getProgress() ?? [ + 'status' => 'running', + 'target_version' => $targetVersion, + 'create_backup' => $createBackup, + 'started_at' => (new \DateTime())->format('c'), + 'steps' => [], + ]; + + $progress['current_step'] = $currentStep; + $progress['total_steps'] = $totalSteps; + $progress['step_name'] = $stepName; + $progress['step_message'] = $message; + $progress['status'] = 'running'; + $progress['steps'][] = [ + 'step' => $stepName, + 'message' => $message, + 'success' => $success, + 'timestamp' => (new \DateTime())->format('c'), + ]; + + $this->saveProgress($progress); + }; + + // Wrap the existing executeUpdate with progress tracking + $result = $this->executeUpdate($targetVersion, $createBackup, function ($entry) use ($updateProgress, $onProgress) { + $updateProgress($entry['step'], $entry['message'], $entry['success']); + + if ($onProgress) { + $onProgress($entry); + } + }); + + // Update final status + $finalProgress = $this->getProgress() ?? []; + $finalProgress['status'] = $result['success'] ? 'completed' : 'failed'; + $finalProgress['completed_at'] = (new \DateTime())->format('c'); + $finalProgress['result'] = $result; + $finalProgress['error'] = $result['error']; + $this->saveProgress($finalProgress); + + return $result; + } +} diff --git a/src/Services/System/WatchtowerClient.php b/src/Services/System/WatchtowerClient.php new file mode 100644 index 00000000..87cc06fd --- /dev/null +++ b/src/Services/System/WatchtowerClient.php @@ -0,0 +1,125 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\System; + +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * HTTP client for communicating with the Watchtower container updater API. + * Used to trigger Docker container updates from the Part-DB UI. + * + * @see https://containrrr.dev/watchtower/ + */ +readonly class WatchtowerClient +{ + public function __construct( + private HttpClientInterface $httpClient, + private LoggerInterface $logger, + #[Autowire(env: 'WATCHTOWER_API_URL')] private string $apiUrl, + #[Autowire(env: 'WATCHTOWER_API_TOKEN')] private string $apiToken, + ) { + } + + /** + * Whether Watchtower integration is configured (URL and token are set). + */ + public function isConfigured(): bool + { + return $this->apiUrl !== '' && $this->apiToken !== ''; + } + + /** + * Check if the Watchtower API is reachable. + * Makes a lightweight HTTP request with a short timeout. + */ + public function isAvailable(): bool + { + if (!$this->isConfigured()) { + return false; + } + + try { + $response = $this->httpClient->request('GET', $this->getUpdateEndpoint(), [ + 'headers' => $this->getAuthHeaders(), + 'timeout' => 3, + ]); + + // Any response means Watchtower is reachable + $statusCode = $response->getStatusCode(); + return $statusCode < 500; + } catch (\Throwable $e) { + $this->logger->debug('Watchtower availability check failed: ' . $e->getMessage()); + return false; + } + } + + /** + * Trigger a container update via the Watchtower HTTP API. + * This is fire-and-forget: Watchtower will pull the new image and restart the container. + * + * @return bool True if Watchtower accepted the update request + */ + public function triggerUpdate(): bool + { + if (!$this->isConfigured()) { + throw new \RuntimeException('Watchtower is not configured. Set WATCHTOWER_API_URL and WATCHTOWER_API_TOKEN.'); + } + + try { + $response = $this->httpClient->request('POST', $this->getUpdateEndpoint(), [ + 'headers' => $this->getAuthHeaders(), + 'timeout' => 10, + ]); + + $statusCode = $response->getStatusCode(); + + if ($statusCode >= 200 && $statusCode < 300) { + $this->logger->info('Watchtower update triggered successfully.'); + return true; + } + + $this->logger->error('Watchtower update request returned HTTP ' . $statusCode); + return false; + } catch (\Throwable $e) { + $this->logger->error('Failed to trigger Watchtower update: ' . $e->getMessage()); + return false; + } + } + + private function getUpdateEndpoint(): string + { + return rtrim($this->apiUrl, '/') . '/v1/update'; + } + + /** + * @return array + */ + private function getAuthHeaders(): array + { + return [ + 'Authorization' => 'Bearer ' . $this->apiToken, + ]; + } +} diff --git a/src/Services/Tools/ExchangeRateUpdater.php b/src/Services/Tools/ExchangeRateUpdater.php index 6eb7ec13..ccc3f19f 100644 --- a/src/Services/Tools/ExchangeRateUpdater.php +++ b/src/Services/Tools/ExchangeRateUpdater.php @@ -44,15 +44,15 @@ class ExchangeRateUpdater try { //Try it in the direction QUOTE/BASE first, as most providers provide rates in this direction $rate = $this->swap->latest($currency->getIsoCode().'/'.$this->localizationSettings->baseCurrency); - $effective_rate = BigDecimal::of($rate->getValue()); + $effective_rate = BigDecimal::fromFloatShortest($rate->getValue()); } catch (UnsupportedCurrencyPairException|UnsupportedExchangeQueryException $exception) { //Otherwise try to get it inverse and calculate it ourselfes, from the format "BASE/QUOTE" $rate = $this->swap->latest($this->localizationSettings->baseCurrency.'/'.$currency->getIsoCode()); //The rate says how many quote units are worth one base unit //So we need to invert it to get the exchange rate - $rate_bd = BigDecimal::of($rate->getValue()); - $effective_rate = BigDecimal::one()->dividedBy($rate_bd, Currency::PRICE_SCALE, RoundingMode::HALF_UP); + $rate_bd = BigDecimal::fromFloatShortest($rate->getValue()); + $effective_rate = BigDecimal::one()->dividedBy($rate_bd, Currency::PRICE_SCALE, RoundingMode::HalfUp); } $currency->setExchangeRate($effective_rate); diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 036797f6..6397e3af 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -29,6 +29,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartCustomState; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; @@ -37,6 +38,9 @@ use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Helpers\Trees\TreeViewNode; use App\Services\Cache\UserCacheKeyGenerator; +use App\Services\ElementTypeNameGenerator; +use App\Services\InfoProviderSystem\Providers\GenericWebProvider; +use App\Settings\InfoProviderSystem\GenericWebProviderSettings; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -49,8 +53,15 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ class ToolsTreeBuilder { - public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security) - { + public function __construct( + protected TranslatorInterface $translator, + protected UrlGeneratorInterface $urlGenerator, + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected Security $security, + private readonly ElementTypeNameGenerator $elementTypeNameGenerator, + private readonly GenericWebProviderSettings $genericWebProviderSettings + ) { } /** @@ -138,7 +149,14 @@ class ToolsTreeBuilder $this->translator->trans('info_providers.search.title'), $this->urlGenerator->generate('info_providers_search') ))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down'); - + + if ($this->genericWebProviderSettings->enabled) { + $nodes[] = (new TreeViewNode( + $this->translator->trans('info_providers.from_url.title'), + $this->urlGenerator->generate('info_providers_from_url') + ))->setIcon('fa-treeview fa-fw fa-solid fa-book-atlas'); + } + $nodes[] = (new TreeViewNode( $this->translator->trans('info_providers.bulk_import.manage_jobs'), $this->urlGenerator->generate('bulk_info_provider_manage') @@ -159,64 +177,70 @@ class ToolsTreeBuilder if ($this->security->isGranted('read', new AttachmentType())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.attachment_types'), + $this->elementTypeNameGenerator->typeLabelPlural(AttachmentType::class), $this->urlGenerator->generate('attachment_type_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-file-alt'); } if ($this->security->isGranted('read', new Category())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.categories'), + $this->elementTypeNameGenerator->typeLabelPlural(Category::class), $this->urlGenerator->generate('category_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); } if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.projects'), + $this->elementTypeNameGenerator->typeLabelPlural(Project::class), $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.suppliers'), + $this->elementTypeNameGenerator->typeLabelPlural(Supplier::class), $this->urlGenerator->generate('supplier_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-truck'); } if ($this->security->isGranted('read', new Manufacturer())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.manufacturer'), + $this->elementTypeNameGenerator->typeLabelPlural(Manufacturer::class), $this->urlGenerator->generate('manufacturer_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); } if ($this->security->isGranted('read', new StorageLocation())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.storelocation'), + $this->elementTypeNameGenerator->typeLabelPlural(StorageLocation::class), $this->urlGenerator->generate('store_location_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-cube'); } if ($this->security->isGranted('read', new Footprint())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.footprint'), + $this->elementTypeNameGenerator->typeLabelPlural(Footprint::class), $this->urlGenerator->generate('footprint_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-microchip'); } if ($this->security->isGranted('read', new Currency())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.currency'), + $this->elementTypeNameGenerator->typeLabelPlural(Currency::class), $this->urlGenerator->generate('currency_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-coins'); } if ($this->security->isGranted('read', new MeasurementUnit())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.measurement_unit'), + $this->elementTypeNameGenerator->typeLabelPlural(MeasurementUnit::class), $this->urlGenerator->generate('measurement_unit_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-balance-scale'); } if ($this->security->isGranted('read', new LabelProfile())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.label_profile'), + $this->elementTypeNameGenerator->typeLabelPlural(LabelProfile::class), $this->urlGenerator->generate('label_profile_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-qrcode'); } + if ($this->security->isGranted('read', new PartCustomState())) { + $nodes[] = (new TreeViewNode( + $this->elementTypeNameGenerator->typeLabelPlural(PartCustomState::class), + $this->urlGenerator->generate('part_custom_state_new') + ))->setIcon('fa-fw fa-treeview fa-solid fa-tools'); + } if ($this->security->isGranted('create', new Part())) { $nodes[] = (new TreeViewNode( $this->translator->trans('tree.tools.edit.part'), @@ -301,6 +325,13 @@ class ToolsTreeBuilder ))->setIcon('fa fa-fw fa-gears fa-solid'); } + if ($this->security->isGranted('@system.show_updates')) { + $nodes[] = (new TreeViewNode( + $this->translator->trans('tree.tools.system.update_manager'), + $this->urlGenerator->generate('admin_update_manager') + ))->setIcon('fa-fw fa-treeview fa-solid fa-arrow-circle-up'); + } + return $nodes; } } diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 73ffa5ba..d55c87b7 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -34,9 +34,9 @@ use App\Entity\ProjectSystem\Project; use App\Helpers\Trees\TreeViewNode; use App\Helpers\Trees\TreeViewNodeIterator; use App\Repository\NamedDBElementRepository; -use App\Repository\StructuralDBElementRepository; use App\Services\Cache\ElementCacheTagGenerator; use App\Services\Cache\UserCacheKeyGenerator; +use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use App\Settings\BehaviorSettings\SidebarSettings; use Doctrine\ORM\EntityManagerInterface; @@ -67,6 +67,7 @@ class TreeViewGenerator protected TranslatorInterface $translator, private readonly UrlGeneratorInterface $router, private readonly SidebarSettings $sidebarSettings, + private readonly ElementTypeNameGenerator $elementTypeNameGenerator ) { $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; $this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded; @@ -212,15 +213,7 @@ class TreeViewGenerator protected function entityClassToRootNodeString(string $class): string { - return match ($class) { - Category::class => $this->translator->trans('category.labelp'), - StorageLocation::class => $this->translator->trans('storelocation.labelp'), - Footprint::class => $this->translator->trans('footprint.labelp'), - Manufacturer::class => $this->translator->trans('manufacturer.labelp'), - Supplier::class => $this->translator->trans('supplier.labelp'), - Project::class => $this->translator->trans('project.labelp'), - default => $this->translator->trans('tree.root_node.text'), - }; + return $this->elementTypeNameGenerator->typeLabelPlural($class); } protected function entityClassToRootNodeIcon(string $class): ?string diff --git a/src/Services/UserSystem/PermissionPresetsHelper.php b/src/Services/UserSystem/PermissionPresetsHelper.php index 554da8b3..3d125b27 100644 --- a/src/Services/UserSystem/PermissionPresetsHelper.php +++ b/src/Services/UserSystem/PermissionPresetsHelper.php @@ -102,6 +102,7 @@ class PermissionPresetsHelper $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'attachment_types', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'currencies', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'measurement_units', PermissionData::ALLOW); + $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'part_custom_states', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'suppliers', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'projects', PermissionData::ALLOW); @@ -110,8 +111,9 @@ class PermissionPresetsHelper //Allow to manage Oauth tokens $this->permissionResolver->setPermission($perm_holder, 'system', 'manage_oauth_tokens', PermissionData::ALLOW); - //Allow to show updates + //Allow to show and manage updates $this->permissionResolver->setPermission($perm_holder, 'system', 'show_updates', PermissionData::ALLOW); + $this->permissionResolver->setPermission($perm_holder, 'system', 'manage_updates', PermissionData::ALLOW); } @@ -131,6 +133,7 @@ class PermissionPresetsHelper $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'attachment_types', PermissionData::ALLOW, ['import']); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'currencies', PermissionData::ALLOW, ['import']); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'measurement_units', PermissionData::ALLOW, ['import']); + $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'part_custom_states', PermissionData::ALLOW, ['import']); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'suppliers', PermissionData::ALLOW, ['import']); $this->permissionResolver->setAllOperationsOfPermissionExcept($permHolder, 'projects', PermissionData::ALLOW, ['import']); diff --git a/src/Services/UserSystem/PermissionSchemaUpdater.php b/src/Services/UserSystem/PermissionSchemaUpdater.php index 104800dc..fd85ee7c 100644 --- a/src/Services/UserSystem/PermissionSchemaUpdater.php +++ b/src/Services/UserSystem/PermissionSchemaUpdater.php @@ -157,4 +157,20 @@ class PermissionSchemaUpdater $permissions->setPermissionValue('system', 'show_updates', $new_value); } } + + private function upgradeSchemaToVersion4(HasPermissionsInterface $holder): void //@phpstan-ignore-line This is called via reflection + { + $permissions = $holder->getPermissions(); + + //If the reports.generate permission is not defined yet, set it to the value of reports.read + if (!$permissions->isPermissionSet('parts_stock', 'stocktake')) { + //Set the new permission to true only if both add and withdraw are allowed + $new_value = TrinaryLogicHelper::and( + $permissions->getPermissionValue('parts_stock', 'withdraw'), + $permissions->getPermissionValue('parts_stock', 'add') + ); + + $permissions->setPermissionValue('parts_stock', 'stocktake', $new_value); + } + } } diff --git a/src/Services/UserSystem/UserAvatarHelper.php b/src/Services/UserSystem/UserAvatarHelper.php index 9dbe9c12..a1a69cb9 100644 --- a/src/Services/UserSystem/UserAvatarHelper.php +++ b/src/Services/UserSystem/UserAvatarHelper.php @@ -154,6 +154,7 @@ class UserAvatarHelper $attachment_type = new AttachmentType(); $attachment_type->setName('Avatars'); $attachment_type->setFiletypeFilter('image/*'); + $attachment_type->setAllowedTargets([UserAttachment::class]); $this->entityManager->persist($attachment_type); } diff --git a/src/Settings/AISettings/AISettings.php b/src/Settings/AISettings/AISettings.php new file mode 100644 index 00000000..732eb597 --- /dev/null +++ b/src/Settings/AISettings/AISettings.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\AISettings; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Settings\EmbeddedSettings; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(label: new TM("settings.ai"))] +#[SettingsIcon("fa-brain")] +class AISettings +{ + use SettingsTrait; + + #[EmbeddedSettings] + public ?OpenRouterSettings $openRouter = null; + + #[EmbeddedSettings] + public ?LMStudioSettings $lmstudio = null; +} diff --git a/src/Settings/AISettings/LMStudioSettings.php b/src/Settings/AISettings/LMStudioSettings.php new file mode 100644 index 00000000..2bdad06e --- /dev/null +++ b/src/Settings/AISettings/LMStudioSettings.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\AISettings; + +use App\Form\Type\APIKeyType; +use App\Services\AI\AIPlatformSettingsInterface; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Translation\StaticMessage; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))] +#[SettingsIcon("fa-robot")] +class LMStudioSettings implements AIPlatformSettingsInterface +{ + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ai.lmstudio.hosturl"), + formType: UrlType::class, + formOptions: ["attr" => ["placeholder" => new StaticMessage("http://localhost:1234")]], + envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $hostURL = null; + + public function isAIPlatformEnabled(): bool + { + return $this->hostURL !== null && $this->hostURL !== ""; + } +} diff --git a/src/Settings/AISettings/OpenRouterSettings.php b/src/Settings/AISettings/OpenRouterSettings.php new file mode 100644 index 00000000..e083513a --- /dev/null +++ b/src/Settings/AISettings/OpenRouterSettings.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\AISettings; + +use App\Form\Type\APIKeyType; +use App\Services\AI\AIPlatformSettingsInterface; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")] +#[SettingsIcon("fa-robot")] +class OpenRouterSettings implements AIPlatformSettingsInterface +{ + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ips.element14.apiKey"), + formType: APIKeyType::class, + formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $apiKey = null; + + public function isAIPlatformEnabled(): bool + { + return $this->apiKey !== null && $this->apiKey !== ""; + } +} diff --git a/src/Settings/AppSettings.php b/src/Settings/AppSettings.php index 42831d08..085f7ffe 100644 --- a/src/Settings/AppSettings.php +++ b/src/Settings/AppSettings.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Settings; +use App\Settings\AISettings\AISettings; use App\Settings\BehaviorSettings\BehaviorSettings; use App\Settings\InfoProviderSystem\InfoProviderSettings; use App\Settings\MiscSettings\MiscSettings; @@ -47,6 +48,15 @@ class AppSettings #[EmbeddedSettings()] public ?InfoProviderSettings $infoProviders = null; + #[EmbeddedSettings] + public ?SynonymSettings $synonyms = null; + + #[EmbeddedSettings] + public ?AISettings $ai = null; + #[EmbeddedSettings()] public ?MiscSettings $miscSettings = null; + + + } diff --git a/src/Settings/BehaviorSettings/BehaviorSettings.php b/src/Settings/BehaviorSettings/BehaviorSettings.php index 3053073f..ec849db3 100644 --- a/src/Settings/BehaviorSettings/BehaviorSettings.php +++ b/src/Settings/BehaviorSettings/BehaviorSettings.php @@ -41,4 +41,7 @@ class BehaviorSettings #[EmbeddedSettings] public ?PartInfoSettings $partInfo = null; + + #[EmbeddedSettings] + public ?KeybindingsSettings $keybindings = null; } diff --git a/src/Settings/BehaviorSettings/KeybindingsSettings.php b/src/Settings/BehaviorSettings/KeybindingsSettings.php new file mode 100644 index 00000000..2788de41 --- /dev/null +++ b/src/Settings/BehaviorSettings/KeybindingsSettings.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\BehaviorSettings; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(name: "keybindings", label: new TM("settings.behavior.keybindings"))] +#[SettingsIcon('fa-keyboard')] +class KeybindingsSettings +{ + /** + * Whether to enable special character keybindings (Alt+key) in text input fields + * @var bool + */ + #[SettingsParameter( + label: new TM("settings.behavior.keybindings.enable_special_characters"), + description: new TM("settings.behavior.keybindings.enable_special_characters.help"), + envVar: "bool:KEYBINDINGS_SPECIAL_CHARS_ENABLED", + envVarMode: EnvVarMode::OVERWRITE + )] + public bool $enableSpecialCharacters = true; +} diff --git a/src/Settings/BehaviorSettings/PartTableColumns.php b/src/Settings/BehaviorSettings/PartTableColumns.php index eea6ad86..32f6100b 100644 --- a/src/Settings/BehaviorSettings/PartTableColumns.php +++ b/src/Settings/BehaviorSettings/PartTableColumns.php @@ -46,9 +46,20 @@ enum PartTableColumns : string implements TranslatableInterface case FAVORITE = "favorite"; case MANUFACTURING_STATUS = "manufacturing_status"; case MPN = "manufacturer_product_number"; + case CUSTOM_PART_STATE = 'partCustomState'; case MASS = "mass"; + case GTIN = "gtin"; case TAGS = "tags"; case ATTACHMENTS = "attachments"; + + case SI_VALUE = "si_value"; + + case EDA_REFERENCE = "eda_reference"; + + case EDA_VALUE = "eda_value"; + + case EDA_STATUS = "eda_status"; + case EDIT = "edit"; public function trans(TranslatorInterface $translator, ?string $locale = null): string @@ -63,4 +74,4 @@ enum PartTableColumns : string implements TranslatableInterface return $translator->trans($key, locale: $locale); } -} \ No newline at end of file +} diff --git a/src/Settings/BehaviorSettings/TableSettings.php b/src/Settings/BehaviorSettings/TableSettings.php index b6964876..b3421e41 100644 --- a/src/Settings/BehaviorSettings/TableSettings.php +++ b/src/Settings/BehaviorSettings/TableSettings.php @@ -68,7 +68,7 @@ class TableSettings #[Assert\All([new Assert\Type(PartTableColumns::class)])] public array $partsDefaultColumns = [PartTableColumns::NAME, PartTableColumns::DESCRIPTION, PartTableColumns::CATEGORY, PartTableColumns::FOOTPRINT, PartTableColumns::MANUFACTURER, - PartTableColumns::LOCATION, PartTableColumns::AMOUNT]; + PartTableColumns::LOCATION, PartTableColumns::AMOUNT, PartTableColumns::CUSTOM_PART_STATE]; #[SettingsParameter(label: new TM("settings.behavior.table.preview_image_min_width"), formOptions: ['attr' => ['min' => 1, 'max' => 100]], diff --git a/src/Settings/InfoProviderSystem/AIExtractorSettings.php b/src/Settings/InfoProviderSystem/AIExtractorSettings.php new file mode 100644 index 00000000..af9753d9 --- /dev/null +++ b/src/Settings/InfoProviderSystem/AIExtractorSettings.php @@ -0,0 +1,78 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Form\Settings\AiModelsType; +use App\Form\Settings\AiPlatformChoiceType; +use App\Services\AI\AIPlatforms; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\AI\Platform\Capability; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Translation\StaticMessage; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints\Language; + +#[Settings(name: "ai_extractor", label: new TM("settings.ips.ai_extractor"), description: new TM("settings.ips.ai_extractor.description"))] +#[SettingsIcon("fa-plug")] +class AIExtractorSettings +{ + private const MODEL_SELECTOR_LABEL = 'ai_extractor'; + + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.ai_platform"), + formType: AiPlatformChoiceType::class, formOptions: ['platform_selector_label' => self::MODEL_SELECTOR_LABEL], + )] + public ?AIPlatforms $platform = null; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.model"), description: new TM("settings.ips.ai_extractor.model.help"), + formType: AiModelsType::class, formOptions: [ + 'platform_selector' => self::MODEL_SELECTOR_LABEL, 'filter_capability' => Capability::OUTPUT_STRUCTURED, + 'attr' => ['placeholder' => new StaticMessage('google/gemini-2.5-flash-lite')] + ], + + )] + public ?string $model = null; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.max_content_length"), + description: new TM("settings.ips.ai_extractor.max_content_length.description"), + )] + public int $maxContentLength = 50000; + + #[Language] + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.output_language"), description: new TM("settings.ips.ai_extractor.output_language.description"), + formType: LanguageType::class, + )] + public ?string $outputLanguage = null; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.additional_instructions"), description: new TM("settings.ips.ai_extractor.additional_instructions.description"), + formType: TextareaType::class, + )] + public ?string $additionalInstructions = null; +} diff --git a/src/Settings/InfoProviderSystem/BrowserPluginSettings.php b/src/Settings/InfoProviderSystem/BrowserPluginSettings.php new file mode 100644 index 00000000..1ad5c50b --- /dev/null +++ b/src/Settings/InfoProviderSystem/BrowserPluginSettings.php @@ -0,0 +1,40 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(name: "browser_plugin", label: new TM("settings.ips.browser_plugin"), description: new TM("settings.ips.browser_plugin.description"))] +#[SettingsIcon("fa-cloud-arrow-up")] +class BrowserPluginSettings +{ + #[SettingsParameter(label: new TM("settings.ips.lcsc.enabled"), description: new TM("settings.ips.browser_plugin.enabled.help"), + envVar: "bool:BROWSER_PLUGIN_ENABLED", envVarMode: EnvVarMode::OVERWRITE + )] + public bool $enabled = false; +} diff --git a/src/Settings/InfoProviderSystem/BuerklinSettings.php b/src/Settings/InfoProviderSystem/BuerklinSettings.php new file mode 100644 index 00000000..c083c07a --- /dev/null +++ b/src/Settings/InfoProviderSystem/BuerklinSettings.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Form\Type\APIKeyType; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\CountryType; +use Symfony\Component\Form\Extension\Core\Type\CurrencyType; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(label: new TM("settings.ips.buerklin"), description: new TM("settings.ips.buerklin.help"))] +#[SettingsIcon("fa-plug")] +class BuerklinSettings +{ + use SettingsTrait; + + #[SettingsParameter( + label: new TM("settings.ips.digikey.client_id"), + formType: APIKeyType::class, + envVar: "PROVIDER_BUERKLIN_CLIENT_ID", envVarMode: EnvVarMode::OVERWRITE + )] + public ?string $clientId = null; + + #[SettingsParameter( + label: new TM("settings.ips.digikey.secret"), + formType: APIKeyType::class, + envVar: "PROVIDER_BUERKLIN_SECRET", envVarMode: EnvVarMode::OVERWRITE + )] + public ?string $secret = null; + + #[SettingsParameter( + label: new TM("settings.ips.buerklin.username"), + formType: APIKeyType::class, + envVar: "PROVIDER_BUERKLIN_USER", envVarMode: EnvVarMode::OVERWRITE + )] + public ?string $username = null; + + #[SettingsParameter( + label: new TM("user.edit.password"), + formType: APIKeyType::class, + envVar: "PROVIDER_BUERKLIN_PASSWORD", envVarMode: EnvVarMode::OVERWRITE + )] + public ?string $password = null; + + #[SettingsParameter(label: new TM("settings.ips.tme.currency"), formType: CurrencyType::class, + formOptions: ["preferred_choices" => ["EUR"]], + envVar: "PROVIDER_BUERKLIN_CURRENCY", envVarMode: EnvVarMode::OVERWRITE)] + #[Assert\Currency()] + public string $currency = "EUR"; + + #[SettingsParameter(label: new TM("settings.ips.tme.language"), formType: LanguageType::class, + formOptions: ["preferred_choices" => ["en", "de"]], + envVar: "PROVIDER_BUERKLIN_LANGUAGE", envVarMode: EnvVarMode::OVERWRITE)] + #[Assert\Language] + public string $language = "en"; +} diff --git a/src/Settings/InfoProviderSystem/CanopySettings.php b/src/Settings/InfoProviderSystem/CanopySettings.php new file mode 100644 index 00000000..3c97a80e --- /dev/null +++ b/src/Settings/InfoProviderSystem/CanopySettings.php @@ -0,0 +1,96 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Form\Type\APIKeyType; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\CountryType; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(label: new TM("settings.ips.canopy"))] +#[SettingsIcon("fa-plug")] +class CanopySettings +{ + public const ALLOWED_DOMAINS = [ + "amazon.de" => "DE", + "amazon.com" => "US", + "amazon.co.uk" => "UK", + "amazon.fr" => "FR", + "amazon.it" => "IT", + "amazon.es" => "ES", + "amazon.ca" => "CA", + "amazon.com.au" => "AU", + "amazon.com.br" => "BR", + "amazon.com.mx" => "MX", + "amazon.in" => "IN", + "amazon.co.jp" => "JP", + "amazon.nl" => "NL", + "amazon.pl" => "PL", + "amazon.sa" => "SA", + "amazon.sg" => "SG", + "amazon.se" => "SE", + "amazon.com.tr" => "TR", + "amazon.ae" => "AE", + "amazon.com.be" => "BE", + "amazon.com.cn" => "CN", + ]; + + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ips.mouser.apiKey"), + formType: APIKeyType::class, + formOptions: ["help_html" => true], envVar: "PROVIDER_CANOPY_API_KEY", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $apiKey = null; + + /** + * @var string The domain used internally for the API requests. This is not necessarily the same as the domain shown to the user, which is determined by the keys of the ALLOWED_DOMAINS constant + */ + #[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: ChoiceType::class, formOptions: ["choices" => self::ALLOWED_DOMAINS, 'translation_domain' => false])] + public string $domain = "DE"; + + /** + * @var bool If true, the provider will always retrieve details for a part, resulting in an additional API request + */ + #[SettingsParameter(label: new TM("settings.ips.canopy.alwaysGetDetails"), description: new TM("settings.ips.canopy.alwaysGetDetails.help"))] + public bool $alwaysGetDetails = false; + + /** + * Returns the real domain (e.g. amazon.de) based on the selected domain (e.g. DE) + * @return string + */ + public function getRealDomain(): string + { + $domain = array_search($this->domain, self::ALLOWED_DOMAINS, true); + if ($domain === false) { + throw new \InvalidArgumentException("Invalid domain selected"); + } + return $domain; + } +} diff --git a/src/Settings/InfoProviderSystem/ConradSettings.php b/src/Settings/InfoProviderSystem/ConradSettings.php new file mode 100644 index 00000000..dda884c8 --- /dev/null +++ b/src/Settings/InfoProviderSystem/ConradSettings.php @@ -0,0 +1,77 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Form\Type\APIKeyType; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; +use Jbtronics\SettingsBundle\ParameterTypes\StringType; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\CountryType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(label: new TM("settings.ips.conrad"))] +#[SettingsIcon("fa-plug")] +class ConradSettings +{ + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ips.element14.apiKey"), + formType: APIKeyType::class, + formOptions: ["help_html" => true], envVar: "PROVIDER_CONRAD_API_KEY", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $apiKey = null; + + #[SettingsParameter(label: new TM("settings.ips.conrad.shopID"), + description: new TM("settings.ips.conrad.shopID.description"), + formType: EnumType::class, + formOptions: ['class' => ConradShopIDs::class], + )] + public ConradShopIDs $shopID = ConradShopIDs::COM_B2B; + + #[SettingsParameter(label: new TM("settings.ips.reichelt.include_vat"))] + public bool $includeVAT = true; + + /** + * @var array|string[] Only attachments in these languages will be downloaded (ISO 639-1 codes) + */ + #[Assert\Unique()] + #[Assert\All([new Assert\Language()])] + #[SettingsParameter(type: ArrayType::class, + label: new TM("settings.ips.conrad.attachment_language_filter"), description: new TM("settings.ips.conrad.attachment_language_filter.description"), + options: ['type' => StringType::class], + formType: LanguageType::class, + formOptions: [ + 'multiple' => true, + 'preferred_choices' => ['en', 'de', 'fr', 'it', 'cs', 'da', 'nl', 'hu', 'hr', 'sk', 'pl'] + ], + )] + public array $attachmentLanguageFilter = ['en']; +} diff --git a/src/Settings/InfoProviderSystem/ConradShopIDs.php b/src/Settings/InfoProviderSystem/ConradShopIDs.php new file mode 100644 index 00000000..e39ed7b1 --- /dev/null +++ b/src/Settings/InfoProviderSystem/ConradShopIDs.php @@ -0,0 +1,167 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum ConradShopIDs: string implements TranslatableInterface +{ + case COM_B2B = 'HP_COM_B2B'; + case DE_B2B = 'CQ_DE_B2B'; + case DE_B2C = 'CQ_DE_B2C'; + case AT_B2C = 'CQ_AT_B2C'; + case CH_B2C_DE = 'CQ_CH_B2C_DE'; + case CH_B2C_FR = 'CQ_CH_B2C_FR'; + case SE_B2B = 'HP_SE_B2B'; + case HU_B2C = 'CQ_HU_B2C'; + case CZ_B2B = 'HP_CZ_B2B'; + case SI_B2B = 'HP_SI_B2B'; + case SK_B2B = 'HP_SK_B2B'; + case BE_B2B = 'HP_BE_B2B'; + case PL_B2B = 'HP_PL_B2B'; + case NL_B2B = 'CQ_NL_B2B'; + case NL_B2C = 'CQ_NL_B2C'; + case DK_B2B = 'HP_DK_B2B'; + case IT_B2B = 'HP_IT_B2B'; + + case FR_B2B = 'HP_FR_B2B'; + case AT_B2B = 'CQ_AT_B2B'; + case HR_B2B = 'HP_HR_B2B'; + + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return match ($this) { + self::DE_B2B => "conrad.de (B2B)", + self::AT_B2C => "conrad.at (B2C)", + self::CH_B2C_DE => "conrad.ch DE (B2C)", + self::CH_B2C_FR => "conrad.ch FR (B2C)", + self::SE_B2B => "conrad.se (B2B)", + self::HU_B2C => "conrad.hu (B2C)", + self::CZ_B2B => "conrad.cz (B2B)", + self::SI_B2B => "conrad.si (B2B)", + self::SK_B2B => "conrad.sk (B2B)", + self::BE_B2B => "conrad.be (B2B)", + self::DE_B2C => "conrad.de (B2C)", + self::PL_B2B => "conrad.pl (B2B)", + self::NL_B2B => "conrad.nl (B2B)", + self::DK_B2B => "conradelektronik.dk (B2B)", + self::IT_B2B => "conrad.it (B2B)", + self::NL_B2C => "conrad.nl (B2C)", + self::FR_B2B => "conrad.fr (B2B)", + self::COM_B2B => "conrad.com (B2B)", + self::AT_B2B => "conrad.at (B2B)", + self::HR_B2B => "conrad.hr (B2B)", + }; + } + + public function getDomain(): string + { + if ($this === self::DK_B2B) { + return 'conradelektronik.dk'; + } + + return 'conrad.' . $this->getDomainEnd(); + } + + /** + * Retrieves the API root URL for this shop ID. e.g. https://api.conrad.de + * @return string + */ + public function getAPIRoot(): string + { + return 'https://api.' . $this->getDomain(); + } + + /** + * Returns the shop ID value used in the API requests. e.g. 'CQ_DE_B2B' + * @return string + */ + public function getShopID(): string + { + if ($this === self::CH_B2C_FR || $this === self::CH_B2C_DE) { + return 'CQ_CH_B2C'; + } + + return $this->value; + } + + public function getDomainEnd(): string + { + return match ($this) { + self::DE_B2B, self::DE_B2C => 'de', + self::AT_B2B, self::AT_B2C => 'at', + self::CH_B2C_DE => 'ch', self::CH_B2C_FR => 'ch', + self::SE_B2B => 'se', + self::HU_B2C => 'hu', + self::CZ_B2B => 'cz', + self::SI_B2B => 'si', + self::SK_B2B => 'sk', + self::BE_B2B => 'be', + self::PL_B2B => 'pl', + self::NL_B2B, self::NL_B2C => 'nl', + self::DK_B2B => 'dk', + self::IT_B2B => 'it', + self::FR_B2B => 'fr', + self::COM_B2B => 'com', + self::HR_B2B => 'hr', + }; + } + + public function getLanguage(): string + { + return match ($this) { + self::DE_B2B, self::DE_B2C, self::AT_B2B, self::AT_B2C => 'de', + self::CH_B2C_DE => 'de', self::CH_B2C_FR => 'fr', + self::SE_B2B => 'sv', + self::HU_B2C => 'hu', + self::CZ_B2B => 'cs', + self::SI_B2B => 'sl', + self::SK_B2B => 'sk', + self::BE_B2B => 'nl', + self::PL_B2B => 'pl', + self::NL_B2B, self::NL_B2C => 'nl', + self::DK_B2B => 'da', + self::IT_B2B => 'it', + self::FR_B2B => 'fr', + self::COM_B2B => 'en', + self::HR_B2B => 'hr', + }; + } + + /** + * Retrieves the customer type for this shop ID. e.g. 'b2b' or 'b2c' + * @return string 'b2b' or 'b2c' + */ + public function getCustomerType(): string + { + return match ($this) { + self::DE_B2B, self::AT_B2B, self::SE_B2B, self::CZ_B2B, self::SI_B2B, + self::SK_B2B, self::BE_B2B, self::PL_B2B, self::NL_B2B, self::DK_B2B, + self::IT_B2B, self::FR_B2B, self::COM_B2B, self::HR_B2B => 'b2b', + self::DE_B2C, self::AT_B2C, self::CH_B2C_DE, self::CH_B2C_FR, self::HU_B2C, self::NL_B2C => 'b2c', + }; + } +} diff --git a/src/Settings/InfoProviderSystem/GenericWebProviderSettings.php b/src/Settings/InfoProviderSystem/GenericWebProviderSettings.php new file mode 100644 index 00000000..07972141 --- /dev/null +++ b/src/Settings/InfoProviderSystem/GenericWebProviderSettings.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(name: "generic_web_provider", label: new TM("settings.ips.generic_web_provider"), description: new TM("settings.ips.generic_web_provider.description"))] +#[SettingsIcon("fa-plug")] +class GenericWebProviderSettings +{ + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ips.lcsc.enabled"), description: new TM("settings.ips.generic_web_provider.enabled.help"), + envVar: "bool:PROVIDER_GENERIC_WEB_ENABLED", envVarMode: EnvVarMode::OVERWRITE + )] + public bool $enabled = false; +} diff --git a/src/Settings/InfoProviderSystem/InfoProviderSettings.php b/src/Settings/InfoProviderSystem/InfoProviderSettings.php index e6e258f5..96de19cb 100644 --- a/src/Settings/InfoProviderSystem/InfoProviderSettings.php +++ b/src/Settings/InfoProviderSystem/InfoProviderSettings.php @@ -37,6 +37,15 @@ class InfoProviderSettings #[EmbeddedSettings] public ?InfoProviderGeneralSettings $general = null; + #[EmbeddedSettings] + public ?BrowserPluginSettings $browserPlugin = null; + + #[EmbeddedSettings] + public ?GenericWebProviderSettings $genericWebProvider = null; + + #[EmbeddedSettings] + public ?AIExtractorSettings $aiExtractor = null; + #[EmbeddedSettings] public ?DigikeySettings $digikey = null; @@ -63,4 +72,14 @@ class InfoProviderSettings #[EmbeddedSettings] public ?PollinSettings $pollin = null; + + #[EmbeddedSettings] + public ?BuerklinSettings $buerklin = null; + + #[EmbeddedSettings] + public ?ConradSettings $conrad = null; + + #[EmbeddedSettings] + public ?CanopySettings $canopy = null; + } diff --git a/src/Settings/MiscSettings/IpnSuggestSettings.php b/src/Settings/MiscSettings/IpnSuggestSettings.php new file mode 100644 index 00000000..2c2cb21a --- /dev/null +++ b/src/Settings/MiscSettings/IpnSuggestSettings.php @@ -0,0 +1,109 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\MiscSettings; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\ParameterTypes\StringType; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\StaticMessage; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(label: new TM("settings.misc.ipn_suggest"))] +#[SettingsIcon("fa-arrow-up-1-9")] +class IpnSuggestSettings +{ + use SettingsTrait; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.regex"), + description: new TM("settings.misc.ipn_suggest.regex.help"), + options: ['type' => StringType::class], + formOptions: ['attr' => ['placeholder' => new StaticMessage( '^[A-Za-z0-9]{3,4}(?:-[A-Za-z0-9]{3,4})*-\d{4}$')]], + envVar: "IPN_SUGGEST_REGEX", envVarMode: EnvVarMode::OVERWRITE, + )] + public ?string $regex = null; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.regex_help"), + description: new TM("settings.misc.ipn_suggest.regex_help_description"), + options: ['type' => StringType::class], + formOptions: ['attr' => ['placeholder' => new TM('settings.misc.ipn_suggest.regex.help.placeholder')]], + envVar: "IPN_SUGGEST_REGEX_HELP", envVarMode: EnvVarMode::OVERWRITE, + )] + public ?string $regexHelp = null; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.autoAppendSuffix"), + envVar: "bool:IPN_AUTO_APPEND_SUFFIX", envVarMode: EnvVarMode::OVERWRITE, + )] + public bool $autoAppendSuffix = false; + + #[SettingsParameter(label: new TM("settings.misc.ipn_suggest.suggestPartDigits"), + description: new TM("settings.misc.ipn_suggest.suggestPartDigits.help"), + formOptions: ['attr' => ['min' => 1, 'max' => 8]], + envVar: "int:IPN_SUGGEST_PART_DIGITS", envVarMode: EnvVarMode::OVERWRITE + )] + #[Assert\Range(min: 1, max: 8)] + public int $suggestPartDigits = 4; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.useDuplicateDescription"), + description: new TM("settings.misc.ipn_suggest.useDuplicateDescription.help"), + envVar: "bool:IPN_USE_DUPLICATE_DESCRIPTION", envVarMode: EnvVarMode::OVERWRITE, + )] + public bool $useDuplicateDescription = false; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.fallbackPrefix"), + description: new TM("settings.misc.ipn_suggest.fallbackPrefix.help"), + options: ['type' => StringType::class], + )] + public string $fallbackPrefix = 'N.A.'; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.numberSeparator"), + description: new TM("settings.misc.ipn_suggest.numberSeparator.help"), + options: ['type' => StringType::class], + )] + public string $numberSeparator = '-'; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.categorySeparator"), + description: new TM("settings.misc.ipn_suggest.categorySeparator.help"), + options: ['type' => StringType::class], + )] + public string $categorySeparator = '-'; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.globalPrefix"), + description: new TM("settings.misc.ipn_suggest.globalPrefix.help"), + options: ['type' => StringType::class], + )] + public ?string $globalPrefix = null; +} diff --git a/src/Settings/MiscSettings/KiCadEDASettings.php b/src/Settings/MiscSettings/KiCadEDASettings.php index d8f1026d..dd223007 100644 --- a/src/Settings/MiscSettings/KiCadEDASettings.php +++ b/src/Settings/MiscSettings/KiCadEDASettings.php @@ -43,4 +43,29 @@ class KiCadEDASettings envVar: "int:EDA_KICAD_CATEGORY_DEPTH", envVarMode: EnvVarMode::OVERWRITE)] #[Assert\Range(min: -1)] public int $categoryDepth = 0; -} \ No newline at end of file + + #[SettingsParameter(label: new TM("settings.misc.kicad_eda.datasheet_link"), + description: new TM("settings.misc.kicad_eda.datasheet_link.help") + )] + public ?bool $datasheetAsPdf = true; + + #[SettingsParameter( + label: new TM("settings.misc.kicad_eda.default_parameter_visibility"), + description: new TM("settings.misc.kicad_eda.default_parameter_visibility.help"), + + )] + public bool $defaultParameterVisibility = false; + + #[SettingsParameter( + label: new TM("settings.misc.kicad_eda.default_orderdetails_visibility"), + description: new TM("settings.misc.kicad_eda.default_orderdetails_visibility.help"), + + )] + public bool $defaultOrderdetailsVisibility = false; + + #[SettingsParameter( + label: new TM("settings.misc.kicad_eda.use_custom_list"), + description: new TM("settings.misc.kicad_eda.use_custom_list.help"), + )] + public bool $useCustomList = false; +} diff --git a/src/Settings/MiscSettings/MiscSettings.php b/src/Settings/MiscSettings/MiscSettings.php index 1c304d4a..050dbcbc 100644 --- a/src/Settings/MiscSettings/MiscSettings.php +++ b/src/Settings/MiscSettings/MiscSettings.php @@ -35,4 +35,7 @@ class MiscSettings #[EmbeddedSettings] public ?ExchangeRateSettings $exchangeRate = null; + + #[EmbeddedSettings] + public ?IpnSuggestSettings $ipnSuggestSettings = null; } diff --git a/src/Settings/SynonymSettings.php b/src/Settings/SynonymSettings.php new file mode 100644 index 00000000..25fc87e9 --- /dev/null +++ b/src/Settings/SynonymSettings.php @@ -0,0 +1,116 @@ +. + */ + +declare(strict_types=1); + +namespace App\Settings; + +use App\Form\Settings\TypeSynonymsCollectionType; +use App\Services\ElementTypes; +use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; +use Jbtronics\SettingsBundle\ParameterTypes\SerializeType; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(label: new TM("settings.synonyms"), description: "settings.synonyms.help")] +#[SettingsIcon("fa-language")] +class SynonymSettings +{ + use SettingsTrait; + + #[SettingsParameter( + ArrayType::class, + label: new TM("settings.synonyms.type_synonyms"), + description: new TM("settings.synonyms.type_synonyms.help"), + options: ['type' => SerializeType::class], + formType: TypeSynonymsCollectionType::class, + formOptions: [ + 'required' => false, + ], + )] + #[Assert\Type('array')] + #[Assert\All([new Assert\Type('array')])] + /** + * @var array> $typeSynonyms + * An array of the form: [ + * 'category' => [ + * 'en' => ['singular' => 'Category', 'plural' => 'Categories'], + * 'de' => ['singular' => 'Kategorie', 'plural' => 'Kategorien'], + * ], + * 'manufacturer' => [ + * 'en' => ['singular' => 'Manufacturer', 'plural' =>'Manufacturers'], + * ], + * ] + */ + public array $typeSynonyms = []; + + /** + * Checks if there is any synonym defined for the given type (no matter which language). + * @param ElementTypes $type + * @return bool + */ + public function isSynonymDefinedForType(ElementTypes $type): bool + { + return isset($this->typeSynonyms[$type->value]) && count($this->typeSynonyms[$type->value]) > 0; + } + + /** + * Returns the singular synonym for the given type and locale, or null if none is defined. + * @param ElementTypes $type + * @param string $locale + * @return string|null + */ + public function getSingularSynonymForType(ElementTypes $type, string $locale): ?string + { + return $this->typeSynonyms[$type->value][$locale]['singular'] ?? null; + } + + /** + * Returns the plural synonym for the given type and locale, or null if none is defined. + * @param ElementTypes $type + * @param string|null $locale + * @return string|null + */ + public function getPluralSynonymForType(ElementTypes $type, ?string $locale): ?string + { + return $this->typeSynonyms[$type->value][$locale]['plural'] + ?? $this->typeSynonyms[$type->value][$locale]['singular'] + ?? null; + } + + /** + * Sets a synonym for the given type and locale. + * @param ElementTypes $type + * @param string $locale + * @param string $singular + * @param string $plural + * @return void + */ + public function setSynonymForType(ElementTypes $type, string $locale, string $singular, string $plural): void + { + $this->typeSynonyms[$type->value][$locale] = [ + 'singular' => $singular, + 'plural' => $plural, + ]; + } +} diff --git a/src/Settings/SystemSettings/AttachmentsSettings.php b/src/Settings/SystemSettings/AttachmentsSettings.php index 6d15c639..5aa3f91d 100644 --- a/src/Settings/SystemSettings/AttachmentsSettings.php +++ b/src/Settings/SystemSettings/AttachmentsSettings.php @@ -58,4 +58,11 @@ class AttachmentsSettings envVar: "bool:ATTACHMENT_DOWNLOAD_BY_DEFAULT", envVarMode: EnvVarMode::OVERWRITE )] public bool $downloadByDefault = false; -} \ No newline at end of file + + #[SettingsParameter( + label: new TM("settings.system.attachments.showHTMLAttachments"), + description: new TM("settings.system.attachments.showHTMLAttachments.help"), + envVar: "bool:ATTACHMENT_SHOW_HTML_FILES", envVarMode: EnvVarMode::OVERWRITE + )] + public bool $showHTMLAttachments = false; +} diff --git a/src/Settings/SystemSettings/LocalizationSettings.php b/src/Settings/SystemSettings/LocalizationSettings.php index 7c83d1ef..d0c3ce75 100644 --- a/src/Settings/SystemSettings/LocalizationSettings.php +++ b/src/Settings/SystemSettings/LocalizationSettings.php @@ -23,8 +23,9 @@ declare(strict_types=1); namespace App\Settings\SystemSettings; -use App\Form\Type\LanguageMenuEntriesType; +use App\Form\Settings\LanguageMenuEntriesType; use App\Form\Type\LocaleSelectType; +use App\Form\Type\TriStateCheckboxType; use App\Settings\SettingsIcon; use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; @@ -46,7 +47,7 @@ class LocalizationSettings #[Assert\Locale()] #[Assert\NotBlank()] #[SettingsParameter(label: new TM("settings.system.localization.locale"), formType: LocaleSelectType::class, - envVar: "string:DEFAULT_LANG", envVarMode: EnvVarMode::OVERWRITE)] + envVar: "string:DEFAULT_LANG", envVarMode: EnvVarMode::OVERWRITE)] public string $locale = 'en'; #[Assert\Timezone()] @@ -73,4 +74,14 @@ class LocalizationSettings )] #[Assert\All([new Assert\Locale()])] public array $languageMenuEntries = []; + + #[SettingsParameter(label: new TM("settings.system.localization.prices_include_tax_by_default"), + description: new TM("settings.system.localization.prices_include_tax_by_default.description"), + formType: TriStateCheckboxType::class + )] + /** + * Indicates whether prices should include tax by default. This is used when creating new pricedetails. + * Null means that the VAT state should be indetermine by default. + */ + public ?bool $pricesIncludeTaxByDefault = null; } diff --git a/src/Settings/SystemSettings/SystemSettings.php b/src/Settings/SystemSettings/SystemSettings.php index 71dd963d..8cbeb560 100644 --- a/src/Settings/SystemSettings/SystemSettings.php +++ b/src/Settings/SystemSettings/SystemSettings.php @@ -33,6 +33,8 @@ class SystemSettings #[EmbeddedSettings()] public ?LocalizationSettings $localization = null; + + #[EmbeddedSettings()] public ?CustomizationSettings $customization = null; diff --git a/src/State/LabelGenerationProcessor.php b/src/State/LabelGenerationProcessor.php new file mode 100644 index 00000000..0472bbbd --- /dev/null +++ b/src/State/LabelGenerationProcessor.php @@ -0,0 +1,141 @@ +. + */ + +namespace App\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\ApiResource\LabelGenerationRequest; +use App\Entity\Base\AbstractDBElement; +use App\Entity\LabelSystem\LabelProfile; +use App\Repository\DBElementRepository; +use App\Repository\LabelProfileRepository; +use App\Services\ElementTypeNameGenerator; +use App\Services\LabelSystem\LabelGenerator; +use App\Services\Misc\RangeParser; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +class LabelGenerationProcessor implements ProcessorInterface +{ + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly LabelGenerator $labelGenerator, + private readonly RangeParser $rangeParser, + private readonly ElementTypeNameGenerator $elementTypeNameGenerator, + private readonly Security $security, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Response + { + // Check if user has permission to create labels + if (!$this->security->isGranted('@labels.create_labels')) { + throw new AccessDeniedHttpException('You do not have permission to generate labels.'); + } + + if (!$data instanceof LabelGenerationRequest) { + throw new BadRequestHttpException('Invalid request data for label generation.'); + } + + /** @var LabelGenerationRequest $request */ + $request = $data; + + // Fetch the label profile + /** @var LabelProfileRepository $profileRepo */ + $profileRepo = $this->entityManager->getRepository(LabelProfile::class); + $profile = $profileRepo->find($request->profileId); + if (!$profile instanceof LabelProfile) { + throw new NotFoundHttpException(sprintf('Label profile with ID %d not found.', $request->profileId)); + } + + // Check if user has read permission for the profile + if (!$this->security->isGranted('read', $profile)) { + throw new AccessDeniedHttpException('You do not have permission to access this label profile.'); + } + + // Get label options from profile + $options = $profile->getOptions(); + + // Override element type if provided, otherwise use profile's default + if ($request->elementType !== null) { + $options->setSupportedElement($request->elementType); + } + + // Parse element IDs from the range string + try { + $idArray = $this->rangeParser->parse($request->elementIds); + } catch (\InvalidArgumentException $e) { + throw new BadRequestHttpException('Invalid element IDs format: ' . $e->getMessage()); + } + + if (empty($idArray)) { + throw new BadRequestHttpException('No valid element IDs provided.'); + } + + // Fetch the target entities + /** @var DBElementRepository $repo */ + $repo = $this->entityManager->getRepository($options->getSupportedElement()->getEntityClass()); + + $elements = $repo->getElementsFromIDArray($idArray); + + if (empty($elements)) { + throw new NotFoundHttpException('No elements found with the provided IDs.'); + } + + // Generate the PDF + try { + $pdfContent = $this->labelGenerator->generateLabel($options, $elements); + } catch (\Exception $e) { + throw new BadRequestHttpException('Failed to generate label: ' . $e->getMessage()); + } + + // Generate filename + $filename = $this->generateFilename($elements[0], $profile); + + + // Return PDF as response + return new Response( + $pdfContent, + Response::HTTP_OK, + [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => sprintf('attachment; filename="%s"', $filename), + 'Content-Length' => (string) strlen($pdfContent), + ] + ); + } + + private function generateFilename(AbstractDBElement $element, LabelProfile $profile): string + { + $ret = 'label_' . $this->elementTypeNameGenerator->typeLabel($element); + $ret .= $element->getID(); + $ret .= '_' . preg_replace('/[^a-z0-9_\-]/i', '_', $profile->getName()); + + return $ret . '.pdf'; + } +} diff --git a/src/State/PartDBInfoProvider.php b/src/State/PartDBInfoProvider.php index b3496cad..b29ef227 100644 --- a/src/State/PartDBInfoProvider.php +++ b/src/State/PartDBInfoProvider.php @@ -7,8 +7,8 @@ namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\PartDBInfo; -use App\Services\Misc\GitVersionInfo; use App\Services\System\BannerHelper; +use App\Services\System\GitVersionInfoProvider; use App\Settings\SystemSettings\CustomizationSettings; use App\Settings\SystemSettings\LocalizationSettings; use Shivas\VersioningBundle\Service\VersionManagerInterface; @@ -17,7 +17,7 @@ class PartDBInfoProvider implements ProviderInterface { public function __construct(private readonly VersionManagerInterface $versionManager, - private readonly GitVersionInfo $gitVersionInfo, + private readonly GitVersionInfoProvider $gitVersionInfo, private readonly BannerHelper $bannerHelper, private readonly string $default_uri, private readonly LocalizationSettings $localizationSettings, @@ -31,8 +31,8 @@ class PartDBInfoProvider implements ProviderInterface { return new PartDBInfo( version: $this->versionManager->getVersion()->toString(), - git_branch: $this->gitVersionInfo->getGitBranchName(), - git_commit: $this->gitVersionInfo->getGitCommitHash(), + git_branch: $this->gitVersionInfo->getBranchName(), + git_commit: $this->gitVersionInfo->getCommitHash(), title: $this->customizationSettings->instanceName, banner: $this->bannerHelper->getBanner(), default_uri: $this->default_uri, diff --git a/src/Translation/Fixes/SegmentAwareXliffFileDumper.php b/src/Translation/Fixes/SegmentAwareXliffFileDumper.php deleted file mode 100644 index 4b44ef01..00000000 --- a/src/Translation/Fixes/SegmentAwareXliffFileDumper.php +++ /dev/null @@ -1,242 +0,0 @@ -. - */ - -declare(strict_types=1); - - -namespace App\Translation\Fixes; - -use Symfony\Component\DependencyInjection\Attribute\AsDecorator; -use Symfony\Component\Translation\Dumper\FileDumper; -use Symfony\Component\Translation\MessageCatalogue; -use Symfony\Component\Translation\Exception\InvalidArgumentException; - -/** - * Backport of the XliffFile dumper from Symfony 7.2, which supports segment attributes and notes, this keeps the - * metadata when editing the translations from inside Symfony. - */ -#[AsDecorator("translation.dumper.xliff")] -class SegmentAwareXliffFileDumper extends FileDumper -{ - - public function __construct( - private string $extension = 'xlf', - ) { - } - - public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string - { - $xliffVersion = '1.2'; - if (\array_key_exists('xliff_version', $options)) { - $xliffVersion = $options['xliff_version']; - } - - if (\array_key_exists('default_locale', $options)) { - $defaultLocale = $options['default_locale']; - } else { - $defaultLocale = \Locale::getDefault(); - } - - if ('1.2' === $xliffVersion) { - return $this->dumpXliff1($defaultLocale, $messages, $domain, $options); - } - if ('2.0' === $xliffVersion) { - return $this->dumpXliff2($defaultLocale, $messages, $domain); - } - - throw new InvalidArgumentException(\sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion)); - } - - protected function getExtension(): string - { - return $this->extension; - } - - private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ?string $domain, array $options = []): string - { - $toolInfo = ['tool-id' => 'symfony', 'tool-name' => 'Symfony']; - if (\array_key_exists('tool_info', $options)) { - $toolInfo = array_merge($toolInfo, $options['tool_info']); - } - - $dom = new \DOMDocument('1.0', 'utf-8'); - $dom->formatOutput = true; - - $xliff = $dom->appendChild($dom->createElement('xliff')); - $xliff->setAttribute('version', '1.2'); - $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2'); - - $xliffFile = $xliff->appendChild($dom->createElement('file')); - $xliffFile->setAttribute('source-language', str_replace('_', '-', $defaultLocale)); - $xliffFile->setAttribute('target-language', str_replace('_', '-', $messages->getLocale())); - $xliffFile->setAttribute('datatype', 'plaintext'); - $xliffFile->setAttribute('original', 'file.ext'); - - $xliffHead = $xliffFile->appendChild($dom->createElement('header')); - $xliffTool = $xliffHead->appendChild($dom->createElement('tool')); - foreach ($toolInfo as $id => $value) { - $xliffTool->setAttribute($id, $value); - } - - if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) { - $xliffPropGroup = $xliffHead->appendChild($dom->createElement('prop-group')); - foreach ($catalogueMetadata as $key => $value) { - $xliffProp = $xliffPropGroup->appendChild($dom->createElement('prop')); - $xliffProp->setAttribute('prop-type', $key); - $xliffProp->appendChild($dom->createTextNode($value)); - } - } - - $xliffBody = $xliffFile->appendChild($dom->createElement('body')); - foreach ($messages->all($domain) as $source => $target) { - $translation = $dom->createElement('trans-unit'); - - $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); - $translation->setAttribute('resname', $source); - - $s = $translation->appendChild($dom->createElement('source')); - $s->appendChild($dom->createTextNode($source)); - - // Does the target contain characters requiring a CDATA section? - $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); - - $targetElement = $dom->createElement('target'); - $metadata = $messages->getMetadata($source, $domain); - if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { - foreach ($metadata['target-attributes'] as $name => $value) { - $targetElement->setAttribute($name, $value); - } - } - $t = $translation->appendChild($targetElement); - $t->appendChild($text); - - if ($this->hasMetadataArrayInfo('notes', $metadata)) { - foreach ($metadata['notes'] as $note) { - if (!isset($note['content'])) { - continue; - } - - $n = $translation->appendChild($dom->createElement('note')); - $n->appendChild($dom->createTextNode($note['content'])); - - if (isset($note['priority'])) { - $n->setAttribute('priority', $note['priority']); - } - - if (isset($note['from'])) { - $n->setAttribute('from', $note['from']); - } - } - } - - $xliffBody->appendChild($translation); - } - - return $dom->saveXML(); - } - - private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ?string $domain): string - { - $dom = new \DOMDocument('1.0', 'utf-8'); - $dom->formatOutput = true; - - $xliff = $dom->appendChild($dom->createElement('xliff')); - $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0'); - $xliff->setAttribute('version', '2.0'); - $xliff->setAttribute('srcLang', str_replace('_', '-', $defaultLocale)); - $xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale())); - - $xliffFile = $xliff->appendChild($dom->createElement('file')); - if (str_ends_with($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX)) { - $xliffFile->setAttribute('id', substr($domain, 0, -\strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX)).'.'.$messages->getLocale()); - } else { - $xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale()); - } - - if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) { - $xliff->setAttribute('xmlns:m', 'urn:oasis:names:tc:xliff:metadata:2.0'); - $xliffMetadata = $xliffFile->appendChild($dom->createElement('m:metadata')); - foreach ($catalogueMetadata as $key => $value) { - $xliffMeta = $xliffMetadata->appendChild($dom->createElement('prop')); - $xliffMeta->setAttribute('type', $key); - $xliffMeta->appendChild($dom->createTextNode($value)); - } - } - - foreach ($messages->all($domain) as $source => $target) { - $translation = $dom->createElement('unit'); - $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); - - if (\strlen($source) <= 80) { - $translation->setAttribute('name', $source); - } - - $metadata = $messages->getMetadata($source, $domain); - - // Add notes section - if ($this->hasMetadataArrayInfo('notes', $metadata)) { - $notesElement = $dom->createElement('notes'); - foreach ($metadata['notes'] as $note) { - $n = $dom->createElement('note'); - $n->appendChild($dom->createTextNode($note['content'] ?? '')); - unset($note['content']); - - foreach ($note as $name => $value) { - $n->setAttribute($name, $value); - } - $notesElement->appendChild($n); - } - $translation->appendChild($notesElement); - } - - $segment = $translation->appendChild($dom->createElement('segment')); - - if ($this->hasMetadataArrayInfo('segment-attributes', $metadata)) { - foreach ($metadata['segment-attributes'] as $name => $value) { - $segment->setAttribute($name, $value); - } - } - - $s = $segment->appendChild($dom->createElement('source')); - $s->appendChild($dom->createTextNode($source)); - - // Does the target contain characters requiring a CDATA section? - $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); - - $targetElement = $dom->createElement('target'); - if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { - foreach ($metadata['target-attributes'] as $name => $value) { - $targetElement->setAttribute($name, $value); - } - } - $t = $segment->appendChild($targetElement); - $t->appendChild($text); - - $xliffFile->appendChild($translation); - } - - return $dom->saveXML(); - } - - private function hasMetadataArrayInfo(string $key, ?array $metadata = null): bool - { - return is_iterable($metadata[$key] ?? null); - } -} \ No newline at end of file diff --git a/src/Translation/Fixes/SegmentAwareXliffFileLoader.php b/src/Translation/Fixes/SegmentAwareXliffFileLoader.php deleted file mode 100644 index 12455e87..00000000 --- a/src/Translation/Fixes/SegmentAwareXliffFileLoader.php +++ /dev/null @@ -1,262 +0,0 @@ -. - */ - -declare(strict_types=1); - - -namespace App\Translation\Fixes; - -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\Config\Util\Exception\InvalidXmlException; -use Symfony\Component\Config\Util\Exception\XmlParsingException; -use Symfony\Component\Config\Util\XmlUtils; -use Symfony\Component\DependencyInjection\Attribute\AsDecorator; -use Symfony\Component\Translation\Exception\InvalidResourceException; -use Symfony\Component\Translation\Exception\NotFoundResourceException; -use Symfony\Component\Translation\Exception\RuntimeException; -use Symfony\Component\Translation\Loader\LoaderInterface; -use Symfony\Component\Translation\MessageCatalogue; -use Symfony\Component\Translation\Util\XliffUtils; - -/** - * Backport of the XliffFile dumper from Symfony 7.2, which supports segment attributes and notes, this keeps the - * metadata when editing the translations from inside Symfony. - */ -#[AsDecorator("translation.loader.xliff")] -class SegmentAwareXliffFileLoader implements LoaderInterface -{ - public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue - { - if (!class_exists(XmlUtils::class)) { - throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.'); - } - - if (!$this->isXmlString($resource)) { - if (!stream_is_local($resource)) { - throw new InvalidResourceException(\sprintf('This is not a local file "%s".', $resource)); - } - - if (!file_exists($resource)) { - throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource)); - } - - if (!is_file($resource)) { - throw new InvalidResourceException(\sprintf('This is neither a file nor an XLIFF string "%s".', $resource)); - } - } - - try { - if ($this->isXmlString($resource)) { - $dom = XmlUtils::parse($resource); - } else { - $dom = XmlUtils::loadFile($resource); - } - } catch (\InvalidArgumentException|XmlParsingException|InvalidXmlException $e) { - throw new InvalidResourceException(\sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); - } - - if ($errors = XliffUtils::validateSchema($dom)) { - throw new InvalidResourceException(\sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); - } - - $catalogue = new MessageCatalogue($locale); - $this->extract($dom, $catalogue, $domain); - - if (is_file($resource) && class_exists(FileResource::class)) { - $catalogue->addResource(new FileResource($resource)); - } - - return $catalogue; - } - - private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void - { - $xliffVersion = XliffUtils::getVersionNumber($dom); - - if ('1.2' === $xliffVersion) { - $this->extractXliff1($dom, $catalogue, $domain); - } - - if ('2.0' === $xliffVersion) { - $this->extractXliff2($dom, $catalogue, $domain); - } - } - - /** - * Extract messages and metadata from DOMDocument into a MessageCatalogue. - */ - private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void - { - $xml = simplexml_import_dom($dom); - $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; - - $namespace = 'urn:oasis:names:tc:xliff:document:1.2'; - $xml->registerXPathNamespace('xliff', $namespace); - - foreach ($xml->xpath('//xliff:file') as $file) { - $fileAttributes = $file->attributes(); - - $file->registerXPathNamespace('xliff', $namespace); - - foreach ($file->xpath('.//xliff:prop') as $prop) { - $catalogue->setCatalogueMetadata($prop->attributes()['prop-type'], (string) $prop, $domain); - } - - foreach ($file->xpath('.//xliff:trans-unit') as $translation) { - $attributes = $translation->attributes(); - - if (!(isset($attributes['resname']) || isset($translation->source))) { - continue; - } - - $source = (string) (isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source); - - if (isset($translation->target) - && 'needs-translation' === (string) $translation->target->attributes()['state'] - && \in_array((string) $translation->target, [$source, (string) $translation->source], true) - ) { - continue; - } - - // If the xlf file has another encoding specified, try to convert it because - // simple_xml will always return utf-8 encoded values - $target = $this->utf8ToCharset((string) ($translation->target ?? $translation->source), $encoding); - - $catalogue->set($source, $target, $domain); - - $metadata = [ - 'source' => (string) $translation->source, - 'file' => [ - 'original' => (string) $fileAttributes['original'], - ], - ]; - if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) { - $metadata['notes'] = $notes; - } - - if (isset($translation->target) && $translation->target->attributes()) { - $metadata['target-attributes'] = []; - foreach ($translation->target->attributes() as $key => $value) { - $metadata['target-attributes'][$key] = (string) $value; - } - } - - if (isset($attributes['id'])) { - $metadata['id'] = (string) $attributes['id']; - } - - $catalogue->setMetadata($source, $metadata, $domain); - } - } - } - - private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void - { - $xml = simplexml_import_dom($dom); - $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; - - $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0'); - - foreach ($xml->xpath('//xliff:unit') as $unit) { - foreach ($unit->segment as $segment) { - $attributes = $unit->attributes(); - $source = $attributes['name'] ?? $segment->source; - - // If the xlf file has another encoding specified, try to convert it because - // simple_xml will always return utf-8 encoded values - $target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding); - - $catalogue->set((string) $source, $target, $domain); - - $metadata = []; - if ($segment->attributes()) { - $metadata['segment-attributes'] = []; - foreach ($segment->attributes() as $key => $value) { - $metadata['segment-attributes'][$key] = (string) $value; - } - } - - if (isset($segment->target) && $segment->target->attributes()) { - $metadata['target-attributes'] = []; - foreach ($segment->target->attributes() as $key => $value) { - $metadata['target-attributes'][$key] = (string) $value; - } - } - - if (isset($unit->notes)) { - $metadata['notes'] = []; - foreach ($unit->notes->note as $noteNode) { - $note = []; - foreach ($noteNode->attributes() as $key => $value) { - $note[$key] = (string) $value; - } - $note['content'] = (string) $noteNode; - $metadata['notes'][] = $note; - } - } - - $catalogue->setMetadata((string) $source, $metadata, $domain); - } - } - } - - /** - * Convert a UTF8 string to the specified encoding. - */ - private function utf8ToCharset(string $content, ?string $encoding = null): string - { - if ('UTF-8' !== $encoding && $encoding) { - return mb_convert_encoding($content, $encoding, 'UTF-8'); - } - - return $content; - } - - private function parseNotesMetadata(?\SimpleXMLElement $noteElement = null, ?string $encoding = null): array - { - $notes = []; - - if (null === $noteElement) { - return $notes; - } - - /** @var \SimpleXMLElement $xmlNote */ - foreach ($noteElement as $xmlNote) { - $noteAttributes = $xmlNote->attributes(); - $note = ['content' => $this->utf8ToCharset((string) $xmlNote, $encoding)]; - if (isset($noteAttributes['priority'])) { - $note['priority'] = (int) $noteAttributes['priority']; - } - - if (isset($noteAttributes['from'])) { - $note['from'] = (string) $noteAttributes['from']; - } - - $notes[] = $note; - } - - return $notes; - } - - private function isXmlString(string $resource): bool - { - return str_starts_with($resource, '. + */ + +declare(strict_types=1); + + +namespace App\Translation; + +use DOMDocument; +use DOMXPath; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\Translation\Dumper\FileDumper; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * The goal of this class, is to ensure that the XLIFF dumper does not output CDATA, but instead outputs the text + * using the normal XML escaping. Crowdin outputs the translations without CDATA, we want to be consistent with that, to + * prevent unnecessary diffs in the translation files when we update them with translations from Crowdin. + */ +#[AsDecorator("translation.dumper.xliff")] +class NoCDATAXliffFileDumper extends FileDumper +{ + + public function __construct(private readonly FileDumper $decorated) + { + + } + + private function convertCDataToEscapedText(string $xmlContent): string + { + $dom = new DOMDocument(); + // Preserve whitespace to keep Symfony's formatting intact + $dom->preserveWhiteSpace = true; + $dom->formatOutput = true; + + // Load the XML (handle internal errors if necessary) + $dom->loadXML($xmlContent); + + $xpath = new DOMXPath($dom); + // Find all CDATA sections + $cdataNodes = $xpath->query('//node()/comment()|//node()/text()|//node()') ; + + // We specifically want CDATA sections. XPath 1.0 doesn't have a direct + // "cdata-section()" selector easily, so we iterate through all nodes + // and check their type. + + $nodesToRemove = []; + foreach ($xpath->query('//text() | //*') as $node) { + foreach ($node->childNodes as $child) { + if ($child->nodeType === XML_CDATA_SECTION_NODE) { + // Create a new text node with the content of the CDATA + // DOMDocument will automatically escape special chars on save + $newTextNode = $dom->createTextNode($child->textContent); + $node->replaceChild($newTextNode, $child); + } + } + } + + return $dom->saveXML(); + } + + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + return $this->convertCDataToEscapedText($this->decorated->formatCatalogue($messages, $domain, $options)); + } + + protected function getExtension(): string + { + return $this->decorated->getExtension(); + } +} diff --git a/src/Twig/AttachmentExtension.php b/src/Twig/AttachmentExtension.php index 9f81abe6..23ab7d6e 100644 --- a/src/Twig/AttachmentExtension.php +++ b/src/Twig/AttachmentExtension.php @@ -23,24 +23,59 @@ declare(strict_types=1); namespace App\Twig; use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentContainingDBElement; +use App\Entity\Parts\Part; use App\Services\Attachments\AttachmentURLGenerator; +use App\Services\Attachments\PartPreviewGenerator; use App\Services\Misc\FAIconGenerator; +use Twig\Attribute\AsTwigFunction; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -final class AttachmentExtension extends AbstractExtension +final readonly class AttachmentExtension { - public function __construct(protected AttachmentURLGenerator $attachmentURLGenerator, protected FAIconGenerator $FAIconGenerator) + public function __construct(private AttachmentURLGenerator $attachmentURLGenerator, private FAIconGenerator $FAIconGenerator, private PartPreviewGenerator $partPreviewGenerator) { } - public function getFunctions(): array + /** + * Returns the URL of the thumbnail of the given attachment. Returns null if no thumbnail is available. + */ + #[AsTwigFunction("attachment_thumbnail")] + public function attachmentThumbnail(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string { - return [ - /* Returns the URL to a thumbnail of the given attachment */ - new TwigFunction('attachment_thumbnail', fn(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string => $this->attachmentURLGenerator->getThumbnailURL($attachment, $filter_name)), - /* Returns the font awesome icon class which is representing the given file extension (We allow null here for attachments without extension) */ - new TwigFunction('ext_to_fa_icon', fn(?string $extension): string => $this->FAIconGenerator->fileExtensionToFAType($extension ?? '')), - ]; + return $this->attachmentURLGenerator->getThumbnailURL($attachment, $filter_name); + } + + /** + * Returns the URL of the thumbnail of the given element. Returns null if no thumbnail is available. + * For parts, a special preview image is generated, for other entities, the master picture is used as preview (if available). + */ + #[AsTwigFunction("entity_thumbnail")] + public function entityThumbnail(AttachmentContainingDBElement $element, string $filter_name = 'thumbnail_sm'): ?string + { + if ($element instanceof Part) { + $preview_attachment = $this->partPreviewGenerator->getTablePreviewAttachment($element); + } else { // For other entities, we just use the master picture as preview, if available + $preview_attachment = $element->getMasterPictureAttachment(); + } + + if ($preview_attachment === null) { + return null; + } + + return $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, $filter_name); + } + + /** + * Return the font-awesome icon type for the given file extension. Returns "file" if no specific icon is available. + * Null is allowed for files withot extension + * @param string|null $extension + * @return string + */ + #[AsTwigFunction("ext_to_fa_icon")] + public function extToFAIcon(?string $extension): string + { + return $this->FAIconGenerator->fileExtensionToFAType($extension ?? ''); } } diff --git a/src/Twig/BarcodeExtension.php b/src/Twig/BarcodeExtension.php index ae1973e3..25c0d78e 100644 --- a/src/Twig/BarcodeExtension.php +++ b/src/Twig/BarcodeExtension.php @@ -23,19 +23,14 @@ declare(strict_types=1); namespace App\Twig; use Com\Tecnick\Barcode\Barcode; -use Twig\Extension\AbstractExtension; -use Twig\TwigFunction; +use Twig\Attribute\AsTwigFunction; -final class BarcodeExtension extends AbstractExtension +final class BarcodeExtension { - public function getFunctions(): array - { - return [ - /* Generates a barcode with the given Type and Data and returns it as an SVG represenation */ - new TwigFunction('barcode_svg', fn(string $content, string $type = 'QRCODE'): string => $this->barcodeSVG($content, $type)), - ]; - } - + /** + * Generates a barcode in SVG format for the given content and type. + */ + #[AsTwigFunction('barcode_svg')] public function barcodeSVG(string $content, string $type = 'QRCODE'): string { $barcodeFactory = new Barcode(); diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index 762ebb09..bff21eb8 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -24,6 +24,7 @@ namespace App\Twig; use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\PartCustomState; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -40,6 +41,9 @@ use App\Exceptions\EntityNotSupportedException; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use App\Services\Trees\TreeViewGenerator; +use Twig\Attribute\AsTwigFunction; +use Twig\Attribute\AsTwigTest; +use Twig\DeprecatedCallableInfo; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; use Twig\TwigTest; @@ -47,59 +51,27 @@ use Twig\TwigTest; /** * @see \App\Tests\Twig\EntityExtensionTest */ -final class EntityExtension extends AbstractExtension +final readonly class EntityExtension { - public function __construct(protected EntityURLGenerator $entityURLGenerator, protected TreeViewGenerator $treeBuilder, private readonly ElementTypeNameGenerator $nameGenerator) + public function __construct(private EntityURLGenerator $entityURLGenerator, private TreeViewGenerator $treeBuilder, private ElementTypeNameGenerator $nameGenerator) { } - public function getTests(): array + /** + * Checks if the given variable is an entity (instance of AbstractDBElement). + */ + #[AsTwigTest("entity")] + public function entityTest(mixed $var): bool { - return [ - /* Checks if the given variable is an entitity (instance of AbstractDBElement) */ - new TwigTest('entity', static fn($var) => $var instanceof AbstractDBElement), - ]; + return $var instanceof AbstractDBElement; } - public function getFunctions(): array - { - return [ - /* Returns a string representation of the given entity */ - new TwigFunction('entity_type', fn(object $entity): ?string => $this->getEntityType($entity)), - /* Returns the URL to the given entity */ - new TwigFunction('entity_url', fn(AbstractDBElement $entity, string $method = 'info'): string => $this->generateEntityURL($entity, $method)), - /* Returns the URL to the given entity in timetravel mode */ - new TwigFunction('timetravel_url', fn(AbstractDBElement $element, \DateTimeInterface $dateTime): ?string => $this->timeTravelURL($element, $dateTime)), - /* Generates a JSON array of the given tree */ - new TwigFunction('tree_data', fn(AbstractDBElement $element, string $type = 'newEdit'): string => $this->treeData($element, $type)), - /* Gets a human readable label for the type of the given entity */ - new TwigFunction('entity_type_label', fn(object|string $entity): string => $this->nameGenerator->getLocalizedTypeLabel($entity)), - ]; - } - - public function timeTravelURL(AbstractDBElement $element, \DateTimeInterface $dateTime): ?string - { - try { - return $this->entityURLGenerator->timeTravelURL($element, $dateTime); - } catch (EntityNotSupportedException) { - return null; - } - } - - public function treeData(AbstractDBElement $element, string $type = 'newEdit'): string - { - $tree = $this->treeBuilder->getTreeView($element::class, null, $type, $element); - - return json_encode($tree, JSON_THROW_ON_ERROR); - } - - public function generateEntityURL(AbstractDBElement $entity, string $method = 'info'): string - { - return $this->entityURLGenerator->getURL($entity, $method); - } - - public function getEntityType(object $entity): ?string + /** + * Returns a string representation of the given entity + */ + #[AsTwigFunction("entity_type")] + public function entityType(object $entity): ?string { $map = [ Part::class => 'part', @@ -115,6 +87,7 @@ final class EntityExtension extends AbstractExtension Currency::class => 'currency', MeasurementUnit::class => 'measurement_unit', LabelProfile::class => 'label_profile', + PartCustomState::class => 'part_custom_state', ]; foreach ($map as $class => $type) { @@ -125,4 +98,69 @@ final class EntityExtension extends AbstractExtension return null; } + + /** + * Returns the URL for the given entity and method. E.g. for a Part and method "edit", it will return the URL to edit this part. + */ + #[AsTwigFunction("entity_url")] + public function entityURL(AbstractDBElement $entity, string $method = 'info'): string + { + return $this->entityURLGenerator->getURL($entity, $method); + } + + + /** + * Returns the URL for the given entity in timetravel mode. + */ + #[AsTwigFunction("timetravel_url")] + public function timeTravelURL(AbstractDBElement $element, \DateTimeInterface $dateTime): ?string + { + try { + return $this->entityURLGenerator->timeTravelURL($element, $dateTime); + } catch (EntityNotSupportedException) { + return null; + } + } + + /** + * Generates a tree data structure for the given element, which can be used to display a tree view of the element and its related entities. + * The type parameter can be used to specify the type of tree view (e.g. "newEdit" for the tree view in the new/edit pages). The returned data is a JSON string. + */ + #[AsTwigFunction("tree_data")] + public function treeData(AbstractDBElement $element, string $type = 'newEdit'): string + { + $tree = $this->treeBuilder->getTreeView($element::class, null, $type, $element); + + return json_encode($tree, JSON_THROW_ON_ERROR); + } + + /** + * Gets the localized type label for the given entity. E.g. for a Part, it will return "Part" in English and "Bauteil" in German. + * @deprecated Use the "type_label" function instead, which does the same but is more concise. + */ + #[AsTwigFunction("entity_type_label", deprecationInfo: new DeprecatedCallableInfo("Part-DB", "2", "Use the 'type_label' function instead."))] + public function entityTypeLabel(object|string $entity): string + { + return $this->nameGenerator->getLocalizedTypeLabel($entity); + } + + /** + * Gets the localized type label for the given entity. E.g. for a Part, it will return "Part" in English and "Bauteil" in German. + */ + #[AsTwigFunction("type_label")] + public function typeLabel(object|string $entity): string + { + return $this->nameGenerator->typeLabel($entity); + } + + /** + * Gets the localized plural type label for the given entity. E.g. for a Part, it will return "Parts" in English and "Bauteile" in German. + * @param object|string $entity + * @return string + */ + #[AsTwigFunction("type_label_p")] + public function typeLabelPlural(object|string $entity): string + { + return $this->nameGenerator->typeLabelPlural($entity); + } } diff --git a/src/Twig/FormatExtension.php b/src/Twig/FormatExtension.php index 46313aaf..b91b7e11 100644 --- a/src/Twig/FormatExtension.php +++ b/src/Twig/FormatExtension.php @@ -29,35 +29,28 @@ use App\Services\Formatters\MarkdownParser; use App\Services\Formatters\MoneyFormatter; use App\Services\Formatters\SIFormatter; use Brick\Math\BigDecimal; -use Twig\Extension\AbstractExtension; -use Twig\TwigFilter; +use Twig\Attribute\AsTwigFilter; -final class FormatExtension extends AbstractExtension +final readonly class FormatExtension { - public function __construct(protected MarkdownParser $markdownParser, protected MoneyFormatter $moneyFormatter, protected SIFormatter $siformatter, protected AmountFormatter $amountFormatter) + public function __construct(private MarkdownParser $markdownParser, private MoneyFormatter $moneyFormatter, private SIFormatter $siformatter, private AmountFormatter $amountFormatter) { } - public function getFilters(): array + /** + * Mark the given text as markdown, which will be rendered in the browser + */ + #[AsTwigFilter("format_markdown", isSafe: ['html'], preEscape: 'html')] + public function formatMarkdown(string $markdown, bool $inline_mode = false): string { - return [ - /* Mark the given text as markdown, which will be rendered in the browser */ - new TwigFilter('format_markdown', fn(string $markdown, bool $inline_mode = false): string => $this->markdownParser->markForRendering($markdown, $inline_mode), [ - 'pre_escape' => 'html', - 'is_safe' => ['html'], - ]), - /* Format the given amount as money, using a given currency */ - new TwigFilter('format_money', fn($amount, ?Currency $currency = null, int $decimals = 5): string => $this->formatCurrency($amount, $currency, $decimals)), - /* Format the given number using SI prefixes and the given unit (string) */ - new TwigFilter('format_si', fn($value, $unit, $decimals = 2, bool $show_all_digits = false): string => $this->siFormat($value, $unit, $decimals, $show_all_digits)), - /** Format the given amount using the given MeasurementUnit */ - new TwigFilter('format_amount', fn($value, ?MeasurementUnit $unit, array $options = []): string => $this->amountFormat($value, $unit, $options)), - /** Format the given number of bytes as human-readable number */ - new TwigFilter('format_bytes', fn(int $bytes, int $precision = 2): string => $this->formatBytes($bytes, $precision)), - ]; + return $this->markdownParser->markForRendering($markdown, $inline_mode); } - public function formatCurrency($amount, ?Currency $currency = null, int $decimals = 5): string + /** + * Format the given amount as money, using a given currency + */ + #[AsTwigFilter("format_money")] + public function formatMoney(BigDecimal|float|string $amount, ?Currency $currency = null, int $decimals = 5): string { if ($amount instanceof BigDecimal) { $amount = (string) $amount; @@ -66,19 +59,22 @@ final class FormatExtension extends AbstractExtension return $this->moneyFormatter->format($amount, $currency, $decimals); } - public function siFormat($value, $unit, $decimals = 2, bool $show_all_digits = false): string + /** + * Format the given number using SI prefixes and the given unit (string) + */ + #[AsTwigFilter("format_si")] + public function siFormat(float $value, string $unit, int $decimals = 2, bool $show_all_digits = false): string { return $this->siformatter->format($value, $unit, $decimals); } - public function amountFormat($value, ?MeasurementUnit $unit, array $options = []): string + #[AsTwigFilter("format_amount")] + public function amountFormat(float|int|string $value, ?MeasurementUnit $unit, array $options = []): string { return $this->amountFormatter->format($value, $unit, $options); } - /** - * @param $bytes - */ + #[AsTwigFilter("format_bytes")] public function formatBytes(int $bytes, int $precision = 2): string { $size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB']; diff --git a/src/Twig/InfoProviderExtension.php b/src/Twig/InfoProviderExtension.php index a963b778..54dbf93a 100644 --- a/src/Twig/InfoProviderExtension.php +++ b/src/Twig/InfoProviderExtension.php @@ -23,31 +23,25 @@ declare(strict_types=1); namespace App\Twig; +use Twig\Attribute\AsTwigFunction; use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -class InfoProviderExtension extends AbstractExtension +final readonly class InfoProviderExtension { public function __construct( - private readonly ProviderRegistry $providerRegistry + private ProviderRegistry $providerRegistry ) {} - public function getFunctions(): array - { - return [ - new TwigFunction('info_provider', $this->getInfoProvider(...)), - new TwigFunction('info_provider_label', $this->getInfoProviderName(...)) - ]; - } - /** * Gets the info provider with the given key. Returns null, if the provider does not exist. * @param string $key * @return InfoProviderInterface|null */ - private function getInfoProvider(string $key): ?InfoProviderInterface + #[AsTwigFunction(name: 'info_provider')] + public function getInfoProvider(string $key): ?InfoProviderInterface { try { return $this->providerRegistry->getProviderByKey($key); @@ -61,7 +55,8 @@ class InfoProviderExtension extends AbstractExtension * @param string $key * @return string|null */ - private function getInfoProviderName(string $key): ?string + #[AsTwigFunction(name: 'info_provider_label')] + public function getInfoProviderName(string $key): ?string { try { return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()['name']; @@ -69,4 +64,4 @@ class InfoProviderExtension extends AbstractExtension return null; } } -} \ No newline at end of file +} diff --git a/src/Twig/LogExtension.php b/src/Twig/LogExtension.php index 34dad988..738a24c2 100644 --- a/src/Twig/LogExtension.php +++ b/src/Twig/LogExtension.php @@ -25,21 +25,26 @@ namespace App\Twig; use App\Entity\LogSystem\AbstractLogEntry; use App\Services\LogSystem\LogDataFormatter; use App\Services\LogSystem\LogDiffFormatter; +use Twig\Attribute\AsTwigFunction; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -final class LogExtension extends AbstractExtension +final readonly class LogExtension { - public function __construct(private readonly LogDataFormatter $logDataFormatter, private readonly LogDiffFormatter $logDiffFormatter) + public function __construct(private LogDataFormatter $logDataFormatter, private LogDiffFormatter $logDiffFormatter) { } - public function getFunctions(): array + #[AsTwigFunction(name: 'format_log_data', isSafe: ['html'])] + public function formatLogData(mixed $data, AbstractLogEntry $logEntry, string $fieldName): string { - return [ - new TwigFunction('format_log_data', fn($data, AbstractLogEntry $logEntry, string $fieldName): string => $this->logDataFormatter->formatData($data, $logEntry, $fieldName), ['is_safe' => ['html']]), - new TwigFunction('format_log_diff', fn($old_data, $new_data): string => $this->logDiffFormatter->formatDiff($old_data, $new_data), ['is_safe' => ['html']]), - ]; + return $this->logDataFormatter->formatData($data, $logEntry, $fieldName); + } + + #[AsTwigFunction(name: 'format_log_diff', isSafe: ['html'])] + public function formatLogDiff(mixed $old_data, mixed $new_data): string + { + return $this->logDiffFormatter->formatDiff($old_data, $new_data); } } diff --git a/src/Twig/MiscExtension.php b/src/Twig/MiscExtension.php index 8b6ebc68..565d56f2 100644 --- a/src/Twig/MiscExtension.php +++ b/src/Twig/MiscExtension.php @@ -22,6 +22,8 @@ declare(strict_types=1); */ namespace App\Twig; +use App\Services\InfoProviderSystem\CreateFromUrlHelper; +use Twig\Attribute\AsTwigFunction; use App\Settings\SettingsIcon; use Symfony\Component\HttpFoundation\Request; use App\Services\LogSystem\EventCommentType; @@ -31,23 +33,14 @@ use Twig\TwigFunction; use App\Services\LogSystem\EventCommentNeededHelper; use Twig\Extension\AbstractExtension; -final class MiscExtension extends AbstractExtension +final readonly class MiscExtension { - public function __construct(private readonly EventCommentNeededHelper $eventCommentNeededHelper) + public function __construct(private EventCommentNeededHelper $eventCommentNeededHelper, private CreateFromUrlHelper $fromUrlHelper) { } - public function getFunctions(): array - { - return [ - new TwigFunction('event_comment_needed', $this->evenCommentNeeded(...)), - - new TwigFunction('settings_icon', $this->settingsIcon(...)), - new TwigFunction('uri_without_host', $this->uri_without_host(...)) - ]; - } - - private function evenCommentNeeded(string|EventCommentType $operation_type): bool + #[AsTwigFunction(name: 'event_comment_needed')] + public function evenCommentNeeded(string|EventCommentType $operation_type): bool { if (is_string($operation_type)) { $operation_type = EventCommentType::from($operation_type); @@ -63,7 +56,8 @@ final class MiscExtension extends AbstractExtension * @return string|null * @throws \ReflectionException */ - private function settingsIcon(string|object $objectOrClass): ?string + #[AsTwigFunction(name: 'settings_icon')] + public function settingsIcon(string|object $objectOrClass): ?string { //If the given object is a proxy, then get the real object if (is_a($objectOrClass, SettingsProxyInterface::class)) { @@ -82,6 +76,7 @@ final class MiscExtension extends AbstractExtension * @param Request $request * @return string */ + #[AsTwigFunction(name: 'uri_without_host')] public function uri_without_host(Request $request): string { if (null !== $qs = $request->getQueryString()) { @@ -90,4 +85,14 @@ final class MiscExtension extends AbstractExtension return $request->getBaseUrl().$request->getPathInfo().$qs; } + + /** + * Returns true if the from url provider is active, false otherwise. + * @return bool + */ + #[AsTwigFunction(name: 'create_from_url_active')] + public function create_from_url_active(): bool + { + return $this->fromUrlHelper->canCreateFromUrl(); + } } diff --git a/src/Twig/Sandbox/SandboxedLabelExtension.php b/src/Twig/Sandbox/SandboxedLabelExtension.php index 59fb0af0..6fe85e80 100644 --- a/src/Twig/Sandbox/SandboxedLabelExtension.php +++ b/src/Twig/Sandbox/SandboxedLabelExtension.php @@ -23,14 +23,18 @@ declare(strict_types=1); namespace App\Twig\Sandbox; +use App\Entity\Base\AbstractPartsContainingDBElement; +use App\Entity\Parts\Part; +use App\Repository\AbstractPartsContainingRepository; use App\Services\LabelSystem\LabelTextReplacer; +use Doctrine\ORM\EntityManagerInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; class SandboxedLabelExtension extends AbstractExtension { - public function __construct(private readonly LabelTextReplacer $labelTextReplacer) + public function __construct(private readonly LabelTextReplacer $labelTextReplacer, private readonly EntityManagerInterface $em) { } @@ -39,6 +43,11 @@ class SandboxedLabelExtension extends AbstractExtension { return [ new TwigFunction('placeholder', fn(string $text, object $label_target) => $this->labelTextReplacer->handlePlaceholderOrReturnNull($text, $label_target)), + + new TwigFunction("associated_parts", $this->associatedParts(...)), + new TwigFunction("associated_parts_count", $this->associatedPartsCount(...)), + new TwigFunction("associated_parts_r", $this->associatedPartsRecursive(...)), + new TwigFunction("associated_parts_count_r", $this->associatedPartsCountRecursive(...)), ]; } @@ -48,4 +57,37 @@ class SandboxedLabelExtension extends AbstractExtension new TwigFilter('placeholders', fn(string $text, object $label_target) => $this->labelTextReplacer->replace($text, $label_target)), ]; } -} \ No newline at end of file + + /** + * Returns all parts associated with the given element. + * @param AbstractPartsContainingDBElement $element + * @return Part[] + */ + public function associatedParts(AbstractPartsContainingDBElement $element): array + { + /** @var AbstractPartsContainingRepository $repo */ + $repo = $this->em->getRepository($element::class); + return $repo->getParts($element); + } + + public function associatedPartsCount(AbstractPartsContainingDBElement $element): int + { + /** @var AbstractPartsContainingRepository $repo */ + $repo = $this->em->getRepository($element::class); + return $repo->getPartsCount($element); + } + + public function associatedPartsRecursive(AbstractPartsContainingDBElement $element): array + { + /** @var AbstractPartsContainingRepository $repo */ + $repo = $this->em->getRepository($element::class); + return $repo->getPartsRecursive($element); + } + + public function associatedPartsCountRecursive(AbstractPartsContainingDBElement $element): int + { + /** @var AbstractPartsContainingRepository $repo */ + $repo = $this->em->getRepository($element::class); + return $repo->getPartsCountRecursive($element); + } +} diff --git a/src/Twig/TwigCoreExtension.php b/src/Twig/TwigCoreExtension.php index 7b2b58f8..ceb4ce82 100644 --- a/src/Twig/TwigCoreExtension.php +++ b/src/Twig/TwigCoreExtension.php @@ -22,7 +22,11 @@ declare(strict_types=1); */ namespace App\Twig; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Twig\Attribute\AsTwigFilter; +use Twig\Attribute\AsTwigFunction; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Twig\Attribute\AsTwigTest; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -32,58 +36,54 @@ use Twig\TwigTest; * The functionalities here extend the Twig with some core functions, which are independently of Part-DB. * @see \App\Tests\Twig\TwigCoreExtensionTest */ -final class TwigCoreExtension extends AbstractExtension +final readonly class TwigCoreExtension { - private readonly ObjectNormalizer $objectNormalizer; + private NormalizerInterface $objectNormalizer; public function __construct() { $this->objectNormalizer = new ObjectNormalizer(); } - public function getFunctions(): array + /** + * Checks if the given variable is an instance of the given class/interface/enum. E.g. `x is instanceof('App\Entity\Parts\Part')` + * @param mixed $var + * @param string $instance + * @return bool + */ + #[AsTwigTest("instanceof")] + public function testInstanceOf(mixed $var, string $instance): bool { - return [ - /* Returns the enum cases as values */ - new TwigFunction('enum_cases', $this->getEnumCases(...)), - ]; - } + if (!class_exists($instance) && !interface_exists($instance) && !enum_exists($instance)) { + throw new \InvalidArgumentException(sprintf('The given class/interface/enum "%s" does not exist!', $instance)); + } - public function getTests(): array - { - return [ - /* - * Checks if a given variable is an instance of a given class. E.g. ` x is instanceof('App\Entity\Parts\Part')` - */ - new TwigTest('instanceof', static fn($var, $instance) => $var instanceof $instance), - /* Checks if a given variable is an object. E.g. `x is object` */ - new TwigTest('object', static fn($var): bool => is_object($var)), - new TwigTest('enum', fn($var) => $var instanceof \UnitEnum), - ]; + return $var instanceof $instance; } /** - * @param string $enum_class - * @phpstan-param class-string $enum_class + * Checks if the given variable is an object. This can be used to check if a variable is an object, without knowing the exact class of the object. E.g. `x is object` + * @param mixed $var + * @return bool */ - public function getEnumCases(string $enum_class): array + #[AsTwigTest("object")] + public function testObject(mixed $var): bool { - if (!enum_exists($enum_class)) { - throw new \InvalidArgumentException(sprintf('The given class "%s" is not an enum!', $enum_class)); - } - - /** @noinspection PhpUndefinedMethodInspection */ - return ($enum_class)::cases(); + return is_object($var); } - public function getFilters(): array + /** + * Checks if the given variable is an enum (instance of UnitEnum). This can be used to check if a variable is an enum, without knowing the exact class of the enum. E.g. `x is enum` + * @param mixed $var + * @return bool + */ + #[AsTwigTest("enum")] + public function testEnum(mixed $var): bool { - return [ - /* Converts the given object to an array representation of the public/accessible properties */ - new TwigFilter('to_array', fn($object) => $this->toArray($object)), - ]; + return $var instanceof \UnitEnum; } + #[AsTwigFilter('to_array')] public function toArray(object|array $object): array { //If it is already an array, we can just return it diff --git a/src/Twig/UpdateExtension.php b/src/Twig/UpdateExtension.php new file mode 100644 index 00000000..7ec7897b --- /dev/null +++ b/src/Twig/UpdateExtension.php @@ -0,0 +1,74 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Twig; + +use Twig\Attribute\AsTwigFunction; +use App\Services\System\UpdateAvailableFacade; +use Symfony\Bundle\SecurityBundle\Security; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +/** + * Twig extension for update-related functions. + */ +final readonly class UpdateExtension +{ + public function __construct(private UpdateAvailableFacade $updateAvailableManager, + private Security $security) + { + + } + + /** + * Check if an update is available and the user has permission to see it. + */ + #[AsTwigFunction(name: 'is_update_available')] + public function isUpdateAvailable(): bool + { + // Only show to users with the show_updates permission + if (!$this->security->isGranted('@system.show_updates')) { + return false; + } + + return $this->updateAvailableManager->isUpdateAvailable(); + } + + /** + * Get the latest available version string. + */ + #[AsTwigFunction(name: 'get_latest_version')] + public function getLatestVersion(): string + { + return $this->updateAvailableManager->getLatestVersionString(); + } + + /** + * Get the URL to the latest version release page. + */ + #[AsTwigFunction(name: 'get_latest_version_url')] + public function getLatestVersionUrl(): string + { + return $this->updateAvailableManager->getLatestVersionUrl(); + } +} diff --git a/src/Twig/UserExtension.php b/src/Twig/UserExtension.php index 5045257a..81ff0857 100644 --- a/src/Twig/UserExtension.php +++ b/src/Twig/UserExtension.php @@ -41,51 +41,24 @@ declare(strict_types=1); namespace App\Twig; -use App\Entity\Base\AbstractDBElement; +use Twig\Attribute\AsTwigFilter; +use Twig\Attribute\AsTwigFunction; use App\Entity\UserSystem\User; -use App\Entity\LogSystem\AbstractLogEntry; -use App\Repository\LogEntryRepository; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Twig\Extension\AbstractExtension; -use Twig\TwigFilter; -use Twig\TwigFunction; /** * @see \App\Tests\Twig\UserExtensionTest */ -final class UserExtension extends AbstractExtension +final readonly class UserExtension { - private readonly LogEntryRepository $repo; - public function __construct(EntityManagerInterface $em, - private readonly Security $security, - private readonly UrlGeneratorInterface $urlGenerator) + public function __construct( + private Security $security, + private UrlGeneratorInterface $urlGenerator) { - $this->repo = $em->getRepository(AbstractLogEntry::class); - } - - public function getFilters(): array - { - return [ - new TwigFilter('remove_locale_from_path', fn(string $path): string => $this->removeLocaleFromPath($path)), - ]; - } - - public function getFunctions(): array - { - return [ - /* Returns the user which has edited the given entity the last time. */ - new TwigFunction('last_editing_user', fn(AbstractDBElement $element): ?User => $this->repo->getLastEditingUser($element)), - /* Returns the user which has created the given entity. */ - new TwigFunction('creating_user', fn(AbstractDBElement $element): ?User => $this->repo->getCreatingUser($element)), - new TwigFunction('impersonator_user', $this->getImpersonatorUser(...)), - new TwigFunction('impersonation_active', $this->isImpersonationActive(...)), - new TwigFunction('impersonation_path', $this->getImpersonationPath(...)), - ]; } /** @@ -93,6 +66,7 @@ final class UserExtension extends AbstractExtension * If the current user is not impersonated, null is returned. * @return User|null */ + #[AsTwigFunction(name: 'impersonator_user')] public function getImpersonatorUser(): ?User { $token = $this->security->getToken(); @@ -107,11 +81,13 @@ final class UserExtension extends AbstractExtension return null; } + #[AsTwigFunction(name: 'impersonation_active')] public function isImpersonationActive(): bool { return $this->security->isGranted('IS_IMPERSONATOR'); } + #[AsTwigFunction(name: 'impersonation_path')] public function getImpersonationPath(User $user, string $route_name = 'homepage'): string { if (! $this->security->isGranted('CAN_SWITCH_USER', $user)) { @@ -124,6 +100,7 @@ final class UserExtension extends AbstractExtension /** * This function/filter generates a path. */ + #[AsTwigFilter(name: 'remove_locale_from_path')] public function removeLocaleFromPath(string $path): string { //Ensure the path has the correct format diff --git a/src/Twig/UserRepoExtension.php b/src/Twig/UserRepoExtension.php new file mode 100644 index 00000000..1dcc6706 --- /dev/null +++ b/src/Twig/UserRepoExtension.php @@ -0,0 +1,57 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Twig; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\UserSystem\User; +use App\Repository\LogEntryRepository; +use Doctrine\ORM\EntityManagerInterface; +use Twig\Attribute\AsTwigFunction; + +final readonly class UserRepoExtension +{ + + public function __construct(private EntityManagerInterface $entityManager) + { + } + + /** + * Returns the user which has edited the given entity the last time. + */ + #[AsTwigFunction('creating_user')] + public function creatingUser(AbstractDBElement $element): ?User + { + return $this->entityManager->getRepository(AbstractLogEntry::class)->getCreatingUser($element); + } + + /** + * Returns the user which has edited the given entity the last time. + */ + #[AsTwigFunction('last_editing_user')] + public function lastEditingUser(AbstractDBElement $element): ?User + { + return $this->entityManager->getRepository(AbstractLogEntry::class)->getLastEditingUser($element); + } +} diff --git a/src/Validator/Constraints/UniquePartIpnConstraint.php b/src/Validator/Constraints/UniquePartIpnConstraint.php new file mode 100644 index 00000000..652f2bcd --- /dev/null +++ b/src/Validator/Constraints/UniquePartIpnConstraint.php @@ -0,0 +1,24 @@ +entityManager = $entityManager; + $this->ipnSuggestSettings = $ipnSuggestSettings; + } + + public function validate($value, Constraint $constraint): void + { + if (null === $value || '' === $value) { + return; + } + + //If the autoAppendSuffix option is enabled, the IPN becomes unique automatically later + if ($this->ipnSuggestSettings->autoAppendSuffix) { + return; + } + + if (!$constraint instanceof UniquePartIpnConstraint) { + return; + } + + /** @var Part $currentPart */ + $currentPart = $this->context->getObject(); + + if (!$currentPart instanceof Part) { + return; + } + + $repository = $this->entityManager->getRepository(Part::class); + $existingParts = $repository->findBy(['ipn' => $value]); + + foreach ($existingParts as $existingPart) { + if ($currentPart->getId() !== $existingPart->getId()) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } + } + } +} diff --git a/assets/controllers/turbo/global_reload_controller.js b/src/Validator/Constraints/ValidGTIN.php similarity index 71% rename from assets/controllers/turbo/global_reload_controller.js rename to src/Validator/Constraints/ValidGTIN.php index ce8a6c72..20e81f1d 100644 --- a/assets/controllers/turbo/global_reload_controller.js +++ b/src/Validator/Constraints/ValidGTIN.php @@ -1,7 +1,8 @@ +. */ -import { Controller } from '@hotwired/stimulus'; +declare(strict_types=1); -export default class extends Controller { - connect() { - //If we encounter an element with global reload controller, then reload the whole page - window.location.reload(); - } -} \ No newline at end of file + +namespace App\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * A constraint to ensure that a GTIN is valid. + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class ValidGTIN extends Constraint +{ + +} diff --git a/src/Validator/Constraints/ValidGTINValidator.php b/src/Validator/Constraints/ValidGTINValidator.php new file mode 100644 index 00000000..57eb23e6 --- /dev/null +++ b/src/Validator/Constraints/ValidGTINValidator.php @@ -0,0 +1,54 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Validator\Constraints; + +use GtinValidation\GtinValidator; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +class ValidGTINValidator extends ConstraintValidator +{ + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof ValidGTIN) { + throw new UnexpectedTypeException($constraint, ValidGTIN::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!is_string($value)) { + throw new UnexpectedTypeException($value, 'string'); + } + + $gtinValidator = new GtinValidator($value); + if (!$gtinValidator->isValid()) { + $this->context->buildViolation('validator.invalid_gtin') + ->addViolation(); + } + } +} diff --git a/symfony.lock b/symfony.lock index 7c136b4b..f8f88675 100644 --- a/symfony.lock +++ b/symfony.lock @@ -305,7 +305,7 @@ "repo": "github.com/symfony/recipes", "branch": "main", "version": "11.1", - "ref": "c6658a60fc9d594805370eacdf542c3d6b5c0869" + "ref": "1117deb12541f35793eec9fff7494d7aa12283fc" }, "files": [ ".env.test", @@ -375,6 +375,54 @@ "shivas/versioning-bundle": { "version": "4.0.3" }, + "symfony/ai-bundle": { + "version": "0.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "2be6ccd77335c2631fdf12d1680649b072efb8ad" + }, + "files": [ + "config/packages/ai.yaml" + ] + }, + "symfony/ai-generic-platform": { + "version": "0.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "f38913b87380322d4c40c302b41626e811516bc4" + }, + "files": [ + "config/packages/ai_generic_platform.yaml" + ] + }, + "symfony/ai-lm-studio-platform": { + "version": "0.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "e35cced28f6559fc5effccb8f22597f309fedfdf" + }, + "files": [ + "config/packages/ai_lm_studio_platform.yaml" + ] + }, + "symfony/ai-open-router-platform": { + "version": "0.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "c39a146c6ec3df8b874accf6ce1cccbda431a688" + }, + "files": [ + "config/packages/ai_open_router_platform.yaml" + ] + }, "symfony/apache-pack": { "version": "1.0", "recipe": { @@ -393,12 +441,6 @@ "symfony/browser-kit": { "version": "v4.2.3" }, - "symfony/cache": { - "version": "v4.2.3" - }, - "symfony/cache-contracts": { - "version": "v1.1.5" - }, "symfony/config": { "version": "v4.2.3" }, @@ -488,12 +530,12 @@ ] }, "symfony/framework-bundle": { - "version": "7.3", + "version": "7.4", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "7.3", - "ref": "5a1497d539f691b96afd45ae397ce5fe30beb4b9" + "version": "7.4", + "ref": "09f6e081c763a206802674ce0cb34a022f0ffc6d" }, "files": [ ".editorconfig", @@ -550,15 +592,15 @@ "version": "v4.4.2" }, "symfony/monolog-bundle": { - "version": "3.10", + "version": "3.11", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", "version": "3.7", - "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + "ref": "1b9efb10c54cb51c713a9391c9300ff8bceda459" }, "files": [ - "./config/packages/monolog.yaml" + "config/packages/monolog.yaml" ] }, "symfony/options-resolver": { @@ -592,12 +634,6 @@ "symfony/polyfill-intl-normalizer": { "version": "v1.17.0" }, - "symfony/polyfill-mbstring": { - "version": "v1.10.0" - }, - "symfony/polyfill-php80": { - "version": "v1.17.0" - }, "symfony/process": { "version": "v4.2.3" }, @@ -617,12 +653,12 @@ ] }, "symfony/routing": { - "version": "7.3", + "version": "7.4", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "7.0", - "ref": "21b72649d5622d8f7da329ffb5afb232a023619d" + "version": "7.4", + "ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded" }, "files": [ "config/packages/routing.yaml", @@ -633,12 +669,12 @@ "version": "v5.3.4" }, "symfony/security-bundle": { - "version": "6.4", + "version": "7.4", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "6.4", - "ref": "2ae08430db28c8eb4476605894296c82a642028f" + "version": "7.4", + "ref": "c42fee7802181cdd50f61b8622715829f5d2335c" }, "files": [ "config/packages/security.yaml", @@ -661,18 +697,18 @@ "version": "v1.1.5" }, "symfony/stimulus-bundle": { - "version": "2.27", + "version": "2.31", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "2.20", - "ref": "e058471c5502e549c1404ebdd510099107bb5549" + "version": "2.24", + "ref": "3357f2fa6627b93658d8e13baa416b2a94a50c5f" }, "files": [ - "assets/bootstrap.js", "assets/controllers.json", "assets/controllers/csrf_protection_controller.js", - "assets/controllers/hello_controller.js" + "assets/controllers/hello_controller.js", + "assets/stimulus_bootstrap.js" ] }, "symfony/stopwatch": { @@ -724,18 +760,17 @@ "files": [] }, "symfony/ux-translator": { - "version": "2.9", + "version": "2.32", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "2.9", - "ref": "bc396565cc4cab95692dd6df810553dc22e352e1" + "version": "2.32", + "ref": "20e2abac415da4c3a9a6bafa059a6419beb74593" }, "files": [ - "./assets/translator.js", - "./config/packages/ux_translator.yaml", - "./var/translations/configuration.js", - "./var/translations/index.js" + "assets/translator.js", + "config/packages/ux_translator.yaml", + "var/translations/index.js" ] }, "symfony/ux-turbo": { @@ -785,12 +820,12 @@ ] }, "symfony/webpack-encore-bundle": { - "version": "2.2", + "version": "2.3", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", "version": "2.0", - "ref": "9ef5412a4a2a8415aca3a3f2b4edd3866aab9a19" + "ref": "719f6110345acb6495e496601fc1b4977d7102b3" }, "files": [ "assets/app.js", diff --git a/templates/_navbar.html.twig b/templates/_navbar.html.twig index 446ccdab..7719ab2b 100644 --- a/templates/_navbar.html.twig +++ b/templates/_navbar.html.twig @@ -10,9 +10,9 @@ - {% if is_granted("@tools.label_scanner") %} + {% if is_granted("@tools.label_scanner") %} - + {% endif %}
@@ -52,6 +52,23 @@ {% trans %}info_providers.search.title{% endtrans %} + {% if create_from_url_active() %} +
  • + + + {% trans %}info_providers.from_url.title{% endtrans %} + +
  • + {% endif %} + + {% if is_granted("@tools.label_scanner") %} +
  • + + + {% trans %}parts.create_from_scan.title{% endtrans %} + +
  • + {% endif %} {% endif %} {% if is_granted('@parts.import') %} @@ -69,11 +86,24 @@ {% if is_granted('@parts.read') %} {{ search.search_form("navbar") }} - {# {% include "_navbar_search.html.twig" %} #} + {# {% include "_navbar_search.html.twig" %} #} {% endif %} diff --git a/templates/admin/_delete_form.html.twig b/templates/admin/_delete_form.html.twig index fd653256..5a1d5a1a 100644 --- a/templates/admin/_delete_form.html.twig +++ b/templates/admin/_delete_form.html.twig @@ -5,7 +5,7 @@
    -
    +
    {% set delete_disabled = (not is_granted("delete", entity)) or (entity.group is defined and entity.id == 1) or entity == app.user %}
    @@ -20,7 +20,7 @@
    {% if entity.parent is defined %} -
    +
    diff --git a/templates/admin/_duplicate.html.twig b/templates/admin/_duplicate.html.twig index 1b18cd71..cf6e8cca 100644 --- a/templates/admin/_duplicate.html.twig +++ b/templates/admin/_duplicate.html.twig @@ -1,5 +1,5 @@
    - \ No newline at end of file +
    diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig index 07b00d43..7ef2e269 100644 --- a/templates/admin/_export_form.html.twig +++ b/templates/admin/_export_form.html.twig @@ -35,8 +35,8 @@
    -
    +
    - \ No newline at end of file + diff --git a/templates/admin/attachment_type_admin.html.twig b/templates/admin/attachment_type_admin.html.twig index 06a8c09d..9aeba934 100644 --- a/templates/admin/attachment_type_admin.html.twig +++ b/templates/admin/attachment_type_admin.html.twig @@ -6,6 +6,7 @@ {% block additional_controls %} {{ form_row(form.filetype_filter) }} + {{ form_row(form.allowed_targets) }} {{ form_row(form.alternative_names) }} {% endblock %} @@ -15,4 +16,4 @@ {% block new_title %} {% trans %}attachment_type.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/base_admin.html.twig b/templates/admin/base_admin.html.twig index 51790c3c..f19f4c44 100644 --- a/templates/admin/base_admin.html.twig +++ b/templates/admin/base_admin.html.twig @@ -86,7 +86,7 @@ - {% if entity.parameters is defined %} + {% if entity.parameters is defined and showParameters == true %} @@ -129,7 +129,7 @@
    -
    +
    {{ form_widget(form.save) }}
    - - {# Include turbo control things, so we can still control page title and reloading #} - {% include "_turbo_control.html.twig" %}
    - {% endblock %} \ No newline at end of file + {% endblock %} diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index 5811640b..e20349e9 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}category.labelp{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block additional_pills %} @@ -31,6 +31,7 @@
    {{ form_row(form.partname_regex) }} {{ form_row(form.partname_hint) }} + {{ form_row(form.part_ipn_prefix) }}
    {{ form_row(form.default_description) }} {{ form_row(form.default_comment) }} @@ -39,14 +40,12 @@
    {{ form_row(form.eda_info.reference_prefix) }} -
    -
    - {{ form_row(form.eda_info.visibility) }} -
    -
    + + {{ form_row(form.eda_info.visibility) }} +
    -
    +
    {{ form_widget(form.eda_info.exclude_from_bom) }} {{ form_widget(form.eda_info.exclude_from_board) }} {{ form_widget(form.eda_info.exclude_from_sim) }} @@ -54,10 +53,10 @@
    -
    +
    {% trans %}eda_info.kicad_section.title{% endtrans %}:
    {{ form_row(form.eda_info.kicad_symbol) }}
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/currency_admin.html.twig b/templates/admin/currency_admin.html.twig index fbd3822c..7e3d3bf8 100644 --- a/templates/admin/currency_admin.html.twig +++ b/templates/admin/currency_admin.html.twig @@ -3,17 +3,17 @@ {% import "vars.macro.twig" as vars %} {% block card_title %} - {% trans %}currency.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block additional_controls %} {{ form_row(form.iso_code) }} {% if entity.isoCode %}
    - + {% trans %}currency.iso_code.caption{% endtrans %}: {{ entity.isoCode }} - + {% trans %}currency.symbol.caption{% endtrans %}: {{ entity.isoCode | currency_symbol }}
    @@ -21,7 +21,7 @@ {{ form_row(form.exchange_rate) }} {% if entity.inverseExchangeRate %} -

    +

    {{ '1'|format_currency(vars.base_currency()) }} = {{ entity.inverseExchangeRate.tofloat | format_currency(entity.isoCode, {fraction_digit: 5}) }}
    {{ '1'|format_currency(entity.isoCode) }} = {{ entity.exchangeRate.tofloat | format_currency(vars.base_currency(), {fraction_digit: 5}) }}

    @@ -41,4 +41,4 @@ {% block new_title %} {% trans %}currency.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/footprint_admin.html.twig b/templates/admin/footprint_admin.html.twig index a2c3e4af..305ffc21 100644 --- a/templates/admin/footprint_admin.html.twig +++ b/templates/admin/footprint_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}footprint.labelp{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block master_picture_block %} @@ -28,10 +28,10 @@ {% block additional_panes %}
    -
    +
    {% trans %}eda_info.kicad_section.title{% endtrans %}:
    {{ form_row(form.eda_info.kicad_footprint) }}
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/group_admin.html.twig b/templates/admin/group_admin.html.twig index 91975524..831c08d5 100644 --- a/templates/admin/group_admin.html.twig +++ b/templates/admin/group_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}group.edit.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} @@ -27,4 +27,4 @@ {% block new_title %} {% trans %}group.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/label_profile_admin.html.twig b/templates/admin/label_profile_admin.html.twig index 10c2320f..6b78d0df 100644 --- a/templates/admin/label_profile_admin.html.twig +++ b/templates/admin/label_profile_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}label_profile.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block additional_pills %} @@ -27,7 +27,7 @@ {{ form_row(form.options.supported_element) }}
    {{ form_label(form.options.width) }} -
    +
    {{ form_widget(form.options.width) }} @@ -58,4 +58,4 @@ {% block new_title %} {% trans %}label_profile.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/manufacturer_admin.html.twig b/templates/admin/manufacturer_admin.html.twig index 5db892c0..4f8f1c2b 100644 --- a/templates/admin/manufacturer_admin.html.twig +++ b/templates/admin/manufacturer_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}manufacturer.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block edit_title %} @@ -10,4 +10,4 @@ {% block new_title %} {% trans %}manufacturer.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/measurement_unit_admin.html.twig b/templates/admin/measurement_unit_admin.html.twig index 31748509..14df7364 100644 --- a/templates/admin/measurement_unit_admin.html.twig +++ b/templates/admin/measurement_unit_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}measurement_unit.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block edit_title %} diff --git a/templates/admin/part_custom_state_admin.html.twig b/templates/admin/part_custom_state_admin.html.twig new file mode 100644 index 00000000..9d857646 --- /dev/null +++ b/templates/admin/part_custom_state_admin.html.twig @@ -0,0 +1,14 @@ +{% extends "admin/base_admin.html.twig" %} + +{% block card_title %} + {{ type_label_p(entity) }} +{% endblock %} + +{% block edit_title %} + {% trans %}part_custom_state.edit{% endtrans %}: {{ entity.name }} +{% endblock %} + +{% block new_title %} + {% trans %}part_custom_state.new{% endtrans %} +{% endblock %} + diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index 1a995069..11c41397 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -3,7 +3,7 @@ {# @var entity App\Entity\ProjectSystem\Project #} {% block card_title %} - {% trans %}project.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block edit_title %} @@ -31,8 +31,8 @@ {{ form_row(form.status) }} {% if entity.id %}
    - -
    + +
    {% if entity.buildPart %} {{ entity.buildPart.name }} {% else %} @@ -59,4 +59,4 @@ {% endif %}
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/storelocation_admin.html.twig b/templates/admin/storelocation_admin.html.twig index c93339dc..4bc5d305 100644 --- a/templates/admin/storelocation_admin.html.twig +++ b/templates/admin/storelocation_admin.html.twig @@ -2,13 +2,13 @@ {% import "label_system/dropdown_macro.html.twig" as dropdown %} {% block card_title %} - {% trans %}storelocation.labelp{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block additional_controls %} {% if entity.id %}
    -
    +
    {{ dropdown.profile_dropdown('storelocation', entity.id) }}
    @@ -38,4 +38,4 @@ {% block new_title %} {% trans %}storelocation.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/supplier_admin.html.twig b/templates/admin/supplier_admin.html.twig index ce38a5ca..d0ca85aa 100644 --- a/templates/admin/supplier_admin.html.twig +++ b/templates/admin/supplier_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}supplier.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block additional_panes %} @@ -19,4 +19,4 @@ {% block new_title %} {% trans %}supplier.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/update_manager/docker_progress.html.twig b/templates/admin/update_manager/docker_progress.html.twig new file mode 100644 index 00000000..e43b9afa --- /dev/null +++ b/templates/admin/update_manager/docker_progress.html.twig @@ -0,0 +1,235 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}update_manager.docker.progress_title{% endtrans %}{% endblock %} + +{% block card_title %} + + {% trans %}update_manager.docker.progress_title{% endtrans %} +{% endblock %} + +{% block card_content %} + +
    + + {# Progress Header #} +
    +
    +
    +
    + + + +
    +
    ~ ~ ~ ~ ~
    +
    +
    +

    + {% trans %}update_manager.docker.updating{% endtrans %} +

    +

    + {% trans %}update_manager.docker.updating_via_watchtower{% endtrans %} +

    +
    + + {# Progress Bar #} +
    +
    + 15% +
    +
    + + {# Current Step Info #} +
    + {% trans %}update_manager.docker.step_trigger{% endtrans %}: + {% trans %}update_manager.docker.step_trigger_desc{% endtrans %} +
    + + {# Success Message #} +
    + + {% trans %}update_manager.docker.success_message{% endtrans %} +
    + {% trans %}update_manager.docker.previous_version{% endtrans %}: + {{ previous_version }} + → + {% trans %}update_manager.docker.new_version{% endtrans %}: + ... +
    + + {# Timeout Message #} +
    + + {% trans %}update_manager.docker.timeout_message{% endtrans %} +
    + + {# Error Message #} +
    + + {% trans %}update_manager.progress.error{% endtrans %}: + +
    + + {# Steps Timeline - matches git progress style #} +
    +
    + {% trans %}update_manager.docker.steps{% endtrans %} +
    +
    +
      + {# Step 1: Trigger Watchtower #} +
    • + +
      + {% trans %}update_manager.docker.step_trigger{% endtrans %} +
      {% trans %}update_manager.docker.step_trigger_desc{% endtrans %} +
      + +
    • + + {# Step 2: Pull Image #} +
    • + +
      + {% trans %}update_manager.docker.step_pull{% endtrans %} +
      {% trans %}update_manager.docker.step_pull_desc{% endtrans %} +
      + +
    • + + {# Step 3: Stop Container #} +
    • + +
      + {% trans %}update_manager.docker.step_stop{% endtrans %} +
      {% trans %}update_manager.docker.step_stop_desc{% endtrans %} +
      + +
    • + + {# Step 4: Restart Container #} +
    • + +
      + {% trans %}update_manager.docker.step_restart{% endtrans %} +
      {% trans %}update_manager.docker.step_restart_desc{% endtrans %} +
      + +
    • + + {# Step 5: Health Check #} +
    • + +
      + {% trans %}update_manager.docker.step_health{% endtrans %} +
      {% trans %}update_manager.docker.step_health_desc{% endtrans %} +
      + +
    • + + {# Step 6: Verify Version #} +
    • + +
      + {% trans %}update_manager.docker.step_verify{% endtrans %} +
      {% trans %}update_manager.docker.step_verify_desc{% endtrans %} +
      + +
    • +
    +
    +
    + + {# Elapsed Time #} +
    + + {% trans %}update_manager.docker.elapsed{% endtrans %}: + 0s +
    + + {# Actions - shown after completion or timeout #} + + + {# Warning Notice #} +
    + + {% trans %}update_manager.docker.warning{% endtrans %}: + {% trans %}update_manager.docker.do_not_close{% endtrans %} +
    +
    +{% endblock %} diff --git a/templates/admin/update_manager/index.html.twig b/templates/admin/update_manager/index.html.twig new file mode 100644 index 00000000..3e4483a6 --- /dev/null +++ b/templates/admin/update_manager/index.html.twig @@ -0,0 +1,638 @@ +{% extends "main_card.html.twig" %} + +{% import "helper.twig" as helper %} + +{% block title %}Part-DB {% trans %}update_manager.title{% endtrans %}{% endblock %} + +{% block card_title %} + Part-DB {% trans %}update_manager.title{% endtrans %} +{% endblock %} + +{% block card_content %} +
    + + {# Maintenance Mode Warning #} + {% if is_maintenance %} + + {% endif %} + + {# Lock Warning #} + {% if is_locked %} + + {% endif %} + + {# Web Updates Disabled Warning #} + {% if web_updates_disabled %} + + {% endif %} + + {# Backup Restore Disabled Warning #} + {% if backup_restore_disabled %} + + {% endif %} + +
    + {# Current Version Card #} +
    +
    +
    + {% trans %}update_manager.current_installation{% endtrans %} +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + {% if status.git.is_git_install %} + + + + + + + + + + + + + {% endif %} + {% if is_docker %} + {# Docker: show Watchtower status #} + + + + + {% else %} + {# Git/other: show update readiness #} + + + + + {% endif %} + + +
    {% trans %}update_manager.version{% endtrans %} + {{ status.current_version }} +
    {% trans %}update_manager.installation_type{% endtrans %} + {{ status.installation.type_name }} +
    {% trans %}update_manager.web_updates_allowed{% endtrans %}{{ helper.boolean_badge(not web_updates_disabled) }}
    {% trans %}update_manager.backup_restore_allowed{% endtrans %}{{ helper.boolean_badge(not backup_restore_disabled) }}
    {% trans %}update_manager.backup_download_allowed{% endtrans %}{{ helper.boolean_badge(not backup_download_disabled) }}
    {% trans %}update_manager.git_branch{% endtrans %}{{ status.git.branch ?? 'N/A' }}
    {% trans %}update_manager.git_commit{% endtrans %}{{ status.git.commit ?? 'N/A' }}
    {% trans %}update_manager.local_changes{% endtrans %} + {% if status.git.has_local_changes %} + + {% trans %}Yes{% endtrans %} + + {% else %} + + {% trans %}No{% endtrans %} + + {% endif %} +
    {% trans %}update_manager.docker.watchtower_status{% endtrans %} + {% if status.watchtower_configured|default(false) and status.watchtower_available|default(false) %} + + {% trans %}update_manager.docker.watchtower_connected{% endtrans %} + + {% elseif status.watchtower_configured|default(false) %} + + {% trans %}update_manager.docker.watchtower_unreachable_short{% endtrans %} + + {% else %} + + {% trans %}update_manager.docker.watchtower_not_configured{% endtrans %} + + {% endif %} +
    {% trans %}update_manager.auto_update_supported{% endtrans %} + {{ helper.boolean_badge(status.can_auto_update) }} +
    +
    + +
    +
    + + {# Latest Version / Update Card #} +
    +
    +
    + {% if status.update_available %} + {% trans %}update_manager.new_version_available.title{% endtrans %} + {% else %} + {% trans %}update_manager.latest_release{% endtrans %} + {% endif %} +
    +
    + {% if status.latest_version %} +
    + + {{ status.latest_tag }} + + {% if not status.update_available %} +

    + + {% trans %}update_manager.already_up_to_date{% endtrans %} +

    + {% endif %} +
    + + {% if status.update_available and status.can_auto_update and validation.valid and not web_updates_disabled %} + {% if is_docker %} + {# Docker update via Watchtower #} +
    + + +
    + +
    + +
    + + +
    + +
    + + {% trans %}update_manager.docker.no_rollback_warning{% endtrans %} +
    +
    + {% else %} + {# Git update #} +
    + + + +
    + +
    + +
    + + +
    +
    + {% endif %} + {% endif %} + + {% if status.published_at %} +

    + + {% trans %}update_manager.released{% endtrans %}: {{ status.published_at|date('Y-m-d') }} +

    + {% endif %} + {% else %} +
    + +

    {% trans %}update_manager.could_not_fetch_releases{% endtrans %}

    +
    + {% endif %} +
    + {% if status.latest_tag %} + + {% endif %} +
    +
    +
    + + {# Validation Issues #} + {% if not validation.valid %} + + {% endif %} + + {# Non-auto-update installations info #} + {% if not status.can_auto_update %} + {% if is_docker and not status.watchtower_configured|default(false) %} + {# Docker without Watchtower - show setup instructions #} +
    +
    + {% trans %}update_manager.docker.setup_title{% endtrans %} +
    +
    +

    {% trans %}update_manager.docker.setup_description{% endtrans %}

    + +
    {% trans %}update_manager.docker.setup_step1{% endtrans %}
    +
    
    +                                # See documentation for full example: https://docs.part-db.de/installation/installation_docker.html
    +                                services:
    +                                  watchtower:
    +                                    image: ghcr.io/nicholas-fedor/watchtower:latest
    +                                    container_name: watchtower
    +                                    restart: unless-stopped
    +                                    volumes:
    +                                      - /var/run/docker.sock:/var/run/docker.sock
    +                                    environment:
    +                                      - WATCHTOWER_HTTP_API_UPDATE=true
    +                                      - WATCHTOWER_HTTP_API_TOKEN=your-secret-token
    +                                      - WATCHTOWER_LABEL_ENABLE=true
    +                                      - WATCHTOWER_CLEANUP=true
    +                            
    + +
    {% trans %}update_manager.docker.setup_step2{% endtrans %}
    +
    WATCHTOWER_API_URL=http://watchtower:8080
    +WATCHTOWER_API_TOKEN=your-secret-token
    + +
    + + {% trans %}update_manager.docker.setup_network_hint{% endtrans %} +
    +
    +
    + {% elseif is_docker and status.watchtower_configured|default(false) and not status.watchtower_available|default(false) %} + {# Docker with Watchtower configured but not reachable #} +
    +
    + {% trans %}update_manager.docker.watchtower_unreachable_title{% endtrans %} +
    +

    {% trans %}update_manager.docker.watchtower_unreachable_description{% endtrans %}

    +
    + {% else %} + {# Other non-auto-update installations (ZIP, unknown) #} +
    +
    + {% trans%}update_manager.cant_auto_update{% endtrans%}: {{ status.installation.type_name }} +
    +

    {{ status.installation.update_instructions }}

    +
    + {% endif %} + {% endif %} + +
    + {# Available Versions #} +
    +
    +
    + {% trans %}update_manager.available_versions{% endtrans %} +
    +
    +
    + + + + + + + + + + {% for release in all_releases %} + + + + + + {% else %} + + + + {% endfor %} + +
    {% trans %}update_manager.version{% endtrans %}{% trans %}update_manager.released{% endtrans %}
    + {{ release.tag }} + {% if release.prerelease %} + pre + {% endif %} + {% if release.version == status.current_version %} + {% trans %}update_manager.current{% endtrans %} + {% endif %} + + {{ release.published_at|date('Y-m-d') }} + +
    + + + + {% if release.version != status.current_version and status.can_auto_update and validation.valid and not web_updates_disabled %} + {% if is_docker %} + {# Docker: version switching not supported, only update to latest via Watchtower #} + {% else %} +
    + + + + +
    + {% endif %} + {% endif %} +
    +
    + {% trans %}update_manager.no_releases_found{% endtrans %} +
    +
    +
    +
    +
    + + {# Update History & Backups #} +
    +
    +
    + +
    +
    +
    +
    +
    + + + + + + + + + + {% for log in update_logs %} + + + + + + {% else %} + + + + {% endfor %} + +
    {% trans %}update_manager.date{% endtrans %}{% trans %}update_manager.log_file{% endtrans %}
    + {{ log.date|date('Y-m-d H:i') }} + {{ log.file }} +
    + + + + {% if is_granted('@system.manage_updates') %} +
    + + + +
    + {% endif %} +
    +
    + {% trans %}update_manager.no_logs_found{% endtrans %} +
    +
    +
    +
    + {% if is_granted('@system.manage_updates') and not is_locked %} +
    +
    + + +
    +
    + {% endif %} + {% if is_docker %} +
    + + {% trans %}update_manager.backup.docker_warning{% endtrans %} +
    + {% endif %} +
    + + + + + + + + + + + {% for backup in backups %} + + + + + + + {% else %} + + + + {% endfor %} + +
    {% trans %}update_manager.date{% endtrans %}{% trans %}update_manager.file{% endtrans %}{% trans %}update_manager.size{% endtrans %}
    + {{ backup.date|date('Y-m-d H:i') }} + {{ backup.file }} + {{ (backup.size / 1024 / 1024)|number_format(1) }} MB + +
    + {% if not backup_download_disabled and is_granted('@system.manage_updates') %} + + {% endif %} + {% if not backup_restore_disabled and is_granted('@system.manage_updates') %} +
    + + + + +
    + {% endif %} + {% if is_granted('@system.manage_updates') %} +
    + + + +
    + {% endif %} +
    + + {% if not backup_download_disabled and is_granted('@system.manage_updates') %} + {# Per-backup download modal - no inline JS needed, CSP compatible with Turbo #} + + {% endif %} +
    + {% trans %}update_manager.no_backups_found{% endtrans %} +
    +
    +
    +
    +
    +
    +
    +
    +
    + +{% endblock %} diff --git a/templates/admin/update_manager/log_viewer.html.twig b/templates/admin/update_manager/log_viewer.html.twig new file mode 100644 index 00000000..64412123 --- /dev/null +++ b/templates/admin/update_manager/log_viewer.html.twig @@ -0,0 +1,40 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{{ filename }} - {% trans %}update_manager.log_viewer{% endtrans %}{% endblock %} + +{% block card_title %} + {{ filename }} +{% endblock %} + +{% block card_content %} + + +
    +
    + + {% trans %}update_manager.update_log{% endtrans %} + + {{ content|length }} {% trans %}update_manager.bytes{% endtrans %} +
    +
    +
    {{ content }}
    +
    +
    + + +{% endblock %} diff --git a/templates/admin/update_manager/progress.html.twig b/templates/admin/update_manager/progress.html.twig new file mode 100644 index 00000000..54ac6595 --- /dev/null +++ b/templates/admin/update_manager/progress.html.twig @@ -0,0 +1,196 @@ +{% extends "main_card.html.twig" %} + +{% block title %} + {% if is_downgrade|default(false) %} + {% trans %}update_manager.progress.downgrade_title{% endtrans %} + {% else %} + {% trans %}update_manager.progress.title{% endtrans %} + {% endif %} +{% endblock %} + +{% block card_title %} + {% if progress and progress.status == 'running' %} + + {% elseif progress and progress.status == 'completed' %} + + {% elseif progress and progress.status == 'failed' %} + + {% else %} + + {% endif %} + {% if is_downgrade|default(false) %} + {% trans %}update_manager.progress.downgrade_title{% endtrans %} + {% else %} + {% trans %}update_manager.progress.title{% endtrans %} + {% endif %} +{% endblock %} + +{% block head %} + {{ parent() }} + {# Auto-refresh while update is running - also refresh when 'starting' status #} + {% if not progress or progress.status == 'running' or progress.status == 'starting' %} + + {% endif %} +{% endblock %} + +{% block card_content %} +
    + + {# Progress Header #} +
    +
    + {% if progress and progress.status == 'completed' %} + + {% elseif progress and progress.status == 'failed' %} + + {% else %} + + {% endif %} +
    +

    + {% if progress and progress.status == 'running' %} + {% if is_downgrade|default(false) %} + {% trans %}update_manager.progress.downgrading{% endtrans %} + {% else %} + {% trans %}update_manager.progress.updating{% endtrans %} + {% endif %} + {% elseif progress and progress.status == 'completed' %} + {% if is_downgrade|default(false) %} + {% trans %}update_manager.progress.downgrade_completed{% endtrans %} + {% else %} + {% trans %}update_manager.progress.completed{% endtrans %} + {% endif %} + {% elseif progress and progress.status == 'failed' %} + {% if is_downgrade|default(false) %} + {% trans %}update_manager.progress.downgrade_failed{% endtrans %} + {% else %} + {% trans %}update_manager.progress.failed{% endtrans %} + {% endif %} + {% else %} + {% trans %}update_manager.progress.initializing{% endtrans %} + {% endif %} +

    +

    + {% if progress %} + {% if is_downgrade|default(false) %} + {% trans with {'%version%': progress.target_version|default('unknown')} %}update_manager.progress.downgrading_to{% endtrans %} + {% else %} + {% trans with {'%version%': progress.target_version|default('unknown')} %}update_manager.progress.updating_to{% endtrans %} + {% endif %} + {% endif %} +

    +
    + + {# Progress Bar #} + {% set percent = progress ? ((progress.current_step|default(0) / progress.total_steps|default(12)) * 100)|round : 0 %} + {% if progress and progress.status == 'completed' %} + {% set percent = 100 %} + {% endif %} +
    +
    + {{ percent }}% +
    +
    + + {# Current Step - shows what's currently being worked on #} + {% if progress and (progress.status == 'running' or progress.status == 'starting') %} +
    + {{ progress.step_name|default('initializing')|replace({'_': ' '})|capitalize }}: + {{ progress.step_message|default('Processing...') }} +
    + {% endif %} + + {# Error Message #} + {% if progress and progress.status == 'failed' %} +
    + {% trans %}update_manager.progress.error{% endtrans %}: + {{ progress.error|default('An unknown error occurred') }} +
    + {% endif %} + + {# Success Message #} + {% if progress and progress.status == 'completed' %} +
    + + {% if is_downgrade|default(false) %} + {% trans %}update_manager.progress.downgrade_success_message{% endtrans %} + {% else %} + {% trans %}update_manager.progress.success_message{% endtrans %} + {% endif %} +
    + {% endif %} + + {# Steps Timeline #} +
    +
    + + {% if is_downgrade|default(false) %} + {% trans %}update_manager.progress.downgrade_steps{% endtrans %} + {% else %} + {% trans %}update_manager.progress.steps{% endtrans %} + {% endif %} +
    +
    +
      + {% if progress and progress.steps %} + {% for step in progress.steps %} +
    • + {% if step.success %} + + {% else %} + + {% endif %} +
      + {{ step.step|replace({'_': ' '})|capitalize }} +
      {{ step.message }} +
      + {{ step.timestamp|date('H:i:s') }} +
    • + {% endfor %} + {% else %} +
    • + {% trans %}update_manager.progress.waiting{% endtrans %} +
    • + {% endif %} +
    +
    +
    + + {# Actions #} +
    + {% if progress and (progress.status == 'completed' or progress.status == 'failed') %} + + {% trans %}update_manager.progress.back{% endtrans %} + + + {% trans %}update_manager.progress.refresh_page{% endtrans %} + + {% endif %} +
    + + {# Warning Notice #} + {% if not progress or progress.status == 'running' or progress.status == 'starting' %} +
    + + {% trans %}update_manager.progress.warning{% endtrans %}: + {% if is_downgrade|default(false) %} + {% trans %}update_manager.progress.downgrade_do_not_close{% endtrans %} + {% else %} + {% trans %}update_manager.progress.do_not_close{% endtrans %} + {% endif %} +
    + + {# JavaScript refresh - more reliable than meta refresh #} + + {% endif %} +
    +{% endblock %} diff --git a/templates/admin/update_manager/release_notes.html.twig b/templates/admin/update_manager/release_notes.html.twig new file mode 100644 index 00000000..9bcd9e04 --- /dev/null +++ b/templates/admin/update_manager/release_notes.html.twig @@ -0,0 +1,110 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{{ release.name }} - {% trans %}update_manager.release_notes{% endtrans %}{% endblock %} + +{% block card_title %} + {{ release.name }} +{% endblock %} + +{% block card_content %} + + +
    +
    + + + + + + + + + + + + + + + + + +
    {% trans %}update_manager.version{% endtrans %} + {{ release.version }} + {% if release.prerelease %} + {% trans %}update_manager.prerelease{% endtrans %} + {% endif %} +
    {% trans %}update_manager.tag{% endtrans %}{{ release.tag }}
    {% trans %}update_manager.released{% endtrans %}{{ release.published_at|date('Y-m-d H:i') }}
    {% trans %}update_manager.status{% endtrans %} + {% if release.version == current_version %} + {% trans %}update_manager.current{% endtrans %} + {% elseif release.version > current_version %} + {% trans %}update_manager.newer{% endtrans %} + {% else %} + {% trans %}update_manager.older{% endtrans %} + {% endif %} +
    +
    +
    + + {% trans %}update_manager.view_on_github{% endtrans %} + + {% if release.zipball_url %} + + ZIP + + {% endif %} +
    +
    + +{% if release.assets is not empty %} +
    +
    + {% trans %}update_manager.download_assets{% endtrans %} +
    +
    +
      + {% for asset in release.assets %} +
    • + + {{ asset.name }} + + ({{ (asset.size / 1024 / 1024)|number_format(1) }} MB) +
    • + {% endfor %} +
    +
    +
    +{% endif %} + +
    +
    + {% trans %}update_manager.changelog{% endtrans %} +
    +
    + {% if release.body %} +
    + {{ release.body|markdown_to_html }} +
    + {% else %} +

    {% trans %}update_manager.no_release_notes{% endtrans %}

    + {% endif %} +
    +
    + +{% if release.version > current_version %} +
    +
    + {% trans %}update_manager.update_to_this_version{% endtrans %} +
    +
    +

    {% trans %}update_manager.run_command_to_update{% endtrans %}

    +
    + php bin/console partdb:update {{ release.tag }} +
    +
    +
    +{% endif %} +{% endblock %} diff --git a/templates/admin/user_admin.html.twig b/templates/admin/user_admin.html.twig index 772b42d9..2092042f 100644 --- a/templates/admin/user_admin.html.twig +++ b/templates/admin/user_admin.html.twig @@ -5,7 +5,7 @@ {# @var entity \App\Entity\UserSystem\User #} {% block card_title %} - {% trans %}user.edit.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block comment %}{% endblock %} @@ -50,7 +50,7 @@
    {% if entity.samlUser %} -
    +
    {% trans %}user.saml_user{% endtrans %}
    {% endif %} @@ -60,7 +60,7 @@ {{ form_row(form.disabled) }} {% if entity.id is not null %} -
    +

    {% trans %}user.edit.tfa.caption{% endtrans %}
    @@ -111,4 +111,4 @@ {% block preview_picture %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/attachment_list.html.twig b/templates/attachment_list.html.twig index 3ff45700..2b2f6c39 100644 --- a/templates/attachment_list.html.twig +++ b/templates/attachment_list.html.twig @@ -44,7 +44,7 @@ {{ form_row(filterForm.discard) }}
    -
    +
    diff --git a/templates/attachments/html_sandbox.html.twig b/templates/attachments/html_sandbox.html.twig new file mode 100644 index 00000000..389ebc2e --- /dev/null +++ b/templates/attachments/html_sandbox.html.twig @@ -0,0 +1,78 @@ + + + + + + + + {# The content block is already escaped. so we must not escape it again. #} + {% trans %}attachment.sandbox.title{% endtrans %}: {{ attachment.filename }} + + + + +{% block body %} + {# We have a fullscreen iframe, with an warning on top #} + +
    + +
    +
    + ⚠️ {% trans%}attachment.sandbox.warning{% endtrans %} + +
    + + {% trans%}[Attachment]{% endtrans%}: {{ attachment.name }} / {{ attachment.filename ?? "" }} ({% trans%}id.label{% endtrans %}: {{ attachment.id }}) + {% trans%}attachment.sandbox.as_plain_text{% endtrans %} + {% trans%}attachment.sandbox.back_to_partdb{% endtrans %} + +
    +
    + + + + +
    + +{% endblock %} + + + diff --git a/templates/base.html.twig b/templates/base.html.twig index 58cccec5..afc7a8bf 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -2,7 +2,7 @@ @@ -26,6 +26,11 @@ + {# Allow pages to request a fully reload of everything #} + {% if global_reload_needed is defined and global_reload_needed %} + + {% endif %} + @@ -68,7 +73,17 @@ {{ encore_entry_script_tags('webauthn_tfa') }} {% endblock %} - + + +{# Listen for the special #} +{% if is_granted("@tools.label_scanner") %} +
    + +
    +{% endif %} + {% block body %}
    @@ -114,13 +129,13 @@ {# Must be outside of the sidebar or it will be hidden too #} diff --git a/templates/bundles/TwigBundle/Exception/error.html.twig b/templates/bundles/TwigBundle/Exception/error.html.twig index efdba462..936f5ca3 100644 --- a/templates/bundles/TwigBundle/Exception/error.html.twig +++ b/templates/bundles/TwigBundle/Exception/error.html.twig @@ -17,7 +17,7 @@ Consider yourself lucky. You found some rare error code.
    You should maybe inform your administrator about it... {% endblock %} - {% block further_actions %}

    You can try to Go Back or Visit the homepage.

    {% endblock %} + {% block further_actions %}

    You can try to Go Back or Visit the homepage.

    {% endblock %} {% block admin_contact %}

    If this error persists, please contact your {% if error_page_admin_email is not empty %} administrator. diff --git a/templates/bundles/TwigBundle/Exception/error500.html.twig b/templates/bundles/TwigBundle/Exception/error500.html.twig index 40a418c2..a4e5827b 100644 --- a/templates/bundles/TwigBundle/Exception/error500.html.twig +++ b/templates/bundles/TwigBundle/Exception/error500.html.twig @@ -17,7 +17,7 @@ Can not load frontend assets.

    Try following things:

    • Run yarn install and yarn build in Part-DB folder.
    • -
    • Run php bin/console cache:clear
    • +
    • Run php bin/console cache:clear and php bin/console cache:pool:clear --all
    {% elseif exception.class == "Doctrine\\DBAL\\Exception\\InvalidFieldNameException" or exception.class == "Doctrine\\DBAL\\Exception\\TableNotFoundException" @@ -26,21 +26,21 @@
    • Check if the DATABASE_URL in .env.local (or docker configure) is correct
    • Run php bin/console doctrine:migrations:migrate to upgrade database schema
    • -
    • Run php bin/console cache:clear
    • +
    • Run php bin/console cache:clear and php bin/console cache:pool:clear --all
    {% elseif exception.class == "Doctrine\\DBAL\\Exception\\DriverException" %} Error while executing database query.
    This is maybe caused by an old database schema.

    Try following things:

    • Check if the DATABASE_URL in .env.local (or docker configure) is correct
    • Run php bin/console doctrine:migrations:migrate to upgrade database schema (if upgrade is available)
    • -
    • Run php bin/console cache:clear
    • +
    • Run php bin/console cache:clear and php bin/console cache:pool:clear --all
    • If this issue persist, create a ticket at GitHub.
    {% else %} You could try following things, if this error is unexpected:
    • Check var/log/prod.log (or docker logs when Part-DB is running inside a docker container) for additional informations
    • -
    • Run php bin/console cache:clear to clear cache
    • +
    • Run php bin/console cache:clear and php bin/console cache:pool:clear --all to clear caches
    {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/components/datatables.macro.html.twig b/templates/components/datatables.macro.html.twig index d7873498..90f8a3e1 100644 --- a/templates/components/datatables.macro.html.twig +++ b/templates/components/datatables.macro.html.twig @@ -62,6 +62,9 @@ + + + diff --git a/templates/components/search.macro.html.twig b/templates/components/search.macro.html.twig index e62af2b1..90c01876 100644 --- a/templates/components/search.macro.html.twig +++ b/templates/components/search.macro.html.twig @@ -11,6 +11,10 @@
    +
    + + +
    @@ -102,4 +106,4 @@ {{ _self.settings_drodown(is_navbar) }} {% endif %} -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 366d42fe..aaa871ea 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -1,13 +1,15 @@ {% macro sidebar_dropdown() %} + {% set currentLocale = app.request.locale %} + {# Format is [mode, route, label, show_condition] #} {% set data_sources = [ - ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read')], - ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read')], - ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')], - ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')], - ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')], - ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')], - ['tools', path('tree_tools'), 'tools.label', true], + ['categories', path('tree_category_root'), '@category@@', is_granted('@categories.read') and is_granted('@parts.read')], + ['locations', path('tree_location_root'), '@storage_location@@', is_granted('@storelocations.read') and is_granted('@parts.read'), ], + ['footprints', path('tree_footprint_root'), '@footprint@@', is_granted('@footprints.read') and is_granted('@parts.read')], + ['manufacturers', path('tree_manufacturer_root'), '@manufacturer@@', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'], + ['suppliers', path('tree_supplier_root'), '@supplier@@', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'], + ['projects', path('tree_device_root'), '@project@@', is_granted('@projects.read'), 'project'], + ['tools', path('tree_tools'), 'tools.label', true, 'tool'], ] %} @@ -18,9 +20,20 @@ {% for source in data_sources %} {% if source[3] %} {# show_condition #} -
  • +
  • + {% if source[2] starts with '@' %} + {% set label = type_label_p(source[2]|replace({'@': ''})) %} + {% else %} + {% set label = source[2]|trans %} + {% endif %} + + +
  • {% endif %} {% endfor %} {% endmacro %} @@ -61,4 +74,4 @@
    -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 96b71bf0..818f0a0f 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -63,8 +63,8 @@
    {{ form_row(form.mountnames) }}
    - -
    + +
    {{ form_widget(form.price) }} {{ form_widget(form.priceCurrency) }} diff --git a/templates/form/extended_bootstrap_layout.html.twig b/templates/form/extended_bootstrap_layout.html.twig index 75e44a15..01d30815 100644 --- a/templates/form/extended_bootstrap_layout.html.twig +++ b/templates/form/extended_bootstrap_layout.html.twig @@ -17,11 +17,11 @@ {% block form_label_class -%} - col-sm-3 + {{ col_label }} {%- endblock form_label_class %} {% block form_group_class -%} - col-sm-9 + {{ col_input }} {%- endblock form_group_class %} {% block si_unit_widget %} @@ -100,6 +100,17 @@ {%- endif -%} {%- endblock tristate_widget %} +{% block tristate_row -%} + {#--#} +
    {#--#} +
    + {{- form_widget(form) -}} + {{- form_help(form) -}} + {{- form_errors(form) -}} +
    {#--#} +
    +{%- endblock tristate_row %} + {%- block choice_widget_collapsed -%} {# Only add the BS5 form-select class if we dont use bootstrap-selectpicker #} {# {% if attr["data-controller"] is defined and attr["data-controller"] not in ["elements--selectpicker"] %} @@ -144,3 +155,8 @@ {{- parent() -}} {% endif %} {% endblock %} + +{% block boolean_constraint_widget %} + {{ form_widget(form.value) }} + {{ form_errors(form.value) }} +{% endblock %} diff --git a/templates/form/settings_form.html.twig b/templates/form/settings_form.html.twig index 9e1a8c4f..8f76720e 100644 --- a/templates/form/settings_form.html.twig +++ b/templates/form/settings_form.html.twig @@ -3,7 +3,7 @@ {% block form_label %} {# If parameter_envvar exists on form then show it as tooltip #} {% if parameter_envvar is defined and parameter_envvar is not null %} - {%- set label_attr = label_attr|merge({title: 'settings.tooltip.overrideable_by_env'|trans(arguments = {'%env%': (parameter_envvar)|trim})}) -%} + {%- set label_attr = label_attr|merge({title: t('settings.tooltip.overrideable_by_env', {'%env%': (parameter_envvar)|trim})}) -%} {% endif %} {{- parent() -}} {% endblock %} @@ -11,7 +11,7 @@ {% block checkbox_radio_label %} {# If parameter_envvar exists on form then show it as tooltip #} {% if parameter_envvar is defined and parameter_envvar is not null %} - {%- set label_attr = label_attr|merge({title: 'settings.tooltip.overrideable_by_env'|trans(arguments = {'%env%': (parameter_envvar)|trim})}) -%} + {%- set label_attr = label_attr|merge({title: t('settings.tooltip.overrideable_by_env', {'%env%': (parameter_envvar)|trim})}) -%} {% endif %} {{- parent() -}} {% endblock %} @@ -19,7 +19,7 @@ {% block tristate_label %} {# If parameter_envvar exists on form then show it as tooltip #} {% if parameter_envvar is defined and parameter_envvar is not null %} - {%- set label_attr = label_attr|merge({title: 'settings.tooltip.overrideable_by_env'|trans(arguments = {'%env%': (parameter_envvar)|trim})}) -%} + {%- set label_attr = label_attr|merge({title: t('settings.tooltip.overrideable_by_env', {'%env%': (parameter_envvar)|trim})}) -%} {% endif %} {{- parent() -}} {% endblock %} diff --git a/templates/form/synonyms_collection.html.twig b/templates/form/synonyms_collection.html.twig new file mode 100644 index 00000000..ee69dffc --- /dev/null +++ b/templates/form/synonyms_collection.html.twig @@ -0,0 +1,59 @@ +{% macro renderForm(child) %} +
    + {% form_theme child "form/vertical_bootstrap_layout.html.twig" %} +
    +
    {{ form_row(child.dataSource) }}
    +
    {{ form_row(child.locale) }}
    +
    {{ form_row(child.translation_singular) }}
    +
    {{ form_row(child.translation_plural) }}
    +
    + +
    +
    +
    +{% endmacro %} + +{% block type_synonyms_collection_widget %} + {% set _attrs = attr|default({}) %} + {% set _attrs = _attrs|merge({ + class: (_attrs.class|default('') ~ ' type_synonyms_collection-widget')|trim + }) %} + + {% set has_proto = prototype is defined %} + {% if has_proto %} + {% set __proto %} + {{- _self.renderForm(prototype) -}} + {% endset %} + {% set _proto_html = __proto|e('html_attr') %} + {% set _proto_name = form.vars.prototype_name|default('__name__') %} + {% set _index = form|length %} + {% endif %} + +
    +
    +
    {% trans%}settings.synonyms.type_synonym.type{% endtrans%}
    +
    {% trans%}settings.synonyms.type_synonym.language{% endtrans%}
    +
    {% trans%}settings.synonyms.type_synonym.translation_singular{% endtrans%}
    +
    {% trans%}settings.synonyms.type_synonym.translation_plural{% endtrans%}
    +
    +
    + +
    + {% for child in form %} + {{ _self.renderForm(child) }} + {% endfor %} +
    + +
    +{% endblock %} diff --git a/templates/form/vertical_bootstrap_layout.html.twig b/templates/form/vertical_bootstrap_layout.html.twig new file mode 100644 index 00000000..5f41d82e --- /dev/null +++ b/templates/form/vertical_bootstrap_layout.html.twig @@ -0,0 +1,26 @@ +{% extends 'bootstrap_5_layout.html.twig' %} + + +{%- block choice_widget_collapsed -%} + {# Only add the BS5 form-select class if we dont use bootstrap-selectpicker #} + {# {% if attr["data-controller"] is defined and attr["data-controller"] not in ["elements--selectpicker"] %} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-select')|trim}) -%} + {% else %} + {# If it is an selectpicker add form-control class to fill whole width + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%} + {% endif %} + #} + + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-select')|trim}) -%} + + {# If no data-controller was explictly defined add data-controller=elements--select #} + {% if attr["data-controller"] is not defined %} + {%- set attr = attr|merge({"data-controller": "elements--select"}) -%} + + {% if attr["data-empty-message"] is not defined %} + {%- set attr = attr|merge({"data-empty-message": ("selectpicker.nothing_selected"|trans)}) -%} + {% endif %} + {% endif %} + + {{- block("choice_widget_collapsed", "bootstrap_base_layout.html.twig") -}} +{%- endblock choice_widget_collapsed -%} diff --git a/templates/helper.twig b/templates/helper.twig index 66268a96..e8c926e7 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -1,8 +1,8 @@ {% macro boolean(value) %} {% if value %} - {% trans %}bool.true{% endtrans %} + {% trans %}Yes{% endtrans %} {% else %} - {% trans %}bool.false{% endtrans %} + {% trans %}No{% endtrans %} {% endif %} {% endmacro %} @@ -14,9 +14,9 @@ {% macro bool_icon(bool) %} {% if bool %} - + {% else %} - + {% endif %} {% endmacro %} @@ -24,7 +24,7 @@ {% if value %} {% set class = class ~ ' bg-success' %} {% else %} - {% set class = class ~ ' bg-danger' %} + {% set class = class ~ ' bg-secondary' %} {% endif %} {{ _self.bool_icon(value) }} {{ _self.boolean(value) }} @@ -192,7 +192,7 @@ {% set preview_attach = part_preview_generator.tablePreviewAttachment(part) %} {% if preview_attach %} Part image + {{ stimulus_controller('elements/hoverpic') }} data-thumbnail="{{ attachment_thumbnail(preview_attach) }}"> {% endif %} {{ part.name }} {% endmacro %} @@ -241,3 +241,11 @@ {{ datetime|format_datetime }} {% endif %} {% endmacro %} + +{% macro vat_text(bool) %} + {% if bool === true %} + ({% trans %}prices.incl_vat{% endtrans %}) + {% elseif bool === false %} + ({% trans %}prices.excl_vat{% endtrans %}) + {% endif %} +{% endmacro %} diff --git a/templates/info_providers/bulk_import/manage.html.twig b/templates/info_providers/bulk_import/manage.html.twig index 9bbed906..b31dd650 100644 --- a/templates/info_providers/bulk_import/manage.html.twig +++ b/templates/info_providers/bulk_import/manage.html.twig @@ -22,103 +22,130 @@

    - {% if jobs is not empty %} -
    - - - - - - - - - - - - - - - - {% for job in jobs %} - - - - - - - - - - - - {% endfor %} - -
    {% trans %}info_providers.bulk_import.job_name{% endtrans %}{% trans %}info_providers.bulk_import.parts_count{% endtrans %}{% trans %}info_providers.bulk_import.results_count{% endtrans %}{% trans %}info_providers.bulk_import.progress{% endtrans %}{% trans %}info_providers.bulk_import.status{% endtrans %}{% trans %}info_providers.bulk_import.created_by{% endtrans %}{% trans %}info_providers.bulk_import.created_at{% endtrans %}{% trans %}info_providers.bulk_import.completed_at{% endtrans %}{% trans %}info_providers.bulk_import.action.label{% endtrans %}
    - {{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }} - {% if job.isInProgress %} - Active - {% endif %} - {{ job.partCount }}{{ job.resultCount }} -
    -
    -
    -
    -
    - {{ job.progressPercentage }}% -
    - - {% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %} - -
    - {% if job.isPending %} - {% trans %}info_providers.bulk_import.status.pending{% endtrans %} - {% elseif job.isInProgress %} - {% trans %}info_providers.bulk_import.status.in_progress{% endtrans %} - {% elseif job.isCompleted %} - {% trans %}info_providers.bulk_import.status.completed{% endtrans %} - {% elseif job.isStopped %} - {% trans %}info_providers.bulk_import.status.stopped{% endtrans %} - {% elseif job.isFailed %} - {% trans %}info_providers.bulk_import.status.failed{% endtrans %} - {% endif %} - {{ job.createdBy.fullName(true) }}{{ job.createdAt|format_datetime('short') }} - {% if job.completedAt %} - {{ job.completedAt|format_datetime('short') }} - {% else %} - - - {% endif %} - -
    - {% if job.isInProgress or job.isCompleted or job.isStopped %} - - {% trans %}info_providers.bulk_import.view_results{% endtrans %} - - {% endif %} - {% if job.canBeStopped %} - - {% endif %} - {% if job.isCompleted or job.isFailed or job.isStopped %} - - {% endif %} -
    -
    -
    - {% else %} + {% if active_jobs is empty and finished_jobs is empty %} + {% else %} + {# Active Jobs #} + {% if active_jobs is not empty %} +
    + {% trans %}info_providers.bulk_import.active_jobs{% endtrans %} + {{ active_jobs|length }} +
    + {{ _self.job_table(active_jobs, false) }} + {% endif %} + + {# Finished Jobs (History) #} + {% if finished_jobs is not empty %} +
    + {% trans %}info_providers.bulk_import.finished_jobs{% endtrans %} + {{ finished_jobs|length }} +
    + {{ _self.job_table(finished_jobs, true) }} + {% endif %} {% endif %}
    {% endblock %} + +{% macro job_table(jobs, showCompletedAt) %} +
    + + + + + + + + + + + {% if showCompletedAt %} + + {% endif %} + + + + + {% for job in jobs %} + {{ _self.job_row(job, showCompletedAt) }} + {% endfor %} + +
    {% trans %}info_providers.bulk_import.job_name{% endtrans %}{% trans %}info_providers.bulk_import.parts_count{% endtrans %}{% trans %}info_providers.bulk_import.results_count{% endtrans %}{% trans %}info_providers.bulk_import.progress{% endtrans %}{% trans %}info_providers.bulk_import.status{% endtrans %}{% trans %}info_providers.bulk_import.created_by{% endtrans %}{% trans %}info_providers.bulk_import.created_at{% endtrans %}{% trans %}info_providers.bulk_import.completed_at{% endtrans %}{% trans %}info_providers.bulk_import.action.label{% endtrans %}
    +
    +{% endmacro %} + +{% macro job_row(job, showCompletedAt) %} + {% set showCompletedAt = showCompletedAt|default(false) %} + + + #{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }} +
    {{ job.formattedTimestamp }} + + {{ job.partCount }} + {{ job.resultCount }} + +
    +
    +
    +
    +
    + {{ job.progressPercentage }}% +
    + + {% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %} + + + + {% if job.isPending %} + {% trans %}info_providers.bulk_import.status.pending{% endtrans %} + {% elseif job.isInProgress %} + {% trans %}info_providers.bulk_import.status.in_progress{% endtrans %} + {% elseif job.isCompleted %} + {% trans %}info_providers.bulk_import.status.completed{% endtrans %} + {% elseif job.isStopped %} + {% trans %}info_providers.bulk_import.status.stopped{% endtrans %} + {% elseif job.isFailed %} + {% trans %}info_providers.bulk_import.status.failed{% endtrans %} + {% endif %} + + {{ job.createdBy.fullName(true) }} + {{ job.createdAt|format_datetime('short') }} + {% if showCompletedAt %} + + {% if job.completedAt %} + {{ job.completedAt|format_datetime('short') }} + {% else %} + - + {% endif %} + + {% endif %} + +
    + {% if job.isInProgress or job.isCompleted or job.isStopped %} + + {% trans %}info_providers.bulk_import.view_results{% endtrans %} + + {% endif %} + {% if job.canBeStopped %} + + {% endif %} + {% if job.isCompleted or job.isFailed or job.isStopped %} + + {% endif %} +
    + + +{% endmacro %} diff --git a/templates/info_providers/bulk_import/step2.html.twig b/templates/info_providers/bulk_import/step2.html.twig index 559ca20a..e68202e0 100644 --- a/templates/info_providers/bulk_import/step2.html.twig +++ b/templates/info_providers/bulk_import/step2.html.twig @@ -9,22 +9,42 @@ {% block card_title %} {% trans %}info_providers.bulk_import.step2.title{% endtrans %} - {{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }} + #{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }} {% endblock %} {% block card_content %} + + + + {% if job.isCompleted %} + + {% endif %} +
    -
    {{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}
    +
    #{{ job.id }} - {{ job.displayNameKey|trans(job.displayNameParams) }}
    {{ job.partCount }} {% trans %}info_providers.bulk_import.parts{% endtrans %} • {{ job.resultCount }} {% trans %}info_providers.bulk_import.results{% endtrans %} • @@ -95,6 +115,13 @@ {% trans %}info_providers.bulk_import.research.all_pending{% endtrans %} +
    @@ -181,39 +208,74 @@ - {% for result in part_result.searchResults %} + {% set sortedResults = part_result.resultsSortedByPriority %} + {% for result in sortedResults %} {# @var result \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO #} {% set dto = result.searchResult %} {% set localPart = result.localPart %} - + {% set isTopResult = loop.first %} + - + {% if dto.preview_image_url %} + + {% endif %} + {# Check for matches against source keyword (what was searched) #} + {% set sourceKw = result.sourceKeyword|default('')|lower %} + {% set nameMatch = sourceKw is not empty and dto.name is not null and dto.name|lower == sourceKw %} + {% set mpnMatch = sourceKw is not empty and dto.mpn is not null and dto.mpn|lower == sourceKw %} + {% set spnMatch = sourceKw is not empty and dto.provider_id is not null and dto.provider_id|lower == sourceKw %} + {% set anyMatch = nameMatch or mpnMatch or spnMatch %} {% if dto.provider_url is not null %} - {{ dto.name }} + {{ dto.name }} {% else %} - {{ dto.name }} + {{ dto.name }} + {% endif %} + {% if nameMatch %} + {% endif %} {% if dto.mpn is not null %} -
    {{ dto.mpn }} +
    {{ dto.mpn }}
    + {% if mpnMatch %} + MPN + {% endif %} {% endif %} {{ dto.description }} {{ dto.manufacturer ?? '' }} {{ info_provider_label(dto.provider_key)|default(dto.provider_key) }} -
    {{ dto.provider_id }} +
    {{ dto.provider_id }} + {% if spnMatch %} + SPN + {% endif %} - {{ result.sourceField ?? 'unknown' }} + {% if anyMatch %} + {% trans %}info_providers.bulk_import.match{% endtrans %} + {% else %} + {{ result.sourceField ?? 'unknown' }} + {% endif %} {% if result.sourceKeyword %} -
    {{ result.sourceKeyword }} - {% endif %} +
    {{ result.sourceKeyword }} + {% endif %}
    + {% if not isCompleted %} + + {% endif %} {% set updateHref = path('info_providers_update_part', {'id': part.id, 'providerKey': dto.provider_key, 'providerId': dto.provider_id}) ~ '?jobId=' ~ job.id %} diff --git a/templates/info_providers/from_url/from_url.html.twig b/templates/info_providers/from_url/from_url.html.twig new file mode 100644 index 00000000..3146c5a5 --- /dev/null +++ b/templates/info_providers/from_url/from_url.html.twig @@ -0,0 +1,63 @@ +{% extends "main_card.html.twig" %} + +{% import "info_providers/providers.macro.html.twig" as providers_macro %} +{% import "helper.twig" as helper %} + +{% block title %} + {% trans %}info_providers.from_url.title{% endtrans %} +{% endblock %} + +{% block card_title %} + {% trans %}info_providers.from_url.title{% endtrans %} +{% endblock %} + +{% block card_content %} +

    {% trans %}info_providers.from_url.help{% endtrans %}

    + + {{ form_start(form) }} + {{ form_row(form.url) }} + + {{ form_row(form.method) }} + +
    + +
    +
    + {{ form_row(form.no_cache) }} + {{ form_row(form.skip_delegation) }} +
    +
    + + {{ form_row(form.submit) }} + + {% if recentBrowserPages is not empty %} +
    + +
    + +
    +

    {% trans %}browser_plugin.recent_pages.help{% endtrans %}

    +
    + {% for page in recentBrowserPages %} + + {% endfor %} +
    +
    +
    + {% endif %} + + {{ form_end(form) }} +{% endblock %} diff --git a/templates/info_providers/providers.macro.html.twig b/templates/info_providers/providers.macro.html.twig index bf530ebd..bec8f24b 100644 --- a/templates/info_providers/providers.macro.html.twig +++ b/templates/info_providers/providers.macro.html.twig @@ -27,7 +27,7 @@ title="{% trans %}info_providers.settings.title{% endtrans %}" > {% endif %} - {% for capability in provider.capabilities %} + {% for capability in provider.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %} {# @var capability \App\Services\InfoProviderSystem\Providers\ProviderCapabilities #} diff --git a/templates/info_providers/search/part_search.html.twig b/templates/info_providers/search/part_search.html.twig index 3d741c34..9e1f0834 100644 --- a/templates/info_providers/search/part_search.html.twig +++ b/templates/info_providers/search/part_search.html.twig @@ -28,11 +28,24 @@ {{ form_row(form.providers) }}
    - + + +
    +
    + {{ form_row(form.no_cache_search) }} + {{ form_row(form.no_cache_details) }} +
    +
    + {{ form_row(form.submit) }} {{ form_end(form) }} @@ -94,7 +107,13 @@ {{ dto.footprint }} {% endif %} - {{ helper.m_status_to_badge(dto.manufacturing_status) }} + + {{ helper.m_status_to_badge(dto.manufacturing_status) }} + {% if dto.gtin %} +
    + {{ dto.gtin }} + {% endif %} + {% if dto.provider_url %} @@ -110,16 +129,16 @@ {% if update_target %} {# We update an existing part #} {% set href = path('info_providers_update_part', - {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'id': update_target.iD}) %} + {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'id': update_target.iD, 'no_cache': no_cache_details ? 1 : null}) %} {% else %} {# Create a fresh part #} {% set href = path('info_providers_create_part', - {'providerKey': dto.provider_key, 'providerId': dto.provider_id}) %} + {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'no_cache': no_cache_details ? 1 : null}) %} {% endif %} {# If we have no local part, then we can just show the create button #} {% if localPart is null %} + target="_blank" title="{% trans %}part.create.btn{% endtrans %}"> {% else %} {# Otherwise add a button group with all three buttons #} @@ -133,7 +152,7 @@ target="_blank" title="{% trans %}info_providers.search.show_existing_part{% endtrans %}"> - diff --git a/templates/info_providers/settings/provider_settings.html.twig b/templates/info_providers/settings/provider_settings.html.twig index 1876c2eb..db942f8a 100644 --- a/templates/info_providers/settings/provider_settings.html.twig +++ b/templates/info_providers/settings/provider_settings.html.twig @@ -8,9 +8,9 @@ {% block card_title %} {% trans %}info_providers.settings.title{% endtrans %}: {{ info_provider_info.name }}{% endblock %} {% block card_content %} -
    +

    - {% if info_provider_info.url %} + {% if info_provider_info.url is defined %} {{ info_provider_info.name }} {% else %} {{ info_provider_info.name }} @@ -23,7 +23,7 @@ {{ form_start(form) }}
    -
    +
    {{ form_help(form) }}
    diff --git a/templates/label_system/dialog.html.twig b/templates/label_system/dialog.html.twig index b9149aa3..532a4b63 100644 --- a/templates/label_system/dialog.html.twig +++ b/templates/label_system/dialog.html.twig @@ -36,7 +36,7 @@ {{ form_row(form.options.supported_element) }}
    {{ form_label(form.options.width) }} -
    +
    {{ form_widget(form.options.width) }} @@ -59,8 +59,8 @@
    - -
    + +
    {{ profile.name ?? '-' }} {% if profile and is_granted("edit", profile) %}
    -
    +