Last active
November 9, 2023 07:27
-
-
Save Ocramius/4bd03ad4d545bb07164c133d4d2b3686 to your computer and use it in GitHub Desktop.
A small compendium of what is possible with `vimeo/psalm` 3.9.x to add some decent type system features to PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// -- types are a compile-time propagated concept | |
// https://psalm.dev/r/338f74a96c | |
class TheType | |
{ | |
/** @var string */ | |
public $foo = 'bar'; | |
} | |
/** @param TheType $typed */ | |
function doSomethingWith($typed) : void | |
{ | |
// While PHP supports types in method signatures, this is sufficient, | |
// when using a type checker | |
$typed->foo; | |
} | |
// -- constant types | |
// https://psalm.dev/r/ae1aadffc7 | |
class SomeConstants | |
{ | |
const C = 'c'; | |
const OTHER_D = 'd'; | |
const OTHER_E = 'e'; | |
} | |
/** @psalm-param 1|2|'a'|'b'|SomeConstants::C|SomeConstants::OTHER_* $parameter */ | |
function intOrAOrB($parameter): void | |
{ | |
echo $parameter; | |
} | |
intOrAOrB(1); | |
intOrAOrB('b'); | |
intOrAOrB('c'); | |
intOrAOrB('d'); | |
intOrAOrB('B'); // error | |
// -- true and false types | |
// https://psalm.dev/r/1d1c889ade | |
/** @psalm-return true */ | |
function alwaysTrue(int $input) : bool | |
{ | |
return (bool) $input; // error | |
} | |
// -- array types | |
// https://psalm.dev/r/16a451edf6 | |
/** | |
* @psalm-param array{ | |
* key: string, | |
* optionalKey?: string, | |
* nullableKey: ?string | |
* } $parameter | |
*/ | |
function requiresSpecificArrayShape(array $parameter) : void | |
{ | |
echo $parameter['key']; | |
} | |
// -- lists | |
// https://psalm.dev/r/c5de1156b5 | |
/** | |
* @psalm-param list<string> $parameter | |
* | |
* @psalm-return list<int> | |
*/ | |
function requiresList(array $parameter) : array | |
{ | |
return array_keys($parameter); | |
} | |
// -- non-empty lists | |
// https://psalm.dev/r/ab304679a9 | |
/** @psalm-param list<string> $parameter */ | |
function requiresPossiblyEmptyList(array $parameter) : string | |
{ | |
return $parameter[0]; // error | |
} | |
/** @psalm-param non-empty-list<string> $parameter */ | |
function requiresNonEmptyList(array $parameter) : string | |
{ | |
return $parameter[0]; | |
} | |
// -- intersection and union types | |
// https://psalm.dev/r/2323ab1156 | |
interface HasA { function doA() : void; } | |
interface HasB { function doB() : void; } | |
/** @psalm-param HasA&HasB $ab */ | |
function doAAndB($ab): void | |
{ | |
$ab->doA(); | |
$ab->doB(); | |
} | |
/** @psalm-param HasA|HasB $ab */ | |
function doAOrB($ab): void | |
{ | |
$ab->doA(); // error | |
$ab->doB(); // error | |
assert(! $ab instanceof HasB); | |
$ab->doA(); // OK | |
} | |
// -- templated types | |
// https://psalm.dev/r/c628b74ea1 | |
class Thing { function doTheThing(): void { echo "ok"; } } | |
/** @psalm-template ContainedThing as mixed */ | |
interface ContainsSomething | |
{ | |
/** @psalm-return ContainedThing */ | |
function get(); | |
/** | |
* @psalm-template ThingToBeWrapped as mixed | |
* @psalm-param ThingToBeWrapped $thing | |
* @psalm-return self<ThingToBeWrapped> | |
*/ | |
public static function make($thing); | |
} | |
/** @psalm-param ContainsSomething<Thing> $container */ | |
function doTheThing($container): void | |
{ | |
$container->get()->doTheThing(); // OK | |
} | |
/** @psalm-param class-string<ContainsSomething> $containerClass */ | |
function makeAndDoTheThing(string $containerClass): void | |
{ | |
$containerClass::make(new Thing())->get()->doTheThing(); | |
} | |
// -- iterable is a generic type | |
// https://psalm.dev/r/07eafb40ae | |
interface ItemType { function doSomething(int $index): void; } | |
/** @psalm-return iterable<int, ItemType> */ | |
function makeIteratorOfItemType(): iterable | |
{ | |
return []; | |
} | |
foreach (makeIteratorOfItemType() as $key => $item) { | |
$item->doSomething($key); | |
} | |
// -- read-only | |
// https://psalm.dev/r/3e8db86f49 | |
class HasReadOnlyProperties | |
{ | |
/** | |
* @var string | |
* @psalm-readonly | |
*/ | |
public $foo = 'a'; | |
} | |
$hasReadOnlyProperties = new HasReadOnlyProperties(); | |
$hasReadOnlyProperties->foo = 'b'; // error | |
// -- mutation-free | |
// https://psalm.dev/r/4f5e2815e9 | |
function unsafeFunction() : void { /* does nothing, but we don't know */ } | |
final class MethodDoesNotMutate { | |
/** @var int */ | |
private $state = 0; | |
/** @psalm-mutation-free */ | |
function cannotMutate() : string { | |
$this->state++; // error | |
return 'ha'; | |
} | |
/** @psalm-external-mutation-free */ | |
function cannotMutateOthers(self $others) : string { | |
$this->state++; // OK | |
unsafeFunction(); // error | |
return 'ha'; | |
} | |
} | |
// -- templated type inheritance | |
// https://psalm.dev/r/692afc91cf | |
class Goodies {} | |
/** @psalm-template ItemType */ | |
interface MyCollection { | |
/** @psalm-return iterable<ItemType> */ | |
function get(); | |
} | |
/** @extends MyCollection<Goodies> */ | |
interface CollectionOfGoodies extends MyCollection{ | |
} | |
/** @implements MyCollection<Goodies> */ | |
class ConcreteCollectionOfGoodies implements MyCollection { | |
function get() { return []; } | |
} | |
// -- assertions | |
// https://psalm.dev/r/4c3aaf5e82 | |
/** | |
* @psalm-assert !'a' $value | |
* @psalm-assert !int $value | |
* @param mixed $value | |
*/ | |
function assertItIsNotThatThing($value): void | |
{ | |
// @TODO exceptions here | |
echo gettype($value); | |
} | |
/** | |
* @psalm-param 'a'|'b'|'c'|1|2 $value | |
* @psalm-return 'b'|'c' | |
*/ | |
function useThePreciseType($value): string | |
{ | |
assertItIsNotThatThing($value); | |
return $value; | |
} | |
// -- supporting both psalm and the IDE or other tools | |
// https://psalm.dev/r/115e4f1814 | |
/** | |
* @psalm-template TKey | |
* @psalm-template TValue | |
*/ | |
interface Collection {} | |
/** | |
* @psalm-template TKey | |
* @psalm-template TValue | |
* @implements Collection<TKey, TValue> | |
*/ | |
final class ArrayCollection implements Collection {} | |
class ReferencedEntity {} | |
class MyEntity { | |
/** | |
* @var Collection|ReferencedEntity[] | |
* @psalm-var Collection<int, ReferencedEntity> | |
*/ | |
private $collection; | |
public function __construct() { | |
$this->collection = new ArrayCollection(); | |
} | |
} | |
Examples for templated callables for properly typed higher-order functions: https://gist.github.com/bcremer/2d056761019c5119328ff33cefb157db
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for those snippets.