diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel.yml
index 496599f..c92f132 100644
--- a/.github/workflows/laravel.yml
+++ b/.github/workflows/laravel.yml
@@ -1,8 +1,7 @@
name: Checks
-# lacommerce has no test suite yet, so there is nothing for PHPUnit to run. This gates what can
-# be checked without one: a valid composer.json, and every PHP file parsing. When tests land,
-# add a Testbench job here and it becomes a required check through the org's ruleset script.
+# Fast checks that need no dependencies: a valid composer.json, and every PHP file parsing.
+# The Testbench suite runs in tests.yml.
on:
pull_request:
branches: [master]
@@ -30,7 +29,7 @@ jobs:
tools: composer
- name: Validate composer.json
- run: composer validate --no-check-publish --no-check-lock
+ run: composer validate --strict --no-check-publish --no-check-lock
- name: Check PHP syntax
- run: find src config database -name '*.php' -print0 | xargs -0 -n1 php -l
+ run: find src config tests -name '*.php' -print0 | xargs -0 -n1 php -l
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..d767189
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,42 @@
+name: Tests
+
+on:
+ pull_request:
+ branches: [master]
+ # No path filter: this workflow is meant to gate merges as a required status check, and a
+ # required check that never reports (a docs-only pull request) blocks the merge for good.
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ phpunit:
+ name: PHP ${{ matrix.php }} · ${{ matrix.stability }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['8.4', '8.5']
+ stability: [prefer-lowest, prefer-stable]
+ steps:
+ - uses: actions/checkout@v7
+
+ - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: pdo_sqlite, sqlite3, mbstring
+ coverage: none
+ tools: composer
+
+ # No lock file is tracked, so this resolves fresh against composer.json every run.
+ - name: Install dependencies
+ run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress
+
+ - name: Run tests
+ run: vendor/bin/phpunit --colors=never
diff --git a/.gitignore b/.gitignore
index e43b0f9..4036744 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
.DS_Store
+/vendor/
+composer.lock
+/build/
+/.phpunit.cache/
+.phpunit.result.cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe57bc0..ed3cc4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,42 @@
# Changelog
-All notable changes to `lacommerce` will be documented in this file
+All notable changes to `simtabi/lacommerce` are documented in this file.
-## 0.1.0 - 2022-02-03
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-- Initial release
+## [Unreleased]
+
+Preparing the first tagged release. An entry dated 2022-02-03 used to sit here as `0.1.0`, but no tag was
+ever cut for it and the package was never published.
+
+### Added
+
+- A Testbench suite covering the `HasSku`, `HasOrderNumber` and `HasTicketNumber` traits and the service
+ provider, run in CI on PHP 8.4 and 8.5 against lowest and stable dependencies.
+- Vendor-scoped config key `simtabi.lacommerce`, published to `config/simtabi/lacommerce.php` with the tag
+ `simtabi::lacommerce-config`.
+- `composer.json` now requires the `illuminate/*` components the source uses.
+
+### Changed
+
+- Requires PHP `^8.4.1 || ^8.5` and Laravel 12 or 13 (was PHP `^8.0`, no Laravel constraint).
+- Package metadata, badges and documentation links point at the `simtabi` org.
+
+### Deprecated
+
+- The bare config key `lacommerce` and publish tag `lacommerce:config`. Both still work: a published
+ `config/lacommerce.php` is still read (the scoped file wins when both exist), `config('lacommerce.*')`
+ still returns the merged configuration, and the bare tag still publishes `config/lacommerce.php`.
+ Earliest removal: 0.2.0.
+
+### Fixed
+
+- The traits failed on every model under current Laravel: they called `static::observe()` while the model
+ was booting, which throws. They now register the observer's handlers directly.
+- The provider loaded the package's `database/migrations` into every host application. That directory only
+ held a test fixture creating a `dummy_models` table; the fixture now lives under `tests/` and nothing is
+ loaded.
+- The config was merged twice; it is merged once.
+- View, translation and asset loaders and publish tags pointed at directories the package does not ship;
+ they are now registered only when the directory exists.
diff --git a/README.md b/README.md
index 32a4982..57f78f6 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,77 @@
# simtabi/lacommerce
-[](https://packagist.org/packages/simtabi/lacommerce)
-[](https://github.com/laranail/lacommerce/actions/workflows/laravel.yml)
+[](https://github.com/simtabi/lacommerce/actions/workflows/tests.yml)
[](LICENSE)
+Two badges, not four: the package is not on Packagist yet, so there is no registry version to show, and
+there is no static-analysis workflow.
+
> Handy helper generators for Laravel e-commerce projects — auto-generate unique SKUs, order numbers, and ticket numbers on your Eloquent models via the `HasSku`, `HasOrderNumber`, and `HasTicketNumber` traits, each configurable globally or per model and fully replaceable.
-Compatible with PHP `^8.0`.
+Compatible with PHP `^8.4.1 || ^8.5` and Laravel 12 or 13.
## Install
```bash
composer require simtabi/lacommerce
-php artisan vendor:publish --tag=lacommerce:config
```
-## Documentation
+## Quick start guide and usage
+
+### Getting started
+
+The service provider is auto-discovered and the defaults work without a config file. Give each model the
+destination column its trait writes to (`sku`, `order_number` or `ticket_number`) in your own migration.
+To change the defaults, publish the config to `config/simtabi/lacommerce.php`:
+
+```bash
+php artisan vendor:publish --tag=simtabi::lacommerce-config
+```
+
+### Usage
+
+```php
+use Illuminate\Database\Eloquent\Model;
+use Simtabi\Lacommerce\Traits\HasSku;
+
+class Product extends Model
+{
+ use HasSku;
+}
+
+$product = Product::create(['name' => 'Laravel is Awesome']);
+
+echo $product->sku; // "LAR-8056449213"
+```
+
+```php
+use Illuminate\Database\Eloquent\Model;
+use Simtabi\Lacommerce\Traits\HasOrderNumber;
+
+class Order extends Model
+{
+ use HasOrderNumber;
+}
+
+echo Order::create(['name' => 'Web order'])->order_number; // "ORD-3920571846"
+```
+
+The full walkthrough is in [docs/getting-started.md](docs/getting-started.md); everything else is in the
+[Documentation](#documentation) index.
+
+## Documentation
-Full documentation is at **[opensource.simtabi.com/documentation/laranail/lacommerce](https://opensource.simtabi.com/documentation/laranail/lacommerce/)** — installation, getting started, each generator trait, and configuration.
+Full documentation is at **[opensource.simtabi.com/documentation/simtabi/lacommerce](https://opensource.simtabi.com/documentation/simtabi/lacommerce/)** — installation, getting started, each generator trait, and configuration.
## Credits
The SKU-generation approach builds on [Cyrill Kalita / binary-cats](https://github.com/binary-cats)' work,
-plus [all contributors](https://github.com/laranail/lacommerce/graphs/contributors).
+plus [all contributors](https://github.com/simtabi/lacommerce/graphs/contributors).
## Contributing & security
Issues and PRs are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Report vulnerabilities per
-[SECURITY.md](SECURITY.md) (opensource@simtabi.com); participation follows the [Code of Conduct](CODE_OF_CONDUCT.md).
+[SECURITY.md](SECURITY.md) (security@simtabi.com); participation follows the [Code of Conduct](CODE_OF_CONDUCT.md).
## License
diff --git a/composer.json b/composer.json
index b41d15b..fdd7e39 100644
--- a/composer.json
+++ b/composer.json
@@ -7,9 +7,11 @@
"laravel",
"sku"
],
- "homepage": "https://opensource.simtabi.com/products/laranail/lacommerce",
+ "homepage": "https://opensource.simtabi.com/products/simtabi/lacommerce",
"support": {
- "docs": "https://opensource.simtabi.com/documentation/laranail/lacommerce/"
+ "issues": "https://github.com/simtabi/lacommerce/issues",
+ "source": "https://github.com/simtabi/lacommerce",
+ "docs": "https://opensource.simtabi.com/documentation/simtabi/lacommerce/"
},
"license": "MIT",
"authors": [
@@ -21,11 +23,15 @@
}
],
"require": {
- "php": "^8.0"
+ "php": "^8.4.1 || ^8.5",
+ "illuminate/contracts": "^12.0 || ^13.0",
+ "illuminate/database": "^12.0 || ^13.0",
+ "illuminate/http": "^12.0 || ^13.0",
+ "illuminate/support": "^12.0 || ^13.0"
},
"require-dev": {
- "orchestra/testbench": "^6.24.1",
- "phpunit/phpunit": "^9.5.19"
+ "orchestra/testbench": "^10.6 || ^11.0",
+ "phpunit/phpunit": "^11.5 || ^12.0"
},
"autoload": {
"psr-4": {
@@ -46,5 +52,10 @@
"Simtabi\\Lacommerce\\Providers\\LacommerceServiceProvider"
]
}
- }
+ },
+ "scripts": {
+ "test": "phpunit"
+ },
+ "minimum-stability": "stable",
+ "prefer-stable": true
}
diff --git a/docs/architecture.md b/docs/architecture.md
index 4c40f99..d999e4b 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -4,8 +4,8 @@ How a generated value is produced on save. See the [Documentation index](../READ
## The moving parts
-- **Traits** (`Traits/HasSku`, `HasOrderNumber`, `HasTicketNumber`) — added to a model; each boots a model
- observer for its destination column.
+- **Traits** (`Traits/HasSku`, `HasOrderNumber`, `HasTicketNumber`) — added to a model; each registers its
+ observer's `creating` and `updating` handlers for its destination column.
- **Observer** (`Generators/Services/Observer`) — hooks the model's create/update events and asks the
generator for a value.
- **Generator** (`Generators/Services/Generator` + per-type `Concerns/*Generator`) — builds the value from
diff --git a/docs/configuration.md b/docs/configuration.md
index 493833f..a2d3a60 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1,7 +1,7 @@
# Configuration
-Every key in `config/lacommerce.php`, published with `vendor:publish --tag=lacommerce:config`. See the
-[Documentation index](../README.md#documentation).
+Every key in `config/simtabi/lacommerce.php`, published with `vendor:publish --tag=simtabi::lacommerce-config`
+and read from the `simtabi.lacommerce` config key. See the [Documentation index](../README.md#documentation).
## Structure
@@ -19,21 +19,21 @@ return [
],
'sku' => [
- 'generator' => SkuGenerator::class, // must implement GeneratorInterface
- 'source' => 'name', // source column(s)
- 'destination' => 'sku', // destination column
+ 'generator' => SkuGenerator::class, // must implement GeneratorInterface
+ 'source_column' => 'name', // source column(s)
+ 'destination_column' => 'sku', // destination column
],
'ticket_number' => [
- 'generator' => TicketNumberGenerator::class,
- 'source' => 'name',
- 'destination' => 'ticket_number',
+ 'generator' => TicketNumberGenerator::class,
+ 'source_column' => 'name',
+ 'destination_column' => 'ticket_number',
],
'order_number' => [
- 'generator' => OrderNumberGenerator::class,
- 'source' => 'name',
- 'destination' => 'order_number',
+ 'generator' => OrderNumberGenerator::class,
+ 'source_column' => 'name',
+ 'destination_column' => 'order_number',
],
],
];
@@ -48,11 +48,28 @@ return [
| `generator.default.generate_on_create` | Generate when the model is created. |
| `generator.default.refresh_on_update` | Regenerate when the model is updated. |
| `generator..generator` | The generator class (must implement its `GeneratorInterface`). |
-| `generator..source` | Source column(s) the value is derived from. |
-| `generator..destination` | Column the generated value is written to. |
+| `generator..source_column` | Source column(s) the value is derived from. |
+| `generator..destination_column` | Column the generated value is written to. |
Override any of these per model via the trait's config method — see [Generators](tools/generators.md).
+The published file replaces the defaults block by block, not key by key: a top-level key you keep
+(`generator`) must carry every nested key the package reads, so publish the whole file and edit it.
+
+## The deprecated `lacommerce` key
+
+Before 0.1.0 the config lived under the bare key `lacommerce`, published to `config/lacommerce.php` with
+`--tag=lacommerce:config`. Both still work and are deprecated; the earliest release that could remove them
+is 0.2.0.
+
+- A published `config/lacommerce.php` is still read. When `config/simtabi/lacommerce.php` also exists, it
+ wins.
+- `config('lacommerce.*')` still returns the merged configuration.
+- `--tag=lacommerce:config` still publishes `config/lacommerce.php`.
+
+To migrate, move the file to `config/simtabi/lacommerce.php` and replace any `config('lacommerce.…')` reads
+with `config('simtabi.lacommerce.…')`.
+
---
[← Docs index](../README.md#documentation)
diff --git a/docs/getting-started.md b/docs/getting-started.md
index a4f1551..be0231a 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -6,7 +6,7 @@ Generate your first SKU in three steps. See the [Documentation index](../README.
```bash
composer require simtabi/lacommerce
-php artisan vendor:publish --tag=lacommerce:config
+php artisan vendor:publish --tag=simtabi::lacommerce-config
```
See [Installation](installation.md).
@@ -32,7 +32,7 @@ $product = new Product();
$product->name = 'Laravel is Awesome';
$product->save();
-echo $product->sku; // "LAR-80564492"
+echo $product->sku; // "LAR-8056449213"
```
Order numbers and ticket numbers work the same way via `HasOrderNumber` / `HasTicketNumber`.
@@ -40,7 +40,7 @@ Order numbers and ticket numbers work the same way via `HasOrderNumber` / `HasTi
## Next steps
- [Generators](tools/generators.md) — all three generators, per-model config, custom generators.
-- [Configuration](configuration.md) — every `config/lacommerce.php` key.
+- [Configuration](configuration.md) — every `config/simtabi/lacommerce.php` key.
---
diff --git a/docs/installation.md b/docs/installation.md
index 71636b7..f68a45b 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -4,8 +4,8 @@ Install `simtabi/lacommerce` and publish its config. See the [Documentation inde
## Requirements
-- PHP `^8.0`
-- Laravel (the `LacommerceServiceProvider` is auto-discovered)
+- PHP `^8.4.1 || ^8.5`
+- Laravel 12 or 13 (the `LacommerceServiceProvider` is auto-discovered)
## Install
@@ -13,15 +13,16 @@ Install `simtabi/lacommerce` and publish its config. See the [Documentation inde
composer require simtabi/lacommerce
```
-The service provider registers itself. Publish the config (and optionally assets/views):
+The service provider registers itself. Publish the config if you want to change the defaults:
```bash
-php artisan vendor:publish --tag=lacommerce:config
-php artisan vendor:publish --tag=lacommerce:assets
-php artisan vendor:publish --tag=lacommerce:views
+php artisan vendor:publish --tag=simtabi::lacommerce-config
```
-This publishes `config/lacommerce.php` — see [Configuration](configuration.md).
+This publishes `config/simtabi/lacommerce.php` — see [Configuration](configuration.md). The package ships
+no views, translations or public assets, so there is nothing else to publish.
+
+The package runs no migrations of its own: add the destination column to your own table.
> Your model needs the destination column (e.g. `sku`) on its table. If you overwrite generated values
> manually, add that column to the model's `$fillable`.
diff --git a/docs/tools/generators.md b/docs/tools/generators.md
index 802a10b..71493d3 100644
--- a/docs/tools/generators.md
+++ b/docs/tools/generators.md
@@ -27,10 +27,10 @@ $product = new Product();
$product->name = 'Laravel is Awesome';
$product->save();
-echo $product->sku; // "LAR-80564492"
+echo $product->sku; // "LAR-8056449213"
```
-The trait registers an observer for the destination field, generated every time you save the model. If you
+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.
@@ -74,14 +74,14 @@ class CustomSkuGenerator extends SkuGenerator
{
protected function getSourceString(): string
{
- $source = $this->configs->source;
+ $source = $this->modelConfig->sourceColumn;
$fields = array_filter($this->model->only($source));
if (empty($fields)) {
return 'some-random-value-logic';
}
- return implode($this->configs->separator, $fields);
+ return implode($this->modelConfig->separator, $fields);
}
}
```
@@ -92,7 +92,9 @@ Then point the config at it:
'generator' => \App\Components\SkuGenerator\CustomSkuGenerator::class,
```
-A custom generator must implement `Simtabi\Lacommerce\SKU\Contracts\SkuGenerator`.
+A custom generator must implement `Simtabi\Lacommerce\Generators\Contracts\SkuGeneratorInterface` (or the
+`OrderNumberGeneratorInterface` / `TicketNumberGeneratorInterface` beside it); extending the shipped generator
+does that for you.
## About SKUs
diff --git a/phpunit.xml b/phpunit.xml
index aa3d4b0..26f0ade 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,29 +1,22 @@
-
+ failOnRisky="true"
+ failOnWarning="true">
-
+ tests
-
-
- src/
-
-
-
-
-
-
-
-
-
+
+
+ src
+
+
+
+
+
diff --git a/src/Providers/LacommerceServiceProvider.php b/src/Providers/LacommerceServiceProvider.php
index 483ec63..8c26901 100644
--- a/src/Providers/LacommerceServiceProvider.php
+++ b/src/Providers/LacommerceServiceProvider.php
@@ -2,6 +2,7 @@
namespace Simtabi\Lacommerce\Providers;
+use Illuminate\Contracts\Foundation\CachesConfiguration;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Simtabi\Lacommerce\Generators\Concerns\OrderNumber\OrderNumberConfigs;
@@ -15,7 +16,37 @@
class LacommerceServiceProvider extends ServiceProvider
{
- private string $packageName = 'lacommerce';
+ /**
+ * Vendor-scoped config key. Published to config/simtabi/lacommerce.php.
+ */
+ public const CONFIG_KEY = 'simtabi.lacommerce';
+
+ /**
+ * Vendor-scoped publish tag for the config file.
+ */
+ public const CONFIG_TAG = 'simtabi::lacommerce-config';
+
+ /**
+ * Vendor-scoped view and translation namespace.
+ */
+ public const VIEW_NAMESPACE = 'simtabi/lacommerce';
+
+ /**
+ * Bare config key used before 0.1.0.
+ *
+ * @deprecated Since 0.1.0; use CONFIG_KEY (`simtabi.lacommerce`). Still populated and a
+ * published config/lacommerce.php is still honoured. Earliest removal: 0.2.0.
+ */
+ public const LEGACY_CONFIG_KEY = 'lacommerce';
+
+ /**
+ * Bare publish tag used before 0.1.0.
+ *
+ * @deprecated Since 0.1.0; use CONFIG_TAG (`simtabi::lacommerce-config`). Still registered and
+ * still publishes config/lacommerce.php. Earliest removal: 0.2.0.
+ */
+ public const LEGACY_CONFIG_TAG = 'lacommerce:config';
+
private const PACKAGE_PATH = __DIR__ . '/../../';
/**
@@ -25,11 +56,41 @@ class LacommerceServiceProvider extends ServiceProvider
*/
public function register()
{
- $this->mergeConfigFrom(self::PACKAGE_PATH . 'config/config.php', $this->packageName);
- $this->loadTranslationsFrom(self::PACKAGE_PATH . "resources/lang/", $this->packageName);
- $this->loadMigrationsFrom(self::PACKAGE_PATH.'database/migrations');
- $this->loadViewsFrom(self::PACKAGE_PATH . "resources/views", $this->packageName);
- $this->mergeConfigFrom(self::PACKAGE_PATH . "config/config.php", $this->packageName);
+ $this->registerConfig();
+
+ if (is_dir($path = self::PACKAGE_PATH . 'resources/lang')) {
+ $this->loadTranslationsFrom($path, self::VIEW_NAMESPACE);
+ }
+
+ if (is_dir($path = self::PACKAGE_PATH . 'resources/views')) {
+ $this->loadViewsFrom($path, self::VIEW_NAMESPACE);
+ }
+ }
+
+ /**
+ * Merge the package defaults into the vendor-scoped key, honouring a config file published
+ * under the deprecated bare key, and mirror the result to that bare key so existing
+ * `config('lacommerce.*')` reads keep working.
+ *
+ * Precedence, lowest to highest: package defaults, config/lacommerce.php (deprecated),
+ * config/simtabi/lacommerce.php. The merge is shallow, as mergeConfigFrom() is.
+ */
+ private function registerConfig(): void
+ {
+ if ($this->app instanceof CachesConfiguration && $this->app->configurationIsCached()) {
+ return;
+ }
+
+ $config = $this->app->make('config');
+
+ $merged = array_merge(
+ require self::PACKAGE_PATH . 'config/config.php',
+ (array) $config->get(self::LEGACY_CONFIG_KEY, []),
+ (array) $config->get(self::CONFIG_KEY, []),
+ );
+
+ $config->set(self::CONFIG_KEY, $merged);
+ $config->set(self::LEGACY_CONFIG_KEY, $merged);
}
/**
@@ -55,21 +116,31 @@ private function registerConsoles(): static
{
if ($this->app->runningInConsole())
{
+ // Laravel keys a provider's publishable paths by source path, so two tags naming the
+ // same source string would share one target and the later one would win. The two
+ // spellings below name the same file but are distinct keys.
$this->publishes([
- self::PACKAGE_PATH . "config/config.php" => config_path("{$this->packageName}.php"),
- ], "{$this->packageName}:config");
-
- $this->publishes([
- self::PACKAGE_PATH . "public" => public_path("vendor/{$this->packageName}"),
- ], "{$this->packageName}:assets");
-
- $this->publishes([
- self::PACKAGE_PATH . "resources/views" => resource_path("views/vendor/{$this->packageName}"),
- ], "{$this->packageName}:views");
+ dirname(__DIR__, 2) . '/config/config.php' => config_path('simtabi/lacommerce.php'),
+ ], self::CONFIG_TAG);
+ // Deprecated bare tag, kept so existing install instructions still work.
$this->publishes([
- self::PACKAGE_PATH . "resources/lang" => $this->app->langPath("vendor/{$this->packageName}"),
- ], "{$this->packageName}:translations");
+ self::PACKAGE_PATH . 'config/config.php' => config_path(self::LEGACY_CONFIG_KEY . '.php'),
+ ], self::LEGACY_CONFIG_TAG);
+
+ // The package ships no public assets, views or translations today. The tags are
+ // registered only when the directory exists, so they never point at nothing.
+ $optional = [
+ 'public' => [public_path('vendor/simtabi/lacommerce'), 'simtabi::lacommerce-assets'],
+ 'resources/views' => [resource_path('views/vendor/simtabi/lacommerce'), 'simtabi::lacommerce-views'],
+ 'resources/lang' => [$this->app->langPath('vendor/simtabi/lacommerce'), 'simtabi::lacommerce-translations'],
+ ];
+
+ foreach ($optional as $source => [$target, $tag]) {
+ if (is_dir(self::PACKAGE_PATH . $source)) {
+ $this->publishes([self::PACKAGE_PATH . $source => $target], $tag);
+ }
+ }
}
return $this;
@@ -108,7 +179,7 @@ private function getConfig(): array
{
$config = $this->app->make('config');
- return $config->get('lacommerce.generator', []);
+ return $config->get(self::CONFIG_KEY . '.generator', []);
}
/**
@@ -138,7 +209,7 @@ protected function bindConfigs()
private function registerStrMacros()
{
- $defSeparator = config('lacommerce.generator.default.separator', '-');
+ $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;
diff --git a/src/Traits/HasOrderNumber.php b/src/Traits/HasOrderNumber.php
index 363fdde..dbad0e0 100644
--- a/src/Traits/HasOrderNumber.php
+++ b/src/Traits/HasOrderNumber.php
@@ -2,6 +2,7 @@
namespace Simtabi\Lacommerce\Traits;
+use Illuminate\Database\Eloquent\Model;
use Simtabi\Lacommerce\Generators\Concerns\OrderNumber\OrderNumberConfigs;
use Simtabi\Lacommerce\Generators\Concerns\OrderNumber\OrderNumberObserver;
@@ -9,13 +10,17 @@ trait HasOrderNumber
{
/**
- * Boot the trait by adding observers.
+ * Boot the trait by registering the observer's model events.
+ *
+ * The listeners are registered directly rather than through static::observe(), which
+ * instantiates the model and therefore throws while the model is still booting.
*
* @return void
*/
public static function bootHasOrderNumber()
{
- static::observe(OrderNumberObserver::class);
+ static::creating(static fn (Model $model) => resolve(OrderNumberObserver::class)->creating($model));
+ static::updating(static fn (Model $model) => resolve(OrderNumberObserver::class)->updating($model));
}
/**
diff --git a/src/Traits/HasSku.php b/src/Traits/HasSku.php
index d9f6fa2..7718305 100644
--- a/src/Traits/HasSku.php
+++ b/src/Traits/HasSku.php
@@ -2,6 +2,7 @@
namespace Simtabi\Lacommerce\Traits;
+use Illuminate\Database\Eloquent\Model;
use Simtabi\Lacommerce\Generators\Concerns\Sku\SkuConfigs;
use Simtabi\Lacommerce\Generators\Concerns\Sku\SkuObserver;
@@ -9,13 +10,17 @@ trait HasSku
{
/**
- * Boot the trait by adding observers.
+ * Boot the trait by registering the observer's model events.
+ *
+ * The listeners are registered directly rather than through static::observe(), which
+ * instantiates the model and therefore throws while the model is still booting.
*
* @return void
*/
public static function bootHasSku()
{
- static::observe(SkuObserver::class);
+ static::creating(static fn (Model $model) => resolve(SkuObserver::class)->creating($model));
+ static::updating(static fn (Model $model) => resolve(SkuObserver::class)->updating($model));
}
/**
diff --git a/src/Traits/HasTicketNumber.php b/src/Traits/HasTicketNumber.php
index c1f81ea..1b8adf4 100644
--- a/src/Traits/HasTicketNumber.php
+++ b/src/Traits/HasTicketNumber.php
@@ -2,6 +2,7 @@
namespace Simtabi\Lacommerce\Traits;
+use Illuminate\Database\Eloquent\Model;
use Simtabi\Lacommerce\Generators\Concerns\TicketNumber\TicketNumberConfigs;
use Simtabi\Lacommerce\Generators\Concerns\TicketNumber\TicketNumberObserver;
@@ -9,13 +10,17 @@ trait HasTicketNumber
{
/**
- * Boot the trait by adding observers.
+ * Boot the trait by registering the observer's model events.
+ *
+ * The listeners are registered directly rather than through static::observe(), which
+ * instantiates the model and therefore throws while the model is still booting.
*
* @return void
*/
public static function bootHasTicketNumber()
{
- static::observe(TicketNumberObserver::class);
+ static::creating(static fn (Model $model) => resolve(TicketNumberObserver::class)->creating($model));
+ static::updating(static fn (Model $model) => resolve(TicketNumberObserver::class)->updating($model));
}
/**
diff --git a/tests/Feature/GenerateOnCreateDisabledTest.php b/tests/Feature/GenerateOnCreateDisabledTest.php
new file mode 100644
index 0000000..b3fa59f
--- /dev/null
+++ b/tests/Feature/GenerateOnCreateDisabledTest.php
@@ -0,0 +1,33 @@
+ $config];
+ }
+
+ #[Test]
+ public function it_does_not_generate_when_disabled(): void
+ {
+ $product = DummyProduct::create(['name' => 'Blue shirt']);
+
+ $this->assertNull($product->fresh()->getRawOriginal('sku'));
+
+ $product->update(['name' => 'Red hat']);
+
+ $this->assertNull($product->fresh()->getRawOriginal('sku'));
+ }
+}
diff --git a/tests/Feature/HasOrderNumberTest.php b/tests/Feature/HasOrderNumberTest.php
new file mode 100644
index 0000000..a7f2c96
--- /dev/null
+++ b/tests/Feature/HasOrderNumberTest.php
@@ -0,0 +1,42 @@
+ 'Blue shirt']);
+
+ $this->assertMatchesRegularExpression('/^ORD-\d{10}$/', $order->order_number);
+ $this->assertNull($order->sku);
+ $this->assertNull($order->ticket_number);
+ }
+
+ #[Test]
+ public function it_keeps_an_order_number_that_was_set_by_hand_on_update(): void
+ {
+ $order = DummyOrder::create(['name' => 'Blue shirt']);
+
+ $order->update(['name' => 'Red hat', 'order_number' => 'ORD-MANUAL']);
+
+ $this->assertSame('ORD-MANUAL', $order->fresh()->order_number);
+ }
+
+ #[Test]
+ public function it_exposes_its_configs(): void
+ {
+ $order = new DummyOrder();
+
+ $this->assertInstanceOf(OrderNumberConfigs::class, $order->orderNumberConfigs());
+ $this->assertSame('order_number', $order->orderNumberConfig('destinationColumn'));
+ }
+}
diff --git a/tests/Feature/HasSkuTest.php b/tests/Feature/HasSkuTest.php
new file mode 100644
index 0000000..ab3ec29
--- /dev/null
+++ b/tests/Feature/HasSkuTest.php
@@ -0,0 +1,63 @@
+ 'Blue shirt']);
+
+ $this->assertMatchesRegularExpression('/^BLU-\d{10}$/', $product->sku);
+ $this->assertSame($product->sku, $product->fresh()->sku);
+ }
+
+ #[Test]
+ public function it_refreshes_the_sku_when_the_source_column_changes(): void
+ {
+ $product = DummyProduct::create(['name' => 'Blue shirt']);
+
+ $product->update(['name' => 'Red hat']);
+
+ $this->assertMatchesRegularExpression('/^RED-\d{10}$/', $product->sku);
+ }
+
+ #[Test]
+ public function it_keeps_a_sku_that_was_set_by_hand_on_update(): void
+ {
+ $product = DummyProduct::create(['name' => 'Blue shirt']);
+
+ $product->update(['name' => 'Red hat', 'sku' => 'MANUAL-1']);
+
+ $this->assertSame('MANUAL-1', $product->fresh()->sku);
+ }
+
+ #[Test]
+ public function it_leaves_the_sku_alone_when_an_unrelated_column_changes(): void
+ {
+ $product = DummyProduct::create(['name' => 'Blue shirt']);
+ $sku = $product->sku;
+
+ $product->touch();
+
+ $this->assertSame($sku, $product->fresh()->sku);
+ }
+
+ #[Test]
+ public function it_exposes_its_configs(): void
+ {
+ $product = new DummyProduct();
+
+ $this->assertInstanceOf(SkuConfigs::class, $product->skuConfigs());
+ $this->assertSame('sku', $product->skuConfig('destinationColumn'));
+ $this->assertSame(['name'], $product->skuConfig('sourceColumn'));
+ }
+}
diff --git a/tests/Feature/HasTicketNumberTest.php b/tests/Feature/HasTicketNumberTest.php
new file mode 100644
index 0000000..e7524ad
--- /dev/null
+++ b/tests/Feature/HasTicketNumberTest.php
@@ -0,0 +1,41 @@
+ 'Printer jam']);
+
+ $this->assertMatchesRegularExpression('/^PRI-\d{10}$/', $ticket->ticket_number);
+ $this->assertNull($ticket->sku);
+ }
+
+ #[Test]
+ public function it_refreshes_the_ticket_number_when_the_source_column_changes(): void
+ {
+ $ticket = DummyTicket::create(['name' => 'Printer jam']);
+
+ $ticket->update(['name' => 'Network down']);
+
+ $this->assertMatchesRegularExpression('/^NET-\d{10}$/', $ticket->ticket_number);
+ }
+
+ #[Test]
+ public function it_exposes_its_configs(): void
+ {
+ $ticket = new DummyTicket();
+
+ $this->assertInstanceOf(TicketNumberConfigs::class, $ticket->ticketNumberConfigs());
+ $this->assertSame('ticket_number', $ticket->ticketNumberConfig('destinationColumn'));
+ }
+}
diff --git a/tests/Feature/LegacyConfigTest.php b/tests/Feature/LegacyConfigTest.php
new file mode 100644
index 0000000..48b88ad
--- /dev/null
+++ b/tests/Feature/LegacyConfigTest.php
@@ -0,0 +1,34 @@
+ $legacy];
+ }
+
+ #[Test]
+ public function it_honours_a_config_published_under_the_bare_key(): void
+ {
+ $this->assertSame('_', config('simtabi.lacommerce.generator.default.separator'));
+ $this->assertSame('_', config('lacommerce.generator.default.separator'));
+
+ $product = DummyProduct::create(['name' => 'Blue shirt']);
+
+ $this->assertMatchesRegularExpression('/^BLU_\d{10}$/', $product->sku);
+ }
+}
diff --git a/tests/Feature/ScopedConfigPrecedenceTest.php b/tests/Feature/ScopedConfigPrecedenceTest.php
new file mode 100644
index 0000000..93cf07e
--- /dev/null
+++ b/tests/Feature/ScopedConfigPrecedenceTest.php
@@ -0,0 +1,32 @@
+ $legacy, 'simtabi.lacommerce' => $scoped];
+ }
+
+ #[Test]
+ public function the_vendor_scoped_config_takes_precedence_over_the_bare_one(): void
+ {
+ $this->assertSame('/', config('simtabi.lacommerce.generator.default.separator'));
+ $this->assertSame('/', config('lacommerce.generator.default.separator'));
+ }
+}
diff --git a/tests/Feature/ServiceProviderTest.php b/tests/Feature/ServiceProviderTest.php
new file mode 100644
index 0000000..d873636
--- /dev/null
+++ b/tests/Feature/ServiceProviderTest.php
@@ -0,0 +1,83 @@
+assertSame('-', config('simtabi.lacommerce.generator.default.separator'));
+ $this->assertSame('sku', config('simtabi.lacommerce.generator.sku.destination_column'));
+ $this->assertSame('order_number', config('simtabi.lacommerce.generator.order_number.destination_column'));
+ $this->assertSame('ticket_number', config('simtabi.lacommerce.generator.ticket_number.destination_column'));
+ }
+
+ #[Test]
+ public function it_mirrors_the_config_to_the_deprecated_bare_key(): void
+ {
+ $this->assertSame(config('simtabi.lacommerce'), config('lacommerce'));
+ }
+
+ #[Test]
+ public function it_registers_the_vendor_scoped_publish_tag(): void
+ {
+ $paths = ServiceProvider::pathsToPublish(LacommerceServiceProvider::class, 'simtabi::lacommerce-config');
+
+ $this->assertCount(1, $paths);
+ $this->assertSame(config_path('simtabi/lacommerce.php'), array_values($paths)[0]);
+ }
+
+ #[Test]
+ public function it_keeps_the_deprecated_bare_publish_tag(): void
+ {
+ $paths = ServiceProvider::pathsToPublish(LacommerceServiceProvider::class, 'lacommerce:config');
+
+ $this->assertCount(1, $paths);
+ $this->assertSame(config_path('lacommerce.php'), array_values($paths)[0]);
+ }
+
+ #[Test]
+ public function it_registers_no_tag_for_directories_the_package_does_not_ship(): void
+ {
+ $groups = ServiceProvider::publishableGroups();
+
+ foreach (['assets', 'views', 'translations'] as $suffix) {
+ $this->assertNotContains("lacommerce:{$suffix}", $groups);
+ $this->assertNotContains("simtabi::lacommerce-{$suffix}", $groups);
+ }
+ }
+
+ #[Test]
+ public function it_does_not_load_migrations_into_the_host_application(): void
+ {
+ $this->assertSame([], $this->app['migrator']->paths());
+ }
+
+ #[Test]
+ public function it_binds_the_configs_for_each_generator(): void
+ {
+ $this->assertSame('sku', $this->app->make(SkuConfigs::class)->getDestinationColumn());
+ $this->assertSame('order_number', $this->app->make(OrderNumberConfigs::class)->getDestinationColumn());
+ $this->assertSame('ticket_number', $this->app->make(TicketNumberConfigs::class)->getDestinationColumn());
+ }
+
+ #[Test]
+ public function it_registers_the_str_macros(): void
+ {
+ $this->assertTrue(Str::hasMacro('sku'));
+ $this->assertTrue(Str::hasMacro('orderNumber'));
+ $this->assertTrue(Str::hasMacro('ticketNumber'));
+ }
+}
diff --git a/tests/Fixtures/Models/DummyOrder.php b/tests/Fixtures/Models/DummyOrder.php
new file mode 100644
index 0000000..1eadf66
--- /dev/null
+++ b/tests/Fixtures/Models/DummyOrder.php
@@ -0,0 +1,17 @@
+table(), function (Blueprint $table) {
$table->increments('id');
$table->string('sku')->index()->nullable();
+ $table->string('order_number')->index()->nullable();
+ $table->string('ticket_number')->index()->nullable();
$table->string('name');
$table->timestamps();
});
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..582f2b7
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,66 @@
+set('database.default', 'testing');
+ }
+
+ /**
+ * Config an application has published, applied where Laravel applies config files: after
+ * they load and before any provider registers. defineEnvironment() runs too late for that.
+ *
+ * @return array
+ */
+ protected function publishedConfig(): array
+ {
+ return [];
+ }
+
+ protected function resolveApplicationConfiguration($app)
+ {
+ parent::resolveApplicationConfiguration($app);
+
+ foreach ($this->publishedConfig() as $key => $value) {
+ $app['config']->set($key, $value);
+ }
+ }
+
+ /**
+ * The package's default config file, for building a published copy.
+ *
+ * @return array
+ */
+ protected static function defaultConfig(): array
+ {
+ return require __DIR__ . '/../config/config.php';
+ }
+
+ protected function defineDatabaseMigrations(): void
+ {
+ require_once __DIR__ . '/Fixtures/database/migrations/CreateDummyModelsTable.php';
+
+ $migration = new \CreateDummyModelsTable();
+ $migration->up();
+
+ $this->beforeApplicationDestroyed(static function () use ($migration): void {
+ if (Schema::hasTable('dummy_models')) {
+ $migration->down();
+ }
+ });
+ }
+}