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
Requires
- php: >=8.1
- symfony/framework-bundle: ^6.4|^7.3
- symfony/property-access: ^6.4|^7.3
- symfony/translation: ^6.4|^7.3
- symfony/twig-bundle: ^6.4|^7.3
Requires (Dev)
- dama/doctrine-test-bundle: ^8.4
- pagerfanta/pagerfanta: ^3.6
- phpunit/phpunit: ^10.5
- symfony/asset-mapper: ^6.4|^7.3
- symfony/dotenv: ^6.4|^7.3
- symfony/http-client: ^6.4|^7.3
- symfony/maker-bundle: ^1.39
- symfony/orm-pack: ^2.5
- symfony/phpunit-bridge: ^6.4|^7.3
- symfony/runtime: ^6.4|^7.3
- symfony/serializer-pack: ^1.3
- symfony/stimulus-bundle: ^2.31
- symfony/uid: ^6.4|^7.3
- symfony/yaml: ^6.4|^7.3
- zenstruck/foundry: ^2.0
This package is auto-updated.
Last update: 2025-11-29 12:09:49 UTC
README
🛒 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.
- Identifier normalization — converting items to scalar IDs:
- Loaders — how IDs are collected from various sources:
- Storage — how selections are persisted (per user, per context):
- See: docs/storage.md
- Twig helpers — UI functions available in templates:
