PHPackages                             ecourty/sitemap-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. ecourty/sitemap-bundle

ActiveSymfony-bundle[Utility &amp; Helpers](/categories/utility)

ecourty/sitemap-bundle
======================

Symfony bundle for generating XML sitemaps with support for static routes, dynamic entities, and sitemap index

1.0.0(3mo ago)194↓50%MITPHPPHP &gt;=8.3CI passing

Since Jan 9Pushed 3mo agoCompare

[ Source](https://github.com/EdouardCourty/sitemap-bundle)[ Packagist](https://packagist.org/packages/ecourty/sitemap-bundle)[ RSS](/packages/ecourty-sitemap-bundle/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (18)Versions (2)Used By (0)

ecourty/sitemap-bundle
======================

[](#ecourtysitemap-bundle)

[![CI](https://github.com/EdouardCourty/sitemap-bundle/actions/workflows/ci.yml/badge.svg)](https://github.com/EdouardCourty/sitemap-bundle/actions/workflows/ci.yml)

A Symfony bundle for generating XML sitemaps. Supports static routes, dynamic Doctrine entities, and extensive configuration options.

The bundle handles both dynamic generation via controller and static file generation via command.
Memory-efficient streaming prevents issues with large datasets.

Table of Contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
    - [Use Cases](#use-cases)
    - [Full Configuration Example](#full-configuration-example)
    - [Configuration Reference](#configuration-reference)
- [Usage](#usage)
    - [Dynamic Generation (Controller)](#dynamic-generation-controller)
    - [Static Generation (Command)](#static-generation-command)
    - [Generated XML Output](#generated-xml-output)
- [Advanced Configuration](#advanced-configuration)
    - [Custom Repository Method](#custom-repository-method)
    - [Custom Service with FQCN::method](#custom-service-with-fqcnmethod)
    - [DQL Conditions](#dql-conditions)
    - [Multiple Route Parameters](#multiple-route-parameters)
    - [Sitemap Index Modes](#sitemap-index-modes)
- [Extensibility](#extensibility)
    - [Custom URL Providers](#custom-url-providers)
    - [Use Cases for Custom Providers](#use-cases-for-custom-providers)
- [Architecture](#architecture)
- [Performance](#performance)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
- [Contributing](#contributing)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP 8.3+
- Symfony 6.4+ / 7.0+ / 8.0+
- Doctrine ORM 3.0+ / 4.0+
- Extension: `ext-xmlwriter`

Installation
------------

[](#installation)

```
composer require ecourty/sitemap-bundle
```

The bundle will be automatically registered in `config/bundles.php` if using Symfony Flex (otherwise, add it manually):

```
return [
    // ...
    Ecourty\SitemapBundle\SitemapBundle::class => ['all' => true],
];
```

Quick Start
-----------

[](#quick-start)

### 1. Basic Configuration

[](#1-basic-configuration)

Create `config/packages/sitemap.yaml`:

**Example: Static routes only**

```
sitemap:
    base_url: 'https://example.com'

    static_routes:
        - route: 'homepage'
          priority: 1.0
          changefreq: 'daily'
```

**Example: With dynamic entities**

```
sitemap:
    base_url: 'https://example.com'

    static_routes:
        - route: 'homepage'
          priority: 1.0
          changefreq: 'daily'

    entity_routes:
        - entity: 'App\Entity\Article'
          route: 'article_show'
          route_params:
              slug: 'slug'  # entity property -> route parameter
          priority: 0.8
          changefreq: 'weekly'
          lastmod_property: 'updatedAt'
```

### 2. Import Routes (Optional)

[](#2-import-routes-optional)

**Only required if you want dynamic generation via `/sitemap.xml`.**

If you only use static generation (`sitemap:dump` command), you can skip this step.

⚠️ **Important**: Dynamic generation only works for simple sitemaps (single file). If you're using sitemap indexes (`use_index: true` or above the configured URLs threshold, default to 50k URLs), you must use static generation.

Add the bundle routes to `config/routes.yaml`:

```
sitemap:
    resource: '@SitemapBundle/Resources/config/routes.yaml'
```

### 3. Access Your Sitemap

[](#3-access-your-sitemap)

**Dynamic generation** - visit in your browser:

```
https://example.com/sitemap.xml

```

**Static generation** - generate a file:

```
php bin/console sitemap:dump # Generates public/sitemap.xml
```

That's it! 🎉

Configuration
-------------

[](#configuration)

### Use Cases

[](#use-cases)

#### Case 1: Simple Website (Static Pages Only)

[](#case-1-simple-website-static-pages-only)

Perfect for marketing sites, landing pages, or small websites with fixed pages:

```
sitemap:
    base_url: 'https://mysite.com'

    static_routes:
        - route: 'homepage'
          priority: 1.0
          changefreq: 'daily'
        - route: 'about'
        - route: 'services'
        - route: 'contact'
          priority: 0.6
```

#### Case 2: Blog or News Site

[](#case-2-blog-or-news-site)

Static pages + dynamic articles from database:

```
sitemap:
    base_url: 'https://myblog.com'

    static_routes:
        - route: 'homepage'
          priority: 1.0
        - route: 'blog_index'
          priority: 0.9

    entity_routes:
        - entity: 'App\Entity\Article'
          route: 'article_show'
          route_params:
              slug: 'slug'
          priority: 0.8
          changefreq: 'weekly'
          lastmod_property: 'publishedAt'
```

#### Case 3: E-commerce Site

[](#case-3-e-commerce-site)

Multiple entity types with different priorities:

```
sitemap:
    base_url: 'https://myshop.com'

    static_routes:
        - route: 'homepage'
          priority: 1.0
        - route: 'catalog'
          priority: 0.9

    entity_routes:
        # Products (high priority, frequently updated)
        - entity: 'App\Entity\Product'
          route: 'product_show'
          route_params:
              id: 'id'
              slug: 'slug'
          priority: 0.8
          changefreq: 'daily'
          lastmod_property: 'updatedAt'
          query_builder_method: 'findActiveProducts'  # Only published products

        # Categories (medium priority)
        - entity: 'App\Entity\Category'
          route: 'category_show'
          route_params:
              slug: 'slug'
          priority: 0.6
          changefreq: 'weekly'

        # Blog articles (lower priority)
        - entity: 'App\Entity\BlogPost'
          route: 'blog_show'
          route_params:
              slug: 'slug'
          priority: 0.5
          changefreq: 'monthly'
```

#### Case 4: Filtering with DQL Conditions

[](#case-4-filtering-with-dql-conditions)

When you need simple filtering without creating custom repository methods, use `conditions`:

```
sitemap:
    base_url: 'https://myblog.com'

    entity_routes:
        # Only published articles
        - entity: 'App\Entity\Article'
          route: 'article_show'
          route_params:
              slug: 'slug'
          priority: 0.8
          changefreq: 'weekly'
          lastmod_property: 'updatedAt'
          conditions: 'e.published = true AND e.deletedAt IS NULL'

        # Only active products in stock
        - entity: 'App\Entity\Product'
          route: 'product_show'
          route_params:
              slug: 'slug'
          priority: 0.7
          changefreq: 'daily'
          conditions: 'e.active = true AND e.stock > 0'

        # Only upcoming events
        - entity: 'App\Entity\Event'
          route: 'event_show'
          route_params:
              id: 'id'
          priority: 0.9
          changefreq: 'daily'
          conditions: 'e.startDate >= CURRENT_DATE()'
```

**Note**: Use the alias `e` in your DQL conditions. You cannot combine `conditions` with `query_builder_method`.

#### Case 5: Large Dataset (Automatic Index)

[](#case-5-large-dataset-automatic-index)

For sites with 50,000+ URLs, the bundle automatically creates a sitemap index:

```
sitemap:
    base_url: 'https://bigsite.com'
    use_index: 'auto'  # Automatically split if > 50,000 URLs
    index_threshold: 50000

    entity_routes:
        - entity: 'App\Entity\Product'
          route: 'product_show'
          route_params:
              slug: 'slug'
          # With 150,000 products, this creates:
          # sitemap_entity_product_1.xml (50,000 URLs)
          # sitemap_entity_product_2.xml (50,000 URLs)
          # sitemap_entity_product_3.xml (50,000 URLs)
```

### Full Configuration Example

[](#full-configuration-example)

```
sitemap:
    # Base URL of your site (required)
    base_url: 'https://example.com'

    # Sitemap index strategy:
    # - 'auto': generate index if total URLs > index_threshold (default)
    # - true: always generate index (even with few URLs)
    # - false: never generate index, single sitemap.xml file
    use_index: 'auto'

    # URL count threshold for auto index mode
    index_threshold: 50000

    # Static routes (without parameters)
    static_routes:
        # Homepage with high priority
        - route: 'homepage'
          priority: 1.0
          changefreq: 'daily'
          lastmod: '-1 day'  # Optional: relative time string

        # Blog listing page
        - route: 'blog_list'
          priority: 0.9
          changefreq: 'daily'

        # About page
        - route: 'about'
          priority: 0.5
          changefreq: 'monthly'

    # Dynamic routes (with Doctrine entities)
    entity_routes:
        # Example: Song entities with custom repository method
        - entity: 'App\Entity\Song'
          route: 'song_show'
          route_params:
              uid: 'uid'  # entity property -> route parameter
          priority: 0.8
          changefreq: 'weekly'
          lastmod_property: 'updatedAt'  # Optional: DateTime property
          query_builder_method: 'getSitemapQueryBuilder'  # Optional: repository method

        # Example: Post entities with custom service
        - entity: 'App\Entity\Post'
          route: 'post_show'
          route_params:
              slug: 'slug'
          priority: 0.7
          changefreq: 'monthly'
          lastmod_property: 'publishedAt'
          query_builder_method: 'App\Service\PostSitemapService::getQueryBuilder'  # Optional: FQCN::method

        # Example: Product entities with DQL conditions
        - entity: 'App\Entity\Product'
          route: 'product_detail'
          route_params:
              id: 'id'
              slug: 'slug'
          priority: 0.6
          changefreq: 'weekly'
```

### Configuration Reference

[](#configuration-reference)

OptionTypeDefaultDescription`base_url`string*required*Base URL for absolute URLs`use_index`string|bool`'auto'`Index strategy: 'auto', true, false`index_threshold`int`50000`URL count threshold for auto index`static_routes[].route`string*required*Symfony route name`static_routes[].priority`float`0.5`Priority (0.0-1.0)`static_routes[].changefreq`string`'weekly'`Change frequency`static_routes[].lastmod`string|null`null`Relative time (e.g., '-2 days')`entity_routes[].entity`string*required*Entity class name (FQCN)`entity_routes[].route`string*required*Symfony route name`entity_routes[].route_params`array*required*Property → parameter mapping`entity_routes[].priority`float`0.5`Priority (0.0-1.0)`entity_routes[].changefreq`string`'weekly'`Change frequency`entity_routes[].lastmod_property`string|null`null`DateTime property name`entity_routes[].query_builder_method`string|null`null`Repository method OR FQCN::method`entity_routes[].conditions`string|null`null`DQL WHERE clause**Valid `changefreq` values**: `always`, `hourly`, `daily`, `weekly`, `monthly`, `yearly`, `never`

**Important**:

- Cannot use both `query_builder_method` and `conditions` simultaneously.
- `query_builder_method` can be a repository method name (e.g., `'getSitemapQueryBuilder'`) or a FQCN::method (e.g., `'App\Service\SitemapService::getArticlesQueryBuilder'`)

Usage
-----

[](#usage)

### Dynamic Generation (Controller)

[](#dynamic-generation-controller)

**Requires routes import** - see step 2 in [Quick Start](#quick-start).

Once routes are imported, access the dynamic sitemap at:

```
https://example.com/sitemap.xml

```

The sitemap is generated on-the-fly from your configuration and database.

**⚠️ Important limitation**: Dynamic generation only works for **simple sitemaps** (single `sitemap.xml` file). If your configuration generates a sitemap index with multiple files (due to `use_index: true` or exceeding the threshold), the controller will only serve the main `sitemap.xml` index file. The individual sitemap files (`sitemap_static.xml`, `sitemap_entity_*.xml`) will **not** be accessible via controller routes.

**Recommended for:**

- Small to medium sites (&lt; 50,000 URLs)
- Sites needing always up-to-date data
- Simple sitemap configuration (`use_index: false`)

**For sitemap indexes, use static generation instead** (see below).

### Static Generation (Command)

[](#static-generation-command)

**No routes import needed** - works out of the box after configuration.

Generate a static sitemap file:

```
# Generate to public/ directory (default)
php bin/console sitemap:dump

# Generate to custom directory (relative to public/)
php bin/console sitemap:dump --output=sitemaps

# Generate to absolute directory path
php bin/console sitemap:dump --output=/var/www/public/sitemaps

# Force overwrite existing files without confirmation
php bin/console sitemap:dump --force
```

**Important**: The `--output` option specifies a **directory**, not a file, because the generator may create multiple files:

- For simple sitemaps: `sitemap.xml`
- For sitemap indexes: `sitemap.xml` (index) + `sitemap_static.xml`, `sitemap_entity_product.xml`, etc.

**✅ Recommended for:**

- Large sites with many URLs (&gt; 50,000 URLs)
- Sites with infrequent content updates
- SEO-critical sites (serve static files via web server/CDN)
- **Any configuration using sitemap indexes** (`use_index: true` or exceeding threshold)

**Required for sitemap indexes**: Dynamic generation via controller cannot serve individual sitemap files. Use static generation to write all files to disk.

**Tip:** Run via cron to regenerate periodically:

```
# Regenerate sitemap every night at 3am
0 3 * * * cd /var/www && php bin/console sitemap:dump --force
```

### Generated XML Output

[](#generated-xml-output)

#### Simple Sitemap (Mixed Content)

[](#simple-sitemap-mixed-content)

When you have both static routes and dynamic entities with `use_index: false`:

```

        https://example.com/
        2026-01-05
        daily
        1.0

        https://example.com/about
        monthly
        0.5

        https://example.com/blog
        daily
        0.9

        https://example.com/article/symfony-best-practices
        2026-01-03
        weekly
        0.8

        https://example.com/article/php-8-features
        2026-01-04
        weekly
        0.8

        https://example.com/product/123/awesome-widget
        2026-01-05
        weekly
        0.7

```

#### Sitemap Index (Large Datasets)

[](#sitemap-index-large-datasets)

When `use_index: true` or URL count exceeds threshold, the bundle generates an index file referencing separate sitemaps per source.

**Benefits:**

- ✅ Better organization (one file per entity type)
- ✅ Faster incremental updates (regenerate only changed sources)
- ✅ Respects sitemap.org 50,000 URL limit per file
- ✅ Easier debugging and monitoring

**`sitemap.xml` (index file):**

```

        https://example.com/sitemap_static.xml
        2026-01-05T20:30:00+00:00

        https://example.com/sitemap_entity_article.xml
        2026-01-05T20:30:15+00:00

        https://example.com/sitemap_entity_product_1.xml
        2026-01-05T20:30:45+00:00

        https://example.com/sitemap_entity_product_2.xml
        2026-01-05T20:30:52+00:00

```

**`sitemap_static.xml` (static routes only):**

```

        https://example.com/
        2026-01-05
        daily
        1.0

        https://example.com/about
        monthly
        0.5

```

**`sitemap_entity_article.xml` (articles only):**

```

        https://example.com/article/symfony-best-practices
        2026-01-03
        weekly
        0.8

        https://example.com/article/php-8-features
        2026-01-04
        weekly
        0.8

```

**Note**: When a source has more than 50,000 URLs, it's automatically split into numbered files (`sitemap_entity_product_1.xml`, `sitemap_entity_product_2.xml`, etc.)

Advanced Configuration
----------------------

[](#advanced-configuration)

### Custom Repository Method

[](#custom-repository-method)

For better performance with filtering and optimization, create a custom repository method that returns a `QueryBuilder`:

```
// src/Repository/PostRepository.php
use Doctrine\ORM\QueryBuilder;

class PostRepository extends ServiceEntityRepository
{
    public function getSitemapQueryBuilder(): QueryBuilder
    {
        return $this->createQueryBuilder('p')
            ->where('p.published = true')
            ->andWhere('p.deletedAt IS NULL')
            ->orderBy('p.updatedAt', 'DESC');
    }
}
```

**Important:** Return a `QueryBuilder`, not the query result. The bundle will:

- Add `COUNT()` for efficient counting
- Optimize the SELECT to fetch only needed fields
- Use `toIterable()` for memory-efficient streaming

Then reference it in config:

```
entity_routes:
    - entity: 'App\Entity\Post'
      route: 'post_show'
      route_params:
          slug: 'slug'
      query_builder_method: 'getSitemapQueryBuilder'
```

### Custom Service with FQCN::method

[](#custom-service-with-fqcnmethod)

For more flexibility, use any service (not just the entity's repository):

```
// src/Service/PostSitemapService.php
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;

class PostSitemapService
{
    public function __construct(
        private EntityManagerInterface $em,
    ) {
    }

    public function getPublishedPostsQueryBuilder(): QueryBuilder
    {
        return $this->em->createQueryBuilder()
            ->select('p')
            ->from(Post::class, 'p')
            ->where('p.status = :published')
            ->setParameter('published', 'published')
            ->orderBy('p.publishedAt', 'DESC');
    }
}
```

Configuration:

```
entity_routes:
    - entity: 'App\Entity\Post'
      route: 'post_show'
      route_params:
          slug: 'slug'
      query_builder_method: 'App\Service\PostSitemapService::getPublishedPostsQueryBuilder'
```

### DQL Conditions

[](#dql-conditions)

Use DQL conditions for simple filtering without custom methods:

```
entity_routes:
    - entity: 'App\Entity\Post'
      route: 'post_show'
      route_params:
          slug: 'slug'
      conditions: 'e.published = true AND e.deletedAt IS NULL'
```

### Multiple Route Parameters

[](#multiple-route-parameters)

Map multiple entity properties to route parameters:

```
entity_routes:
    - entity: 'App\Entity\Product'
      route: 'product_detail'
      route_params:
          category: 'category.slug'  # Nested property
          slug: 'slug'
      priority: 0.8
```

### Sitemap Index Modes

[](#sitemap-index-modes)

Control how sitemaps are split:

```
sitemap:
    # Auto mode (default): index if total URLs > threshold
    use_index: 'auto'
    index_threshold: 50000

    # Always use index (even with few URLs)
    use_index: true

    # Never use index (single sitemap.xml)
    use_index: false
```

Example with index:

```
sitemap.xml                  # Index file
sitemap_static.xml           # Static routes
sitemap_entity_song.xml      # Song entities
sitemap_entity_post_1.xml    # Post entities (first 50k)
sitemap_entity_post_2.xml    # Post entities (remaining)

```

Extensibility
-------------

[](#extensibility)

### Custom URL Providers

[](#custom-url-providers)

For complex URL generation needs beyond static routes and Doctrine entities, implement a custom `UrlProviderInterface`.

This is the most powerful extension point in the bundle, giving you complete control over URL generation.

### Use Cases for Custom Providers

[](#use-cases-for-custom-providers)

- **Multi-entity relationships**: URLs requiring parameters from different entities (e.g., `/category/{slug}/product/{id}`)
- **Dynamic routing**: Different routes based on entity type or database fields
- **External data sources**: Generate URLs from APIs, MongoDB, Redis, etc.
- **Complex business logic**: Custom filtering, permissions, multi-tenancy
- **Legacy systems**: Integrate with existing URL structures

### Implementation Example: Products with Category Parameters

[](#implementation-example-products-with-category-parameters)

This example shows how to generate URLs like `/category/electronics/product/smartphone-x` where both category and product slugs are needed:

```
// src/Service/ProductUrlProvider.php
namespace App\Service;

use Doctrine\ORM\EntityManagerInterface;
use Ecourty\SitemapBundle\Contract\UrlProviderInterface;
use Ecourty\SitemapBundle\Enum\ChangeFrequency;
use Ecourty\SitemapBundle\Model\SitemapUrl;
use App\Entity\Product;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class ProductUrlProvider implements UrlProviderInterface
{
    // Inject dependencies

    public function getUrls(): iterable
    {
        // Join with Category to get both slugs in one query
        $qb = $this->em->createQueryBuilder()
            ->select('p', 'c') // Select both product and category
            ->from(Product::class, 'p')
            ->innerJoin('p.category', 'c')
            ->where('p.active = true')
            ->andWhere('c.active = true')
            ->orderBy('p.updatedAt', 'DESC');

        foreach ($qb->getQuery()->toIterable() as $product) {
            // Generate URL with both category and product parameters
            $path = $this->urlGenerator->generate('product_show', [
                'categorySlug' => $product->getCategory()->getSlug(),
                'productSlug' => $product->getSlug(),
            ]);

            yield new SitemapUrl(
                loc: rtrim($this->baseUrl, '/') . $path,
                priority: 0.7,
                changefreq: ChangeFrequency::WEEKLY,
                lastmod: $product->getUpdatedAt(),
            );
        }
    }

    public function count(): int
    {
        // Implement the count efficiently
        // $qb = $this->em->createQueryBuilder()
        //    ->select('COUNT(p.id)')
        //    ...
    }

    public function getSourceName(): string
    {
        return 'products';
    }
}
```

**Configuration:**

```
# config/services.yaml
services:
    App\Service\ProductUrlProvider:
        arguments:
            $baseUrl: '%sitemap.base_url%'
        # Automatically tagged as 'sitemap.url_provider' via autoconfiguration
```

That's it! The provider will be automatically discovered and used. No additional configuration needed.

Architecture
------------

[](#architecture)

### Design Patterns

[](#design-patterns)

- **Registry Pattern**: `UrlProviderRegistry` collects all URL providers via tagged services
- **Provider Pattern**: Each URL source implements `UrlProviderInterface`
- **Strategy Pattern**: Index vs single sitemap decision based on configuration
- **DTO Pattern**: Immutable readonly configuration objects

### Extension Points

[](#extension-points)

The bundle is designed for extensibility:

- **Custom URL Providers**: Implement `UrlProviderInterface` for any URL source (see [Extensibility](#extensibility) section)
- **Custom Repository Methods**: Fine-tune Doctrine queries for entity routes
- **Tagged Services**: Automatic discovery via `sitemap.url_provider` tag
- **Configuration DTOs**: Type-safe configuration objects

Performance
-----------

[](#performance)

### Memory Optimization

[](#memory-optimization)

The bundle automatically uses Doctrine's `toIterable()` to stream entities, preventing memory issues with large datasets.

**What the bundle does internally:**

```
// ✅ Automatic streaming - no memory issues with 100k+ entities
$query->toIterable();

// ❌ Would load all entities in memory at once
$query->getResult();
```

**Your responsibility:** Return a `QueryBuilder` from repository methods (not query results):

```
// ✅ Correct - return QueryBuilder
public function getSitemapQueryBuilder(): QueryBuilder
{
    return $this->createQueryBuilder('p')
        ->where('p.published = true');
}

// ❌ Wrong - don't call getQuery() or toIterable()
public function getSitemapData(): iterable
{
    return $this->createQueryBuilder('p')
        ->getQuery()
        ->toIterable();  // Bundle handles this automatically
}
```

### Recommendations

[](#recommendations)

1. **Use repository methods** - The bundle optimizes the SELECT to fetch only needed fields
2. **Add database indexes** on columns used in WHERE clauses and route parameters
3. **Enable sitemap index** for datasets &gt;50k URLs (automatic with `use_index: 'auto'`)
4. **⚠️ Use static generation for sitemap indexes** - Dynamic controller cannot serve individual sitemap files
5. **Run static generation** as a cron job during low-traffic periods
6. **Use a CDN** to cache sitemap files

### Example: Optimized for Large Datasets

[](#example-optimized-for-large-datasets)

```
sitemap:
    base_url: 'https://bigsite.com'
    use_index: 'auto'  # Splits at 50k URLs per file

    entity_routes:
        - entity: 'App\Entity\Product'
          route: 'product_show'
          route_params:
              slug: 'slug'
          query_builder_method: 'getActiveProductsQueryBuilder'
```

```
// Repository method with filtering
public function getActiveProductsQueryBuilder(): QueryBuilder
{
    return $this->createQueryBuilder('p')
        ->where('p.active = true')
        ->andWhere('p.stock > 0')
        ->orderBy('p.updatedAt', 'DESC');
}
```

**Result:** Can handle millions of products with minimal memory usage.

Development
-----------

[](#development)

### Development Workflow

[](#development-workflow)

Contributions are welcome! The project follows strict coding standards to maintain high code quality.

**Setup:**

```
composer install
```

**Development cycle:**

```
# Make your changes, then run quality checks:
composer qa              # Runs all checks (phpstan, cs-check, tests)

# Or run individual checks:
composer phpstan         # Static analysis (Level 9)
composer cs-check        # Code style check (PSR-12)
composer cs-fix          # Fix code style automatically
composer test            # Run PHPUnit tests
```

**Before submitting:**

- Ensure `composer qa` passes without errors
- Add tests for the new feature
- Update documentation as needed

### Code Standards

[](#code-standards)

All contributions must follow the project's coding standards:

- PHP 8.3+ with `declare(strict_types=1)` in all files
- PSR-12 code style (enforced by PHP-CS-Fixer)
- PHPStan Level 9 (strict type safety, no mixed types)
- Full test coverage for new features
- Complete PHPDoc blocks with types

See [AGENTS.md](AGENTS.md) for detailed developer and AI agent guide.

License
-------

[](#license)

MIT License - see [LICENSE](LICENSE) file for details.

Support
-------

[](#support)

- 🐛 [Report a bug](https://github.com/EdouardCourty/sitemap-bundle/issues)
- 💡 [Request a feature](https://github.com/EdouardCourty/sitemap-bundle/issues)
- 📖 [Documentation](https://github.com/EdouardCourty/sitemap-bundle#readme)
- 📧 Email:

###  Health Score

40

—

FairBetter than 87% of packages

Maintenance82

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

119d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3150ffb131124e5f03272d9ed8084c514f18fff6aafff1a5973c016993f6ef66?d=identicon)[ecourty](/maintainers/ecourty)

---

Top Contributors

[![EdouardCourty](https://avatars.githubusercontent.com/u/37371516?v=4)](https://github.com/EdouardCourty "EdouardCourty (14 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ecourty-sitemap-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/ecourty-sitemap-bundle/health.svg)](https://phpackages.com/packages/ecourty-sitemap-bundle)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.4k5.6M646](/packages/sylius-sylius)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.3M152](/packages/sulu-sulu)[prestashop/prestashop

PrestaShop is an Open Source e-commerce platform, committed to providing the best shopping cart experience for both merchants and customers.

9.0k15.4k](/packages/prestashop-prestashop)[contao/core-bundle

Contao Open Source CMS

1231.6M2.3k](/packages/contao-core-bundle)[ec-cube/ec-cube

EC-CUBE EC open platform.

78527.0k1](/packages/ec-cube-ec-cube)[netgen/layouts-core

Netgen Layouts enables you to build and manage complex web pages in a simpler way and with less coding. This is the core of Netgen Layouts, its heart and soul.

3689.4k10](/packages/netgen-layouts-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
