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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- `Simtabi\Lacommerce\Supports\Identifiers`: `sku()`, `orderNumber()` and `ticketNumber()` as a final class
that goes through no registry. It is now the primary way to generate a value without a model.
- Vendor-scoped `Str` macros `Str::simtabiLacommerceSku()`, `Str::simtabiLacommerceOrderNumber()` and
`Str::simtabiLacommerceTicketNumber()`, taking the same arguments as the bare macros they replace.
- Config key `register_legacy_macros` (default `true`). Set it to `false` to stop registering the bare macros.

### Changed

- The `HasSku`, `HasOrderNumber` and `HasTicketNumber` generators call `Identifiers` directly instead of the
bare `Str` macro. A package or application that registers its own `Str::sku()` no longer changes what
your models generate. A custom generator naming another `$strMixin` still has that macro called.

### Deprecated

- The bare macros `Str::sku()`, `Str::orderNumber()` and `Str::ticketNumber()`. `Str`'s macro registry is one
flat map, so any other registration of those names silently replaces them. They still work, with the
same arguments and output, forwarding to the scoped macros, and raise one `E_USER_DEPRECATED` per name per
boot. Earliest removal: 0.2.0.

## [0.1.0] - 2026-10-05

The first tagged release. An entry dated 2022-02-03 used to sit here as `0.1.0`, but no tag was ever
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ class Order extends Model
echo Order::create(['name' => 'Web order'])->order_number; // "ORD-3920571846"
```

To generate a value without a model, call `Identifiers` directly:

```php
use Simtabi\Lacommerce\Supports\Identifiers;

Identifiers::sku('Laravel is Awesome'); // "LAR-8056449213"
Identifiers::orderNumber('INV', '/'); // "INV/3920571846"
Identifiers::ticketNumber('Support'); // "SUP-1749302865"
```

The full walkthrough is in [docs/getting-started.md](docs/getting-started.md); everything else is in the
[Documentation](#documentation) index.

Expand Down
19 changes: 19 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
# Upgrading

## From 0.1.0

### The bare `Str` macros are deprecated

`Str::sku()`, `Str::orderNumber()` and `Str::ticketNumber()` still work and are deprecated. Replace them with
`Simtabi\Lacommerce\Supports\Identifiers`, or rename them to the scoped macros, which take the same
arguments:

| Before | After |
|--------|-------|
| `Str::sku($name)` | `Identifiers::sku($name)` or `Str::simtabiLacommerceSku($name)` |
| `Str::orderNumber(null, '-', 'INV')` | `Identifiers::orderNumber('INV', '-')` or `Str::simtabiLacommerceOrderNumber(null, '-', 'INV')` |
| `Str::ticketNumber($subject)` | `Identifiers::ticketNumber($subject)` or `Str::simtabiLacommerceTicketNumber($subject)` |

`Identifiers` uses the separator you pass, and `-` when you pass none; the macros fall back to the
configured `generator.default.separator`. Once nothing calls the bare names, set `register_legacy_macros` to
`false` in `config/simtabi/lacommerce.php`.

16 changes: 16 additions & 0 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@

return [

/*
|--------------------------------------------------------------------------
| Deprecated bare Str macros
|--------------------------------------------------------------------------
|
| 0.1.0 registered Str::sku(), Str::orderNumber() and Str::ticketNumber().
| Those names sit in Str's flat macro registry, where another package or
| your application can silently replace them. They still work and are
| deprecated: use Simtabi\Lacommerce\Supports\Identifiers, or the scoped
| Str::simtabiLacommerceSku() family. Set this to false to stop registering
| the bare names and leave them free. Earliest removal: 0.2.0.
|
*/

'register_legacy_macros' => true,

/*
|--------------------------------------------------------------------------
| Generator settings
Expand Down
5 changes: 4 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ and read from the `simtabi.lacommerce` config key. See the [Documentation index]

## Structure

Config lives under `generator`: a shared `default` block plus one block per generator (`sku`,
Config lives under `generator`, plus the top-level `register_legacy_macros` switch. `generator` holds a shared `default` block plus one block per generator (`sku`,
`ticket_number`, `order_number`).

```php
return [
'register_legacy_macros' => true, // register the deprecated bare Str macros

'generator' => [
'default' => [
'separator' => '-', // separator between source parts
Expand Down Expand Up @@ -43,6 +45,7 @@ return [

| Key | Purpose |
|-----|---------|
| `register_legacy_macros` | Register the deprecated bare `Str::sku()`, `Str::orderNumber()` and `Str::ticketNumber()` (default `true`). Set `false` to leave those names free; see [Generators](tools/generators.md#str-macros). |
| `generator.default.separator` | Joins multiple source values (default `-`). |
| `generator.default.unique` | Enforce the generated value is unique. |
| `generator.default.generate_on_create` | Generate when the model is created. |
Expand Down
46 changes: 45 additions & 1 deletion docs/tools/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,51 @@ echo $product->sku; // "LAR-8056449213"
The trait registers model-event listeners for the destination field, generated every time you save the model. If you
plan to overwrite values manually, add the destination column to the model's `$fillable`.

`Illuminate\Support\Str::sku()` is also registered, so you can generate a value directly.
## Generating a value directly

`Simtabi\Lacommerce\Supports\Identifiers` generates a value without a model. It is a final class of three
static methods, and it goes through no registry, so nothing another package or your application registers
can change what it returns:

```php
use Simtabi\Lacommerce\Supports\Identifiers;

Identifiers::sku('Laravel is Awesome'); // "LAR-8056449213"
Identifiers::sku('Laravel is Awesome', '_'); // "LAR_8056449213"
Identifiers::orderNumber(); // "ORD-3920571846"
Identifiers::orderNumber('INV', '/'); // "INV/3920571846"
Identifiers::ticketNumber('Support request'); // "SUP-1749302865"
```

| Method | Returns |
|--------|---------|
| `sku(string $source, string $separator = '-')` | First three characters of the studly-cased source, the separator, ten random digits, upper-cased. |
| `orderNumber(?string $prefix = null, string $separator = '-')` | The prefix (`ORD` when empty), the separator, ten random digits, upper-cased. |
| `ticketNumber(string $source, string $separator = '-')` | As `sku()`. |

The separator is used as given. The traits pass the configured `generator.default.separator`. A value from
`Identifiers` is not checked against your table; uniqueness is enforced by the traits' generators.

### `Str` macros

The same three are registered on `Illuminate\Support\Str` under vendor-and-package names. They read the
configured separator when you pass none:

| Macro | Forwards to |
|-------|-------------|
| `Str::simtabiLacommerceSku(string $source, ?string $separator = null)` | `Identifiers::sku()` |
| `Str::simtabiLacommerceOrderNumber(?string $source, ?string $separator = null, ?string $prefix = null)` | `Identifiers::orderNumber($prefix, …)`; `$source` is ignored |
| `Str::simtabiLacommerceTicketNumber(string $source, ?string $separator = null)` | `Identifiers::ticketNumber()` |

Each takes the same arguments as the bare macro it replaces, so migrating is a rename.

> The bare `Str::sku()`, `Str::orderNumber()` and `Str::ticketNumber()` from 0.1.0 are deprecated.
> `Str`'s macros are one flat map keyed by name, so another package or your application registering
> `Str::sku()` silently replaces this one, or this one replaces theirs. They still work, forward to the
> scoped macros with the same arguments, and raise one `E_USER_DEPRECATED` per name per boot. Set
> `register_legacy_macros` to `false` to stop registering them and leave the names free. The earliest release
> that could remove them is 0.2.0. Since this release, the traits no longer call any `Str` macro for the three
> shipped generators, so overriding `Str::sku()` does not change the SKUs your models get.

## Per-model configuration

Expand Down
33 changes: 32 additions & 1 deletion src/Generators/Services/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Simtabi\Lacommerce\Generators\Services\Contracts\GeneratorInterface;
use Simtabi\Lacommerce\Providers\LacommerceServiceProvider;
use Simtabi\Lacommerce\Supports\Identifiers;

class Generator implements Jsonable, Renderable, GeneratorInterface
{
Expand Down Expand Up @@ -89,7 +91,7 @@ protected function getSourceString(): string
protected function generate(string $source, string $separator, bool $unique = false): string
{
// Make
$value = Str::{$this->strMixin}($source, $separator);
$value = $this->makeValue($source, $separator);

// if we are forcing uniques, and it already exists, re-try
if ($unique and $this->exists($value)) {
Expand All @@ -99,6 +101,35 @@ protected function generate(string $source, string $separator, bool $unique = fa
return $value;
}

/**
* Make one candidate value.
*
* The three shipped generators call Identifiers directly rather than the `Str` macro named by
* `$strMixin`: Str's macro registry is a flat, host-owned map, so going through it let any package
* or application that registered its own `Str::sku()` replace what every model generated. A
* subclass naming any other `$strMixin` still has that macro called, as before.
*
* An empty separator falls back to the configured default, as the 0.1.0 macros did.
*
* @param string $source
* @param string $separator
* @return string
*/
protected function makeValue(string $source, string $separator): string
{
$separator = $separator ?: (string) config(
LacommerceServiceProvider::CONFIG_KEY . '.generator.default.separator',
Identifiers::DEFAULT_SEPARATOR,
);

return match ($this->strMixin) {
'sku' => Identifiers::sku($source, $separator),
'orderNumber' => Identifiers::orderNumber(null, $separator),
'ticketNumber' => Identifiers::ticketNumber($source, $separator),
default => Str::{$this->strMixin}($source, $separator),
};
}

/**
* True if the value already exists in the DB.
*
Expand Down
45 changes: 11 additions & 34 deletions src/Providers/LacommerceServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

use Illuminate\Contracts\Foundation\CachesConfiguration;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Simtabi\Lacommerce\Generators\Concerns\OrderNumber\OrderNumberConfigs;
use Simtabi\Lacommerce\Generators\Concerns\Sku\SkuConfigs;
use Simtabi\Lacommerce\Generators\Concerns\TicketNumber\TicketNumberConfigs;
use Simtabi\Lacommerce\Generators\Contracts\SkuGeneratorInterface;
use Simtabi\Lacommerce\Generators\Contracts\TicketNumberGeneratorInterface;
use Simtabi\Lacommerce\Generators\Contracts\OrderNumberGeneratorInterface;
use Simtabi\Lacommerce\Supports\Helpers;
use Simtabi\Lacommerce\Supports\StrMacros;

class LacommerceServiceProvider extends ServiceProvider
{
Expand Down Expand Up @@ -206,39 +205,17 @@ protected function bindConfigs()

}

private function registerStrMacros()
/**
* Register the vendor-scoped Str macros, and the deprecated bare ones unless the host opted out.
*
* The bodies moved to Supports\StrMacros, which forwards to Supports\Identifiers.
*/
private function registerStrMacros(): void
{

$defSeparator = config(self::CONFIG_KEY . '.generator.default.separator', '-');

Str::macro('sku', function (string $source, ?string $separator = null, ?string $prefix = null) use ($defSeparator){
$separator = $separator ?: $defSeparator;
// Clean up the source
$source = Str::studly($source);
// Limit the source
$source = Str::limit($source, 3, '');
// Add prefix @todo

return Helpers::makeRandomString($source, $separator);
});

Str::macro('orderNumber', function (?string $source, ?string $separator = null, ?string $prefix = null) use ($defSeparator){
$separator = $separator ?: $defSeparator;
$source = !empty($prefix) ? $prefix: 'ORD';

return Helpers::makeRandomString($source, $separator);
});

Str::macro('ticketNumber', function (string $source, ?string $separator = null, ?string $prefix = null) use ($defSeparator){
$separator = $separator ?: $defSeparator;
// Clean up the source
$source = Str::studly($source);
// Limit the source
$source = Str::limit($source, 3, '');

return Helpers::makeRandomString($source, $separator);
});

StrMacros::register(
(string) config(self::CONFIG_KEY . '.generator.default.separator', '-'),
(bool) config(self::CONFIG_KEY . '.register_legacy_macros', true),
);
}

/**
Expand Down
55 changes: 55 additions & 0 deletions src/Supports/Identifiers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Simtabi\Lacommerce\Supports;

use Illuminate\Support\Str;

/**
* Generates SKUs, order numbers and ticket numbers without going through a host-owned registry.
*
* This is the package's primary API for generating an identifier directly. The `HasSku`,
* `HasOrderNumber` and `HasTicketNumber` traits use it through their generators, and the
* `Str::simtabiLacommerce*()` macros forward to it. Calling it directly means another package or the
* host application registering a `Str` macro of the same name cannot change what it returns.
*
* Each value is a source part, the separator, and ten random digits, upper-cased:
* `Identifiers::sku('laravel is awesome')` returns e.g. `LAR-8056449213`. Uniqueness against a table
* is the generator's job, not this class's.
*/
final class Identifiers
{
public const DEFAULT_SEPARATOR = '-';

public const DEFAULT_ORDER_PREFIX = 'ORD';

/**
* A SKU from the first three characters of the studly-cased source.
*/
public static function sku(string $source, string $separator = self::DEFAULT_SEPARATOR): string
{
return Helpers::makeRandomString(self::sourcePart($source), $separator);
}

/**
* An order number: the prefix (`ORD` when empty), the separator and ten random digits.
*/
public static function orderNumber(?string $prefix = null, string $separator = self::DEFAULT_SEPARATOR): string
{
return Helpers::makeRandomString(! empty($prefix) ? $prefix : self::DEFAULT_ORDER_PREFIX, $separator);
}

/**
* A ticket number from the first three characters of the studly-cased source.
*/
public static function ticketNumber(string $source, string $separator = self::DEFAULT_SEPARATOR): string
{
return Helpers::makeRandomString(self::sourcePart($source), $separator);
}

private static function sourcePart(string $source): string
{
return Str::limit(Str::studly($source), 3, '');
}
}
Loading
Loading