Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.env
338 changes: 301 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading