diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..713d500 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env diff --git a/README.md b/README.md index c90738e..43c4140 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,345 @@ -Variable generator -====================== +# Variable Generator ![Integrity check](https://github.com/baraja-core/variable-generator/workflows/Integrity%20check/badge.svg) -Generate new variable symbol by last variable and selected strategy. +A smart PHP library for generating unique variable symbols, order numbers, and sequential identifiers in e-commerce applications. It handles complex problems like format specifications, transaction safety, duplicate prevention, and overflow management through pluggable strategies. -Idea ----- +## :bulb: Key Principles -A series of smart tools for generating variable symbols and order numbers in your e-shop. +- **Automatic duplicate protection** - Uses locking mechanism to prevent concurrent generation of the same number +- **Pluggable strategies** - Choose from built-in strategies or implement your own formatting logic +- **Doctrine integration** - Automatic entity discovery for seamless database integration +- **Transaction safety** - Built-in lock management ensures data integrity in high-concurrency environments +- **Year-aware numbering** - Default strategy automatically resets sequences on year change +- **Zero configuration** - Works out of the box with sensible defaults -Generating order numbers or other number series hides a number of complex problems. For example, adhering to the specified format according to the specification, handling transaction entries (to avoid duplication) and handling the case when the generated value overflows. +## :building_construction: Architecture Overview -This package contains a set of algorithms and ready-made strategies to elegantly solve these problems. If any of the algorithms do not suit you, you can implement your own just by satisfying the defined interface. +The library is built around a modular architecture with clear separation of concerns: -πŸ“¦ Installation ---------------- +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ VariableGenerator β”‚ +β”‚ (Main Entry Point) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚VariableLoaderβ”‚ β”‚FormatStrategyβ”‚ β”‚ Lock β”‚ β”‚ +β”‚ β”‚ (Interface) β”‚ β”‚ (Interface) β”‚ β”‚ (External) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚DefaultOrder β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚VariableLoaderβ”‚ β”‚ β”‚YearPrefixIncrementStrategy β”‚ β”‚ β”‚ +β”‚ β”‚ (Doctrine) β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚SimpleIncrementStrategy β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ +β”‚ β”‚ β”‚Custom Strategy (Your Own) β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` -It's best to use [Composer](https://getcomposer.org) for installation, and you can also find the package on -[Packagist](https://packagist.org/packages/baraja-core/variable-generator) and -[GitHub](https://github.com/baraja-core/variable-generator). +### :jigsaw: Main Components -To install, simply use the command: +| Component | Description | +|-----------|-------------| +| `VariableGenerator` | Main service class that orchestrates number generation with locking and strategy execution | +| `VariableLoader` | Interface for retrieving the last used number from your data source | +| `FormatStrategy` | Interface defining how the next number should be calculated | +| `OrderEntity` | Interface for Doctrine entities that enables automatic loader discovery | +| `VariableGeneratorExtension` | Nette DI extension for framework integration | +| `VariableGeneratorAccessor` | Accessor interface for lazy service injection | + +### :gear: Built-in Strategies + +#### YearPrefixIncrementStrategy (Default) + +Generates numbers in format `YYXXXXXX` where `YY` is the current year and `XXXXXX` is an incrementing sequence: ``` -$ composer require baraja-core/variable-generator +Year: 2024 +Format: 24000001, 24000002, 24000003, ... + +Year: 2025 (automatic reset on year change) +Format: 25000001, 25000002, 25000003, ... ``` -You can use the package manually by creating an instance of the internal classes, or register a DIC extension to link the services directly to the Nette Framework. +Features: +- Automatic year prefix based on current date +- Automatic sequence reset on year change +- Configurable total length (default: 8 characters) +- Overflow protection (expands length if needed) -How to use ----------- +#### SimpleIncrementStrategy -At the beginning, create an instance of the Generator or get it from the DIC. If you are using Doctrine entities, there is an autoconfiguration that will automatically find your entity with an order (must meet the `OrderEntity` interface) and you can start generating numbers. +A straightforward incrementing strategy that adds one to the previous number: -Example: +``` +Input: 21000034 +Output: 21000035 +``` + +Features: +- Maintains consistent number length with zero-padding +- Configurable length (minimum: 4 characters) +- Falls back to year-prefixed first number if no previous exists + +## :rocket: Basic Usage + +### Creating the Generator ```php +use Baraja\VariableGenerator\VariableGenerator; +use Baraja\VariableGenerator\Strategy\YearPrefixIncrementStrategy; + +// With Doctrine EntityManager (automatic entity discovery) $generator = new VariableGenerator( - variableLoader, // last used variable loader, default is DefaultOrderVariableLoader - strategy, // generator strategy, default is YearPrefixIncrementStrategy - entityManager, // if you want use default variable loader by Doctrine entity + variableLoader: null, // Auto-discovered from Doctrine + strategy: null, // Uses YearPrefixIncrementStrategy by default + em: $entityManager, ); + +// With custom variable loader +$generator = new VariableGenerator( + variableLoader: new MyCustomVariableLoader(), + strategy: new YearPrefixIncrementStrategy(length: 6), +); +``` + +### Generating Numbers + +```php +// Generate next number (automatically retrieves last used number) +$newOrderNumber = $generator->generate(); +// Result: 24000001 (if first order in 2024) + +// Generate next number based on specific previous value +$newNumber = $generator->generate('24000034'); +// Result: 24000035 + +// Get current (last used) number without generating new one +$current = $generator->getCurrent(); +// Result: 24000034 +``` + +### Using Custom Strategies + +```php +use Baraja\VariableGenerator\Strategy\SimpleIncrementStrategy; + +// Switch to simple increment strategy +$generator->setStrategy(new SimpleIncrementStrategy(length: 10)); + +// Or use custom strategy at initialization +$generator = new VariableGenerator( + variableLoader: $loader, + strategy: new SimpleIncrementStrategy(length: 8), +); +``` + +## :closed_lock_with_key: Duplicate Prevention + +This library automatically protects against generating duplicate numbers in high-concurrency environments. The protection mechanism works as follows: + +1. **Wait for existing transactions** - Before generating, the system waits if another process is currently generating +2. **Lock acquisition** - A 15-second transaction lock is acquired for the generation process +3. **Number generation** - The new number is calculated using the selected strategy +4. **Short protection window** - A 1-second lock remains to allow saving the new entity to database + +```php +// The generate() method handles all locking automatically +$number = $generator->generate(); +// IMPORTANT: Save your entity immediately after generation! + +// Custom transaction name for different entity types +$orderNumber = $generator->generate(transactionName: 'order-generator'); +$invoiceNumber = $generator->generate(transactionName: 'invoice-generator'); +``` + +> **Warning:** You must save the generated number to your database within 1 second. After that, the lock is released and another process may generate the same number. + +## :wrench: Custom Implementations + +### Custom Variable Loader + +Implement the `VariableLoader` interface to retrieve the last number from your data source: + +```php +use Baraja\VariableGenerator\VariableLoader; + +final class MyCustomVariableLoader implements VariableLoader +{ + public function __construct( + private PDO $pdo, + ) { + } + + public function getCurrent(): ?string + { + $stmt = $this->pdo->query( + 'SELECT order_number FROM orders ORDER BY id DESC LIMIT 1' + ); + $result = $stmt->fetchColumn(); + + return $result !== false ? (string) $result : null; + } +} ``` -The generator is easy to use. +### Custom Format Strategy -Retrieve the next free number (using it without an argument automatically retrieves the last used number based on the variableLoader service). +Implement the `FormatStrategy` interface for custom number formatting: ```php -echo $generator->generate(); +use Baraja\VariableGenerator\Strategy\FormatStrategy; + +final class MonthlyResetStrategy implements FormatStrategy +{ + public function generate(string $last): string + { + $prefix = date('ym'); // e.g., "2401" for January 2024 + + if (str_starts_with($last, $prefix)) { + $sequence = (int) substr($last, 4); + return $prefix . str_pad((string) ($sequence + 1), 4, '0', STR_PAD_LEFT); + } + + return $this->getFirst(); + } + + public function getFirst(): string + { + return date('ym') . '0001'; + } +} ``` -Getting the next available number based on the user's choice: +### Doctrine Entity Integration + +Implement the `OrderEntity` interface on your Doctrine entity for automatic discovery: ```php -echo $generator->generate(21010034); // next will be 21010035 +use Baraja\VariableGenerator\Order\OrderEntity; +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +class Order implements OrderEntity +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private ?int $id = null; + + #[ORM\Column(type: 'string', unique: true)] + private string $number; + + #[ORM\Column(type: 'datetime')] + private \DateTime $insertedDate; + + public function getId(): ?int + { + return $this->id; + } + + public function getNumber(): string + { + return $this->number; + } + + public function getInsertedDate(): \DateTime + { + return $this->insertedDate; + } +} +``` + +> **Note:** If your entity has `getInsertedDate()` method, the `DefaultOrderVariableLoader` will automatically filter orders from the last year when searching for the latest number. + +## :zap: Nette Framework Integration + +Register the extension in your configuration: + +```neon +extensions: + variableGenerator: Baraja\VariableGenerator\VariableGeneratorExtension ``` -Retrieving the last generated number: +Then inject the generator into your services: ```php -echo $generator->getCurrent(); +final class OrderFacade +{ + public function __construct( + private VariableGenerator $generator, + ) { + } + + public function createOrder(array $data): Order + { + $order = new Order(); + $order->setNumber((string) $this->generator->generate()); + // ... save order + + return $order; + } +} ``` -You can always choose your own strategy for generating numbers: +For lazy loading, use the accessor: ```php -$generator->setStrategy(); +public function __construct( + private VariableGeneratorAccessor $generatorAccessor, +) { +} + +public function process(): void +{ + $generator = $this->generatorAccessor->get(); + // ... +} +``` + +## :warning: Important Considerations + +1. **Save immediately** - Always save the generated number to your database immediately after calling `generate()`. The lock protection lasts only 1 second. + +2. **Single entity per interface** - If using automatic Doctrine discovery, only one entity can implement `OrderEntity`. For multiple entities, implement custom `VariableLoader` services. + +3. **No caching** - The `VariableLoader::getCurrent()` method should always fetch real data from the database. Never use cached values. + +4. **Transaction safety** - The generator uses the `baraja-core/lock` library for thread safety. Make sure this dependency is properly installed. + +## :package: Installation + +It's best to use [Composer](https://getcomposer.org) for installation, and you can also find the package on +[Packagist](https://packagist.org/packages/baraja-core/variable-generator) and +[GitHub](https://github.com/baraja-core/variable-generator). + +To install, simply use the command: + +```shell +$ composer require baraja-core/variable-generator ``` -Protection duplicate number generation --------------------------------------- +You can use the package manually by creating an instance of the internal classes, or register a DIC extension to link the services directly to the Nette Framework. + +### Requirements + +- PHP 8.0 or higher +- `baraja-core/lock` package (installed automatically) +- Doctrine ORM (optional, for automatic entity discovery) +- Nette DI (optional, for framework integration) + +## :bust_in_silhouette: Author -This tool automatically protects you from generating a duplicate number. To protect you, an automatic lock (see the `baraja-core/lock` library for more information) is used, which allows only one number to be generated at a time, while competing processes in other threads are suspended in the meantime. +**Jan BarΓ‘Ε‘ek** - [https://baraja.cz](https://baraja.cz) -πŸ“„ License ------------ +## :page_facing_up: License `baraja-core/variable-generator` is licensed under the MIT license. See the [LICENSE](https://github.com/baraja-core/variable-generator/blob/master/LICENSE) file for more details.