tito10047/persistent-selection-bundle

Symfony bundle for handling persistent selections across paginated lists using Session or other storage.

Installs: 6

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 2

Type:symfony-bundle

pkg:composer/tito10047/persistent-selection-bundle

0.3.0 2025-11-26 13:02 UTC

This package is auto-updated.

Last update: 2025-11-29 12:09:49 UTC


README

Tests

🛒 Persistent Selection Bundle


Make true "Select All" and persistent state management effortless in Symfony.

More than just checkboxes. This bundle provides a robust engine for managing persistent selections and state across sessions, pages, and filters.

It allows you to store IDs + Metadata (context) efficiently. Whether you need a "Select All" for 50,000 items in an admin grid, a session-based, database-based or any-based, Shopping Cart for domains, or simply need to remember which accordion items are expanded—this bundle handles the persistence layer so you don't have to.

⚠️ v0.1.0 Stable Beta: Public API is frozen but the bundle is under active development.

✨ Key Features

  • True "Select All": Efficiently handle selections across thousands of records using Doctrine-optimized loaders (ID-only).
  • Metadata Support: Store context with your selection (e.g., ['qty' => 5] or ['variant' => 'XL']) alongside the ID.
  • Context-Aware: Manage multiple independent selections simultaneously (e.g., main_grid, wishlist, user_123_cart).
  • Flexible Inputs: Accepts Entities, UUIDs, Integers, or Strings.
  • Memory Safe: Works with scalar IDs internally; hydrates objects only when you need them.
  • Zero-Config UI: Includes Twig helpers and Stimulus controllers for instant integration.

📌 Use Cases

🛠️ Admin & Batch Operations

  • Mass Actions: Select all users in a filtered view (spanning 50 pages) and apply a "Block" action.
  • Invoicing: Select specific invoices across pagination, then export them to a single ZIP.
  • Inverted Selection: "Select All" 10,000 items, uncheck 3 specific exceptions, and process the rest.

🛒 State & Metadata (New in v0.5.0)

  • Shopping Carts: Build a domain registrar cart where users select domains (ID) and years (Metadata) without page reloads.
  • UI Persistence: Remember which tree-view nodes are expanded or which tabs are active across page refreshes.
  • Wizards: Collect items across multiple steps of a wizard before final processing.

🚀 Quick Start

1) Configure the bundle

# config/packages/persistent_selection.yaml
persistent_selection:
    default:
        normalizer: 'persistent_selection.normalizer.object' # Auto-detects IDs
        storage: 'persistent_selection.storage.session'      # Uses PHP Session
    scalar:
        normalizer: 'persistent_selection.normalizer.scalar'
    array:
        normalizer: 'persistent_selection.normalizer.array'
        identifier_path: 'id'
# config/routes/persistent_selection.yaml
persistent_selection:
    resource: '@PersistentSelectionBundle/config/routes.php'

2) Usage in Controller (The Manager Pattern)

The SelectionManager acts as a factory. You request a specific context (e.g., 'event_attendees' or 'my_cart') and interact with that state object.

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Tito10047\PersistentSelectionBundle\Service\SelectionManagerInterface;
use App\Entity\Product;

final class CartController extends AbstractController
{
    public function __construct(
        private readonly SelectionManagerInterface $selectionManager,
    ) {}

    public function addToCart(Product $product, Request $request): Response
    {
        // 1. Get the interface for a specific context
        $cart = $this->selectionManager->getSelection('my_cart');

        // 2. Add item with Metadata (Contextual Data)
        // You can pass null, an array, or a serializable object
        $cart->select($product, [
            'quantity' => $request->get('qty', 1),
            'added_at' => new \DateTime()
        ]);

        return $this->json(['count' => $cart->getTotal()]);
    }

    public function checkout(): void
    {
        $cart = $this->selectionManager->getSelection('my_cart');

        // 3. Retrieve hydrated objects with their metadata
        // Returns: [ 101 => ['quantity' => 2], 102 => ['quantity' => 1] ]
        $selectedItems = $cart->getSelectedObjects(); 
        
        // ... process checkout logic ...

        // 4. Cleanup
        $cart->destroy();
    }
}

3) "Select All" with Doctrine Source

For mass actions in Grids, register a source (QueryBuilder) so the bundle knows how to fetch "All" IDs when the user clicks "Select All".

public function list(SelectionManagerInterface $manager): void
{
    $qb = $this->repo->createQueryBuilder('u')->where('u.active = true');

    // Register the source to enable "Select All" functionality for this context
    $manager->registerSource('user_grid', $qb);
}

4) Wire up the UI (Twig)

The bundle provides powerful Twig helpers to check state and retrieve metadata.

{# Check global state #}
{% set isAllSelected = persistent_selection_is_selected_all('user_grid') %}

<table>
    <thead>
        <tr>
            <th>
                {# Stimulus controller handles the UI toggling #}
                <div {{ persistent_selection_stimulus_controller('user_grid') }}>
                    <button data-action="{{ persistent_selection_stimulus_controller_name }}#selectAll">Select All</button>
                    <button data-action="{{ persistent_selection_stimulus_controller_name }}#unselectAll">Unselect All</button>
                </div>
            </th>
            <th>Product</th>
            <th>Qty</th>
        </tr>
    </thead>
    <tbody>
    {% for product in products %}
        <tr>
            <td>
                {# Check individual state #}
                {% if persistent_selection_is_selected('user_grid', product) %}
                    <input type="checkbox" checked>
                {% endif %}
            </td>
            <td>{{ product.name }}</td>
            <td>
                {# Retrieve Metadata #}
                {% set meta = persistent_selection_metadata('user_grid', product) %}
                
                {# Access metadata values easily #}
                {{ meta.quantity|default(0) }}
            </td>
        </tr>
    {% endfor %}
    </tbody>
</table>

🧠 Architecture

  • The bundle is built on stable interfaces to ensure long-term compatibility:
  • SelectionManager (Factory): Creates context-aware instances.
  • SelectionInterface (Stateful): The main API (select, unselect, getMetadata).
  • StorageInterface: "Dumb" persistence layer (Session, Redis, DB) handling map storage.
  • MetadataConverter: Handles complex object serialization for metadata.

This documentation covers the most important extension points of the bundle with focused, example‑driven guides.