# Upgrading io-mirror

## 4.0.0 → 4.1.0

Additive. Nothing is removed or renamed, and an install that does none of the
below keeps working — but two of the changes alter behaviour that was
previously wrong, so read §3 before deploying.

### 1. Depend on the contract, not the helper

`UploaderServiceResolverInterface` is new, and `UploaderServiceResolver`
implements it. The interface is bound to the concrete class in
`src/services.php`, so injecting either works.

```php
-public function __construct(private UploaderServiceResolver $resolver) {}
+public function __construct(private UploaderServiceResolverInterface $resolver) {}
```

The class stays `final readonly` — the interface exists so consumers can state
what they need, not so the helper can be subclassed.

### 2. Uploaders can register a short alias

`MIRROR_DEFAULT_UPLOADER` accepted a class-string only, so every install named
an FQCN in its environment and re-broke it whenever a class moved. A driver
package can now register an alias on the `uploaderServiceAliases` filter hook:

```php
final readonly class AddS3Alias implements HookInterface
{
    public static function hooks(): array
    {
        return [UploaderServiceResolverInterface::HOOK__UPLOADER_SERVICE_ALIASES];
    }

    public function __invoke(StringCollection $aliases): StringCollection
    {
        $aliases->set('s3', S3UploaderService::class);   // mutate in place

        return $aliases;
    }
}
```

```diff
-MIRROR_DEFAULT_UPLOADER='Apexl\Io\MirrorS3\Services\S3UploaderService'
+MIRROR_DEFAULT_UPLOADER='s3'
```

**Mutate the collection in place.** The resolver memoises the instance it
passed in, so returning a *new* collection resolves correctly but re-fires the
whole hook chain on every subsequent call.

FQCNs still work and are still resolved first — an unknown alias is passed
through unchanged, so it fails in `UploaderServiceFactory::make()` exactly as a
bad class name always did.

### 3. Two config values behaved wrongly — check yours

Both fixes can change what your install does. Neither is opt-in.

**`allow_no_default_uploader` read the wrong variable.** It was
`env('MIRROR_DEFAULT_UPLOADER', false)` — the *uploader* variable, not its own.
Any install that set a default uploader therefore had this silently enabled,
whatever it meant to say. It now reads `MIRROR_ALLOW_NO_DEFAULT_UPLOADER`.

> If you relied on "no exception when the uploader is missing" without ever
> setting that variable, you were relying on the bug: set
> `MIRROR_ALLOW_NO_DEFAULT_UPLOADER=true` explicitly, or you will now get the
> `UploaderException` the setting exists to suppress.

**Booleans were never coerced.** `env()` returns strings, and `"false"` is
truthy, so `MIRROR_RETAIN_LOCAL_FILES=false` **retained local files anyway** —
the exact opposite of what it says. Both booleans now go through
`filter_var(..., FILTER_VALIDATE_BOOLEAN)`.

> If you set `MIRROR_RETAIN_LOCAL_FILES=false` and quietly kept every local
> file, that stops now, and local files will start being cleaned up after
> mirroring. Confirm that is what you want before deploying.

### 4. `voku/arrayy` is now a declared dependency

It always came in through `apexl/io`; the alias map is typed as
`Arrayy\Type\StringCollection`, so the package declares what it imports.
No action needed.
