Skip to content

Instantly share code, notes, and snippets.

@Ocramius
Last active November 9, 2023 07:27
Show Gist options
  • Star 40 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ocramius/4bd03ad4d545bb07164c133d4d2b3686 to your computer and use it in GitHub Desktop.
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
<?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();
}
}
@SenseException
Copy link

Thank you for those snippets.

@bcremer
Copy link

bcremer commented Mar 9, 2020

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