# Upgrading

## 4.x → 5.0.0 (the io 6 port)

`5.0.0` moves the package onto Olly's io 6 line. It is a pure port: same five routes, same
paths, same route names, same permissions, same `products` schema, same service behaviour. The
`4.x` branch keeps the io 5 line alive for hosts that have not cut over.

Done under the io 6 migration programme (Town Centre `plans/io-6-migration.md`, Phase A wave A1)
following the estate recipe in Cygnus Documentation → `docs/operations/io-6-package-port.md`.
Ported by the Apexl side rather than by this repo's owner per the owner ruling of 2026-08-29
("6 will be as settled as anything ever is; io-product ports fall to us").

### 1. Namespace

`apexl\Io\modules\product\` → `Apexl\Io\Product\`.

The package's **own** directory segments are unchanged, so every import maps one-for-one:

| was | now |
|---|---|
| `apexl\Io\modules\product\Entity\Product` | `Apexl\Io\Product\Entity\Product` |
| `apexl\Io\modules\product\Service\ProductService` | `Apexl\Io\Product\Service\ProductService` |
| `apexl\Io\modules\product\Controller\ProductController` | `Apexl\Io\Product\Controller\ProductController` |
| `apexl\Io\modules\product\EntityFactory\ProductEntityFactory` | `Apexl\Io\Product\EntityFactory\ProductEntityFactory` |
| `apexl\Io\modules\product\Enum\permissions\ProductPermission` | `Apexl\Io\Product\Enum\permissions\ProductPermission` |

Note `Enum\permissions\` keeps its lowercase segment — that is this package's directory, not
io's, and renaming it would be a second breaking change in a release that already has one.

### 2. Module class

`apexl\Io\modules\product\productModule` → `Apexl\Io\Product\ProductModule`.

Update the host's `config/modules.php` (and the `modules` table row, if the host binds
`DbModuleFinder`):

```diff
-use apexl\Io\modules\product\productModule;
-…
-    productModule::class,
+use Apexl\Io\Product\ProductModule;
+…
+    ProductModule::class,
```

The class is now `readonly` and extends `Apexl\Io\Module` directly; `BasicApiModule` and
`apexl/io-basic-module` are gone on the io 6 line. `routes()` takes no `RouteManager`.

### 3. One route generator deleted (no live route lost)

`productModule::initialise()` registered `Product` with io-basic-module's entity-route
generator. That generator pointed the four routes it created at
`Controller::get`/`put`/`post`/`delete` — methods that **do not exist** on io 5's abstract
`Controller`. Every one of them answered a 500 from callable resolution before the permission it
declared was ever read, and they sat at an unprefixed `/entities/…` that nothing referenced.

`initialise()` and the module-store registration are deleted rather than ported. io 6 has no
module store and no generator. **No route that ever answered has been removed** — the five real
routes are unchanged:

| verb | path | name | permission |
|---|---|---|---|
| GET | `/api/v1/products` | `product.index` | `CAN_VIEW_PRODUCTS` |
| POST | `/api/v1/products` | `product.create` | `CAN_MANAGE_PRODUCTS` |
| GET | `/api/v1/products/{id}` | `product.get` | `CAN_VIEW_PRODUCTS` |
| PUT | `/api/v1/products/{id}` | `product.update` | `CAN_MANAGE_PRODUCTS` |
| DELETE | `/api/v1/products/{id}` | `product.delete` | `CAN_MANAGE_PRODUCTS` |

The io 6 route dump was diffed against the 4.x registration order and matches on verb, path,
name, permission and order.

### 4. Routes are middleware, not route args

`RouteManager` is deleted in io 6, and so is
`addRouteArg('name', 'permission', serialize(…))`. The five routes now live in one
`apiGroup('products', …)` — which supplies the `/api/v1/products` prefix from `api.version` —
with a `RoutePermissionMiddleware` **instance** on each route, because reads and writes are
gated differently. Nothing a consumer calls changes.

If a host was reading the permission back off a route, it now asks the route:

```php
$route->getMiddleware()->find(DeclaresRoutePermission::class)?->getPermission();
```

### 5. `services.php` moved

`services.php` → `src/services.php`, and `extra.module` is dropped from `composer.json`. io 6
discovers a package's DI definitions by globbing `vendor/apexl/*/src/services.php`; a file
anywhere else is ignored silently.

### 6. Requires

```diff
-    "apexl/io": "^5.0",
-    "apexl/io-user": "^5.0"
+    "apexl/io": "^6.0",
+    "apexl/io-user": "^6.0"
```

### 7. `apexl/io-permissions` — an undeclared dependency, now stated

`ProductPermission` has implemented `PermissionFlagInterface` since `3.0.0` while
`apexl/io-permissions` has never appeared in `require`. It resolved because every host that
registers this module also installs io-permissions; standalone, that enum cannot load.

io-permissions has no io 6 release yet, so it still cannot go in `require`. Instead:

- a `suggest` entry now says so out loud;
- the interface is stubbed for the test suite only, in
  `tests/Support/io-permissions-contracts.php`, loaded from `tests/bootstrap.php` and **never**
  from composer autoload, under the predicted io 6 name
  `Apexl\Io\Permissions\Interface\PermissionFlagInterface`.

**Undo, when io-permissions ships on io 6:** add `"apexl/io-permissions": "^3.0"` to `require`,
drop the `suggest` entry, delete `tests/Support/io-permissions-contracts.php` and the `require`
of it in `tests/bootstrap.php`, and re-run the suite. If the real release lands on a different
name than predicted, the stub's guard stops matching, both declare, and the suite fatals on a
duplicate symbol rather than passing quietly against a fiction.

### 8. Not changed

- The `products` table and its migration (`20260606010001_InstallProducts`) — a host upgrading
  from 4.x has the table already and phinx records the version as run.
- `ProductService`'s behaviour, validation and `serialise()` shape.
- `ProductPermission`'s cases and `defaultRoleNames()`.
- The five route names, however plain they look. Renaming them is a separate breaking change.
