Psalm fixes. Introduced PHP Standard library

This commit is contained in:
theUniC
2022-08-19 21:12:23 +02:00
parent c29fb666af
commit 7a23ca97f9
22 changed files with 1412 additions and 447 deletions

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace ApiPlatform\Core\DataPersister
{
/** @template T */
interface DataPersisterInterface
{
/**
* @param mixed $data
*/
public function supports($data): bool;
/**
* @param T $data
* @return T|void
*/
public function persist($data);
/**
* @param T $data
* @return void
*/
public function remove($data);
}
/**
* @template T
* @template-extends DataPersisterInterface<T>
*/
interface ContextAwareDataPersisterInterface extends DataPersisterInterface { }
}

View File

@@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM
{
/** @template T */
abstract class AbstractQuery
{
/** @psalm-return EntityManagerInterface<T> */
public function getEntityManager() { }
/**
* @psalm-param string|1|2|3|4|5|null $hydrationMode
* @psalm-return T[]
*/
public function getResult($hydrationMode = self::HYDRATE_OBJECT) { }
/**
* @psalm-param string|1|2|3|4|5|null $hydrationMode
* @psalm-return ?T
* @throws NonUniqueResultException
*/
public function getOneOrNullResult($hydrationMode = null) { }
/**
* @psalm-param string|1|2|3|4|5|null $hydrationMode
* @psalm-return T
* @throws NonUniqueResultException
* @throws NoResultException
*/
public function getSingleResult($hydrationMode = null) { }
/**
* @psalm-param ArrayCollection|array|null $parameters
* @psalm-param string|1|2|3|4|5|null $hydrationMode
* @psalm-return \Doctrine\ORM\Internal\Hydration\IterableResult<T>
*/
public function iterate($parameters = null, $hydrationMode = null) { }
/**
* @psalm-param ArrayCollection|array|null $parameters
* @psalm-param string|1|2|3|4|5|null $hydrationMode
* @psalm-return T|T[]|null
*/
public function execute($parameters = null, $hydrationMode = null) { }
}
/**
* @template T
* @template-extends AbstractQuery<T>
*/
final class Query extends AbstractQuery {}
/**
* @template T
*/
class QueryBuilder
{
/** @psalm-return \Doctrine\ORM\Query<T> */
public function getQuery() { }
}
}
namespace Doctrine\ORM\Internal\Hydration
{
/**
* @template T
* @template-implements \Iterator<T>
*/
class IterableResult implements \Iterator { }
}

View File

@@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace Functional;
/**
* @template A
*
* @psalm-param \Traversable<A>|list<A> $collection
* @psalm-param callable(A, mixed=, A[]): bool $callback
* @psalm-return list<A>
*/
function select($collection, callable $callback) {}
/**
* @template B
*
* @psalm-param \Traversable<B>|list<B> $collection
* @psalm-param callable(B, mixed=, B[]): bool $callback
* @psalm-return B|null
*/
function head($collection, callable $callback = null) {}
/**
* @template C
* @template D
*
* @psalm-param \Traversable<C>|list<C> $collection
* @psalm-param callable(C, array-key=, C[]=): D $callback
* @psalm-return list<D>
*/
function map($collection, callable $callback) {}
/**
* @template E
*
* @psalm-param \Traversable<E>|list<C> $collection
* @psalm-param callable(E, array-key=, E[]=): void $callback
* @psalm-return null
* @no-named-arguments
*/
function each($collection, callable $callback) {}

View File

@@ -1,162 +0,0 @@
<?php
declare(strict_types=1);
namespace Predis
{
/**
* @template T
* @implements \Iterator<T>
* @method int del(array $keys)
* @method string dump($key)
* @method int exists($key)
* @method int expire($key, $seconds)
* @method int expireat($key, $timestamp)
* @method array keys($pattern)
* @method int move($key, $db)
* @method mixed object($subcommand, $key)
* @method int persist($key)
* @method int pexpire($key, $milliseconds)
* @method int pexpireat($key, $timestamp)
* @method int pttl($key)
* @method string randomkey()
* @method mixed rename($key, $target)
* @method int renamenx($key, $target)
* @method array scan($cursor, array $options = null)
* @method array sort($key, array $options = null)
* @method int ttl($key)
* @method mixed type($key)
* @method int append($key, $value)
* @method int bitcount($key, $start = null, $end = null)
* @method int bitop($operation, $destkey, $key)
* @method array bitfield($key, $subcommand, ...$subcommandArg)
* @method int decr($key)
* @method int decrby($key, $decrement)
* @method string get($key)
* @method int getbit($key, $offset)
* @method string getrange($key, $start, $end)
* @method string getset($key, $value)
* @method int incr($key)
* @method int incrby($key, $increment)
* @method string incrbyfloat($key, $increment)
* @method array mget(array $keys)
* @method mixed mset(array $dictionary)
* @method int msetnx(array $dictionary)
* @method mixed psetex($key, $milliseconds, $value)
* @method mixed set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
* @method int setbit($key, $offset, $value)
* @method int setex($key, $seconds, $value)
* @method int setnx($key, $value)
* @method int setrange($key, $offset, $value)
* @method int strlen($key)
* @method int hdel($key, array $fields)
* @method int hexists($key, $field)
* @method string hget($key, $field)
* @method array hgetall($key)
* @method int hincrby($key, $field, $increment)
* @method string hincrbyfloat($key, $field, $increment)
* @method array hkeys($key)
* @method int hlen($key)
* @method array hmget($key, array $fields)
* @method mixed hmset($key, array $dictionary)
* @method array hscan($key, $cursor, array $options = null)
* @method int hset($key, $field, $value)
* @method int hsetnx($key, $field, $value)
* @method array hvals($key)
* @method int hstrlen($key, $field)
* @method array blpop(array $keys, $timeout)
* @method array brpop(array $keys, $timeout)
* @method array brpoplpush($source, $destination, $timeout)
* @method string lindex($key, $index)
* @method int linsert($key, $whence, $pivot, $value)
* @method int llen($key)
* @method string lpop($key)
* @method int lpush($key, array $values)
* @method int lpushx($key, $value)
* @method array lrange($key, $start, $stop)
* @method int lrem($key, $count, $value)
* @method mixed lset($key, $index, $value)
* @method mixed ltrim($key, $start, $stop)
* @method string rpop($key)
* @method string rpoplpush($source, $destination)
* @method int rpush($key, array $values)
* @method int rpushx($key, $value)
* @method int sadd($key, array $members)
* @method int scard($key)
* @method array sdiff(array $keys)
* @method int sdiffstore($destination, array $keys)
* @method array sinter(array $keys)
* @method int sinterstore($destination, array $keys)
* @method int sismember($key, $member)
* @method array smembers($key)
* @method int smove($source, $destination, $member)
* @method string spop($key, $count = null)
* @method string srandmember($key, $count = null)
* @method int srem($key, $member)
* @method array sscan($key, $cursor, array $options = null)
* @method array sunion(array $keys)
* @method int sunionstore($destination, array $keys)
* @method int zadd($key, array $membersAndScoresDictionary)
* @method int zcard($key)
* @method string zcount($key, $min, $max)
* @method string zincrby($key, $increment, $member)
* @method int zinterstore($destination, array $keys, array $options = null)
* @method array zrange($key, $start, $stop, array $options = null)
* @method array zrangebyscore($key, $min, $max, array $options = null)
* @method int zrank($key, $member)
* @method int zrem($key, $member)
* @method int zremrangebyrank($key, $start, $stop)
* @method int zremrangebyscore($key, $min, $max)
* @method array zrevrange($key, $start, $stop, array $options = null)
* @method array zrevrangebyscore($key, $max, $min, array $options = null)
* @method int zrevrank($key, $member)
* @method int zunionstore($destination, array $keys, array $options = null)
* @method string zscore($key, $member)
* @method array zscan($key, $cursor, array $options = null)
* @method array zrangebylex($key, $start, $stop, array $options = null)
* @method array zrevrangebylex($key, $start, $stop, array $options = null)
* @method int zremrangebylex($key, $min, $max)
* @method int zlexcount($key, $min, $max)
* @method int pfadd($key, array $elements)
* @method mixed pfmerge($destinationKey, array $sourceKeys)
* @method int pfcount(array $keys)
* @method mixed pubsub($subcommand, $argument)
* @method int publish($channel, $message)
* @method mixed discard()
* @method array exec()
* @method mixed multi()
* @method mixed unwatch()
* @method mixed watch($key)
* @method mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method mixed evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method mixed script($subcommand, $argument = null)
* @method mixed auth($password)
* @method string echo($message)
* @method mixed ping($message = null)
* @method mixed select($database)
* @method mixed bgrewriteaof()
* @method mixed bgsave()
* @method mixed client($subcommand, $argument = null)
* @method mixed config($subcommand, $argument = null)
* @method int dbsize()
* @method mixed flushall()
* @method mixed flushdb()
* @method array info($section = null)
* @method int lastsave()
* @method mixed save()
* @method mixed slaveof($host, $port)
* @method mixed slowlog($subcommand, $argument = null)
* @method array time()
* @method array command()
* @method int geoadd($key, $longitude, $latitude, $member)
* @method array geohash($key, array $members)
* @method array geopos($key, array $members)
* @method string geodist($key, $member1, $member2, $unit = null)
* @method array georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
* @method array georadiusbymember($key, $member, $radius, $unit, array $options = null)
*/
class Client
{
}
}

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace Symfony\Component\Messenger\Stamp
{
/** @template T */
final class HandledStamp
{
/** @psam-return T */
public function getResult() { }
}
}
namespace Symfony\Component\Messenger
{
trait HandleTrait
{
/**
* @template T
* @psalm-param object|Envelope $message
* @psalm-return T|Envelope
*/
private function handle($message) { }
}
}

View File

@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace Symfony\Component\Validator
{
class GroupSequence
{
}
interface ConstraintViolationInterface
{
}
/**
* @extends \Traversable<int, ConstraintViolationInterface>
* @extends \ArrayAccess<int, ConstraintViolationInterface>
*/
interface ConstraintViolationListInterface extends \Traversable, \Countable, \ArrayAccess
{
}
}

View File

@@ -9,6 +9,7 @@
"ext-json": "*",
"ext-pdo": "*",
"ext-redis": "*",
"azjezz/psl": "^2.0",
"beberlei/assert": "^3.2",
"doctrine/annotations": "^1.0",
"doctrine/doctrine-bundle": "^2.4",
@@ -53,7 +54,11 @@
"fakerphp/faker": "^1.20",
"keyvanakbary/mimic": "^1.0",
"mockery/mockery": "^1.5",
"php-standard-library/psalm-plugin": "^2.0",
"phpunit/phpunit": "^9.0",
"psalm/plugin-mockery": "^0.9.1",
"psalm/plugin-phpunit": "^0.17.0",
"psalm/plugin-symfony": "^3.1",
"roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.1.*",
"symfony/css-selector": "6.1.*",
@@ -63,7 +68,9 @@
"symfony/phpunit-bridge": "^6.1",
"symfony/stopwatch": "6.1.*",
"symfony/web-profiler-bundle": "6.1.*",
"theofidry/psysh-bundle": "^4.3"
"theofidry/psysh-bundle": "^4.3",
"vimeo/psalm": "^4.26",
"weirdan/doctrine-psalm-plugin": "^2.3"
},
"config": {
"preferred-install": {

1316
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,11 @@ services:
MYSQL_USER: user
MYSQL_PASSWORD: pass
MYSQL_DATABASE: db
app:
build: .docker/php
init: true
env_file: .env.docker.dist
ports:
- "8000:80"
volumes:
- ./:/var/www/html
# app:
# build: .docker/php
# init: true
# env_file: .env.docker.dist
# ports:
# - "8000:80"
# volumes:
# - ./:/var/www/html

View File

@@ -1,26 +1,22 @@
<?xml version="1.0"?>
<psalm
errorLevel="4"
resolveFromConfigFile="true"
errorLevel="1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<directory name="src"/>
<ignoreFiles>
<directory name="vendor" />
<directory name="src/CheeperSpaghetti" />
<directory name="src/CheeperCommandHandlers" />
<directory name="src/CheeperLayered" />
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<stubs>
<file name=".psalm-stubs/api-platform.phpstub"/>
<file name=".psalm-stubs/doctrine-orm.phpstub"/>
<file name=".psalm-stubs/symfony-messenger.phpstub"/>
<file name=".psalm-stubs/symfony-validator.phpstub"/>
<file name=".psalm-stubs/lstrojny-functional-php.phpstub"/>
</stubs>
<plugins>
<pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin"/>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
<pluginClass class="Weirdan\DoctrinePsalmPlugin\Plugin"/>
<pluginClass class="Psl\Psalm\Plugin"/>
<pluginClass class="Psalm\MockeryPlugin\Plugin"/>
</plugins>
</psalm>

View File

@@ -16,7 +16,8 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Constraints as Assert;
use function Safe\json_decode;
use Psl\Type;
use Psl\Json;
use OpenApi\Attributes as OA;
final class PostAuthorController extends AbstractController
@@ -95,7 +96,7 @@ final class PostAuthorController extends AbstractController
'birth_date' => new Assert\Optional(new Assert\DateTime(format: "Y-m-d"))
]);
$data = json_decode($request->getContent(), true);
$data = Json\typed($request->getContent(), Type\dict(Type\string(), Type\string()));
$violations = $this->validator->validate($data, $constraint);
if (count($violations) > 0) {

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace App\Controller;
use App\Dto\AuthorDto;
use App\Dto\CheepDto;
use Cheeper\Application\CheepApplicationService;
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
@@ -12,11 +11,11 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Constraints as Assert;
use function Safe\json_decode;
use Psl\Json;
use Psl\Type;
use OpenApi\Attributes as OA;
final class PostCheepController extends AbstractController
@@ -85,7 +84,7 @@ final class PostCheepController extends AbstractController
'message' => new Assert\NotBlank()
]);
$data = json_decode($request->getContent(), true);
$data = Json\typed($request->getContent(), Type\dict(Type\string(), Type\string()));
$violations = $this->validator->validate(
$data,

View File

@@ -4,18 +4,17 @@ declare(strict_types=1);
namespace App\Controller;
use App\Dto\CheepDto;
use Cheeper\Application\FollowApplicationService;
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
use Nelmio\ApiDocBundle\Annotation\Model;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use function Safe\json_decode;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints as Assert;
use OpenApi\Attributes as OA;
use Psl\Json;
use Psl\Type;
final class PostFollowersController extends AbstractController
{
@@ -80,7 +79,7 @@ final class PostFollowersController extends AbstractController
'to_author_id' => [new Assert\NotBlank(), new Assert\Uuid()],
]);
$data = json_decode($request->getContent(), true);
$data = Json\typed($request->getContent(), Type\dict(Type\string(), Type\string()));
$violations = $this->validator->validate($data, $constraints);
if (count($violations) > 0) {

View File

@@ -28,6 +28,7 @@ final class AppFixtures extends Fixture
$manager->flush();
}
/** @return Author[] */
private function makeAuthorFixtures(ObjectManager $manager): array
{
$carlos = Author::signUp(

View File

@@ -6,6 +6,7 @@ namespace App\Dto;
use Cheeper\DomainModel\Cheep\Cheep;
use DateTimeInterface;
use Safe\DateTimeImmutable;
final class CheepDto
{
@@ -24,7 +25,7 @@ final class CheepDto
$cheep->cheepId()->toString(),
$cheep->authorId()->toString(),
$cheep->cheepMessage()->message(),
\DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $cheep->cheepDate()->date())->format(DateTimeInterface::ATOM)
DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $cheep->cheepDate()->date())->format(DateTimeInterface::ATOM)
);
}
}

View File

@@ -9,6 +9,9 @@ use Cheeper\DomainModel\Follow\Follow;
use DateTimeImmutable;
use InvalidArgumentException;
/**
* @final
*/
class Author
{
private function __construct(
@@ -35,8 +38,8 @@ class Author
?string $location = null,
?Website $website = null,
?BirthDate $birthDate = null
): static {
return new static(
): self {
return new self(
$authorId->toString(),
$userName->userName(),
$email->value(),

View File

@@ -10,11 +10,11 @@ use DateTimeInterface;
interface CheepRepository
{
public function add(Cheep $cheep): void;
public function ofId(CheepId $cheepId): ?Cheep;
public function ofId(CheepId $cheepId): Cheep|null;
/** @return Cheep[] */
/** @return list<Cheep> */
public function all(): array;
/** @return Cheep[] */
/** @return list<Cheep> */
public function ofFollowingPeopleOf(Author $author, int $offset, int $size): array;
}

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Cheeper\DomainModel\Clock;
use DateTimeImmutable;
final class DateCollectionClockStrategy implements ClockStrategy
{
private int $iterator;
public function __construct(
private readonly array $collection = []
) {
$this->iterator = 0;
}
public function now(): DateTimeImmutable
{
if (empty($this->collection)) {
throw new \InvalidArgumentException('Date collection is empty');
}
$currentDate = $this->collection[$this->iterator];
$this->iterator = ($this->iterator + 1) % count($this->collection);
return $currentDate;
}
}

View File

@@ -9,6 +9,7 @@ use Cheeper\DomainModel\Cheep\Cheep;
use Cheeper\DomainModel\Cheep\CheepId;
use Cheeper\DomainModel\Cheep\CheepRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Ramsey\Uuid\Uuid;
final class DoctrineOrmCheepRepository implements CheepRepository
@@ -45,12 +46,14 @@ WHERE f.fromAuthorId = :fromAuthorId
ORDER BY c.cheepDate.date DESC
DQL;
/** @psalm-var Query<Cheep> $query */
$query = $this->em->createQuery($dql);
$query->setFirstResult($offset);
$query->setMaxResults($size);
return $query->execute([
$query->setParameters([
'fromAuthorId' => $author->authorId()
]);
return $query->getResult();
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Cheeper\Infrastructure\Persistence;
use Cheeper\DomainModel\Author\Author;
use Cheeper\DomainModel\Cheep\Cheep;
use Cheeper\DomainModel\Cheep\CheepId;
use Cheeper\DomainModel\Cheep\CheepRepository;
use Psl\Iter;
final class InMemoryCheepRepository implements CheepRepository
{
/** @var list<Cheep> */
private array $cheeps = [];
public function add(Cheep $cheep): void
{
$this->cheeps[] = $cheep;
}
public function ofId(CheepId $cheepId): Cheep|null
{
return Iter\search($this->cheeps, fn(Cheep $c) => $c->cheepId()->equals($cheepId));
}
public function all(): array
{
return $this->cheeps;
}
public function ofFollowingPeopleOf(Author $author, int $offset, int $size): array
{
return $this->all();
}
}

View File

@@ -19,11 +19,11 @@ final class PostCheepControllerTest extends ApiTestCase
$autorData = $this->createAuthorWithRandomizedData($client);
$cheepData = $this->makeRandomizedCheep($client, $autorData['userName']);
$client->request(Request::METHOD_GET, "/api/cheeps/${cheepData['id']}");
$response = $client->request(Request::METHOD_GET, "/api/cheeps/${cheepData['id']}");
$this->assertResponseIsSuccessful();
$data = $client->getResponse()->toArray();
$data = $response->toArray();
$this->assertEquals($cheepData, $data);
}

View File

@@ -5,21 +5,16 @@ declare(strict_types=1);
namespace Cheeper\Tests\Application;
use Cheeper\Application\CheepApplicationService;
use Cheeper\DomainModel\Author\Author;
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
use Cheeper\DomainModel\Author\AuthorId;
use Cheeper\DomainModel\Author\AuthorRepository;
use Cheeper\DomainModel\Author\EmailAddress;
use Cheeper\DomainModel\Author\UserName;
use Cheeper\DomainModel\Cheep\Cheep;
use Cheeper\DomainModel\Cheep\CheepId;
use Cheeper\DomainModel\Cheep\CheepMessage;
use Cheeper\DomainModel\Cheep\CheepRepository;
use Cheeper\Infrastructure\Persistence\InMemoryAuthorRepository;
use Cheeper\Infrastructure\Persistence\InMemoryCheepRepository;
use Cheeper\Tests\DomainModel\Author\AuthorTestDataBuilder;
use Cheeper\Tests\DomainModel\Cheep\CheepTestDataBuilder;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psl\Iter;
final class CheepApplicationServiceTest extends TestCase
{
@@ -29,7 +24,7 @@ final class CheepApplicationServiceTest extends TestCase
public function setUp(): void
{
$this->cheepRepository = Mockery::mock(CheepRepository::class);
$this->cheepRepository = new InMemoryCheepRepository();
$this->authorRepository = new InMemoryAuthorRepository();
$this->cheepService = new CheepApplicationService($this->authorRepository, $this->cheepRepository);
}
@@ -49,11 +44,8 @@ final class CheepApplicationServiceTest extends TestCase
AuthorTestDataBuilder::anAuthor()->build()
);
$this->cheepRepository->expects('add');
$cheep = $this->cheepService->postCheep('irrelevant', 'message');
$this->assertNotNull($cheep);
$this->assertNotNull($cheep->authorId());
$this->assertEquals('message', $cheep->cheepMessage()->message());
}
@@ -74,12 +66,12 @@ final class CheepApplicationServiceTest extends TestCase
$this->authorRepository->add($author);
$cheeps = [
CheepTestDataBuilder::aCheep()->withAMessage("test1"),
CheepTestDataBuilder::aCheep()->withAMessage("test2"),
CheepTestDataBuilder::aCheep()->withAMessage("test3"),
CheepTestDataBuilder::aCheep()->withAMessage("test1")->build(),
CheepTestDataBuilder::aCheep()->withAMessage("test2")->build(),
CheepTestDataBuilder::aCheep()->withAMessage("test3")->build(),
];
$this->cheepRepository->allows()->ofFollowingPeopleOf($author, 0, 10)->andReturn($cheeps);
Iter\apply($cheeps, fn(Cheep $c) => $this->cheepRepository->add($c));
$this->assertCount(
count($cheeps),