NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist · #3053 most downloaded on Packagist
Unleash the Power of Optics in your code!
Last release 4 months ago
29 May 2026
Release timing varies
gaps range from 2 weeks to 8 months
Some releases are documented
notes for 10 of 22 stable releases
Nothing withdrawn
no release was ever pulled
3 years old
22 releases · first in 2023
Keep unnecessary artefacts out of releases by @raphaelstolt in #33
Full Changelog: 1.0.0...1.0.1
Migrate Iso/Lens from SA to STAB form (Iso<S,T,A,B>) by @veewee in #31
Full Changelog: 0.19.0...1.0.0
This release migrates Iso and Lens from the two-parameter form to the
four-parameter STAB form. The change is almost entirely at the type
level. Runtime method signatures are unchanged, so most code keeps working
without edits. You only need to act if you write explicit Psalm/PHPDoc type
annotations for optics, implement the interfaces yourself, or rely on
compose() rejecting empty input.
New to STAB? Read the 🧬 STAB optics walkthrough for the
why behind the four parameters.
| Before | After |
|---|---|
Iso<S, A> |
Iso<S, T, A, B> |
Lens<S, A> |
Lens<S, T, A, B> |
IsoInterface<S, A> |
IsoInterface<S, T, A, B> |
LensInterface<S, A> |
LensInterface<S, T, A, B> |
S and A keep their old meaning (the whole, and the focus). T and B are
the outgoing counterparts that let a write change the type. If your optic
never changes type on write, repeat the parameters: Lens<S, S, A, A>.
Iso and Lens (and their interfaces) now carry S, T, A, B:
/** @var Lens<Person, Person, string, string> */ // non-type-changing
/** @var Lens<Person, AnonymizedPerson, string, HashedName> */ // type-changingThe concrete Iso/Lens classes are invariant on all four slots; the
IsoInterface/LensInterface are covariant on all four. See the next
section for what that distinction means in practice.
The variance differs between the concrete types and the interfaces, and that is
deliberate:
Iso and Lens use @template (invariant). The four slotsset bothB and produces T, so the concrete type can't safely vary onIsoInterface and LensInterface use @template-covariant. ALensInterface<Cat, Cat, string, string> is accepted where aLensInterface<Animal, Animal, string, string> is expected, so you can storeSo when you want that subtype flexibility, type your stored properties,
parameters, and return types against the interface (LensInterface<...> or
IsoInterface<...>) rather than the concrete class. A property typed as
Lens<Animal, ...> rejects a Lens<Cat, ...>; the same property typed as
LensInterface<Animal, ...> accepts it.
// invariant: only an exact Lens<Animal,...> fits
/** @var Lens<Animal, Animal, string, string> */
// covariant: a LensInterface<Cat,...> is also accepted
/** @var LensInterface<Animal, Animal, string, string> */The signatures now allow a write to produce a different type:
Lens::set(S, B): TLens::update(S, callable(A): B): TIso::from(B): TAt runtime these take exactly the same arguments as before; only the declared
types widened. Existing non-type-changing usage (B = A, T = S) is
unaffected.
compose() accepts empty inputIso\compose() and Lens\compose() now take array<...> instead of
non-empty-array<...>. Composition is a monoid:
If you previously relied on Psalm flagging an empty compose() call, that
check is gone.
optional() return type widened// before
LensInterface<S, A|null>
// after
LensInterface<S|null, T|null, A|null, B|null>AdjacentTemplateValidator removedThe custom Psalm plugin that detected compose() boundary mismatches is gone.
Boundary mismatches are now caught by standard Psalm inference from the STAB
signatures. No configuration change is needed; just expect the diagnostics to
come from Psalm core rather than the plugin.
Nothing. set, update, from, compose and friends behave identically at
runtime.
Update two-parameter annotations to four. For the common non-type-changing
case, duplicate:
-/** @var Lens<Person, string> */
+/** @var Lens<Person, Person, string, string> */
-/** @param IsoInterface<Foo, Bar> $iso */
+/** @param IsoInterface<Foo, Foo, Bar, Bar> $iso */If your optic genuinely changes type on write, fill in the real T/B:
/** @var Lens<Person, AnonymizedPerson, string, HashedName> */IsoInterface / LensInterface yourselfAdd the T and B template parameters and update the method signatures
(set, trySet, update, tryUpdate, from, tryFrom, optional,
compose, inverse, asLens) to match the new interface. See
src/Lens/LensInterface.php and src/Iso/IsoInterface.php for the reference
shapes.
compose() rejecting empty arraysAdd your own non-empty check before calling compose() if you still need it.
Run your static analyzer after bumping the dependency:
composer update veewee/reflecta
vendor/bin/psalmMost fallout shows up as InvalidArgument or MismatchingDocblockParamType on
optic annotations. Apply the two-to-four parameter expansion above to fix them.
One column per month.
Fix compose() boundary detection under covariant Iso/Lens templates by @veewee in #30
Full Changelog: 0.18.0...0.19.0
Split into PSL components and Bump PHP to 84 by @veewee in #27
Full Changelog: 0.17.0...0.18.0
Optimize properties_set to clone once instead of per property by @veewee in #26
Full Changelog: 0.16.0...0.17.0
Replace azjezz/psl with php-standard-library/php-standard-library by @veewee in #25
Full Changelog: 0.15.0...0.16.0
Allow PSL 5.x by @veewee in #24
Upgrade PHP project to support PHP 8.5 by @veewee in #23
Full Changelog: 0.13.0...0.14.0
Infer properties_get and properties_set settings. by @veewee in #22
Full Changelog: 0.12.0...0.13.0
Let psalm known assignments of unknown properties by @veewee in #21
Full Changelog: 0.11.0...0.12.0
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →