From 644dcc36f6297e74eb72c90bdd57637e054ee137 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 09:58:29 +0200 Subject: [PATCH 01/16] Integrate `mll-lab/microplate` --- CHANGELOG.md | 4 + composer.json | 8 +- src/Microplate/AbstractMicroplate.php | 148 ++++ src/Microplate/AbstractSection.php | 27 + src/Microplate/Casts/Coordinates96Well.php | 40 + src/Microplate/CoordinateSystem.php | 128 +++ src/Microplate/CoordinateSystem12Well.php | 19 + src/Microplate/CoordinateSystem48Well.php | 19 + src/Microplate/CoordinateSystem96Well.php | 19 + src/Microplate/Coordinates.php | 146 ++++ src/Microplate/Enums/FlowDirection.php | 26 + .../Exceptions/MicroplateIsFullException.php | 11 + .../SectionDoesNotExistException.php | 5 + .../Exceptions/SectionIsFullException.php | 11 + .../Exceptions/UnexpectedFlowDirection.php | 13 + .../Exceptions/WellNotEmptyException.php | 5 + src/Microplate/FullColumnSection.php | 93 ++ src/Microplate/Microplate.php | 129 +++ src/Microplate/MicroplateSet/Location.php | 24 + .../MicroplateSet/MicroplateSet.php | 53 ++ .../MicroplateSet/MicroplateSetAB.php | 21 + .../MicroplateSet/MicroplateSetABCD.php | 21 + .../MicroplateSet/MicroplateSetABCDE.php | 21 + src/Microplate/Scalars/Column96Well.php | 56 ++ src/Microplate/Scalars/Row96Well.php | 15 + src/Microplate/Section.php | 27 + src/Microplate/SectionedMicroplate.php | 72 ++ src/Microplate/WellWithCoordinates.php | 26 + src/QxManager/FilledWell.php | 4 +- src/QxManager/QxManagerSampleSheet.php | 8 +- tests/Microplate/CoordinateSystemTest.php | 39 + tests/Microplate/CoordinatesTest.php | 824 ++++++++++++++++++ .../MicroplateSet/MicroplateSetABCDETest.php | 48 + .../MicroplateSet/MicroplateSetABCDTest.php | 153 ++++ .../MicroplateSet/MicroplateSetABTest.php | 159 ++++ tests/Microplate/MicroplateSetTest.php | 21 + tests/Microplate/MicroplateTest.php | 273 ++++++ tests/Microplate/Scalars/Column96WellTest.php | 59 ++ tests/Microplate/Scalars/Row96WellTest.php | 68 ++ .../FullColumnSectionTest.php | 140 +++ .../SectionedMicroplate/SectionTest.php | 33 + .../SectionedMicroplateTest.php | 48 + tests/QxManager/FilledWellTest.php | 4 +- tests/QxManager/QxManagerSampleSheetTest.php | 6 +- 44 files changed, 3062 insertions(+), 12 deletions(-) create mode 100644 src/Microplate/AbstractMicroplate.php create mode 100644 src/Microplate/AbstractSection.php create mode 100644 src/Microplate/Casts/Coordinates96Well.php create mode 100644 src/Microplate/CoordinateSystem.php create mode 100644 src/Microplate/CoordinateSystem12Well.php create mode 100644 src/Microplate/CoordinateSystem48Well.php create mode 100644 src/Microplate/CoordinateSystem96Well.php create mode 100644 src/Microplate/Coordinates.php create mode 100644 src/Microplate/Enums/FlowDirection.php create mode 100644 src/Microplate/Exceptions/MicroplateIsFullException.php create mode 100644 src/Microplate/Exceptions/SectionDoesNotExistException.php create mode 100644 src/Microplate/Exceptions/SectionIsFullException.php create mode 100644 src/Microplate/Exceptions/UnexpectedFlowDirection.php create mode 100644 src/Microplate/Exceptions/WellNotEmptyException.php create mode 100644 src/Microplate/FullColumnSection.php create mode 100644 src/Microplate/Microplate.php create mode 100644 src/Microplate/MicroplateSet/Location.php create mode 100644 src/Microplate/MicroplateSet/MicroplateSet.php create mode 100644 src/Microplate/MicroplateSet/MicroplateSetAB.php create mode 100644 src/Microplate/MicroplateSet/MicroplateSetABCD.php create mode 100644 src/Microplate/MicroplateSet/MicroplateSetABCDE.php create mode 100644 src/Microplate/Scalars/Column96Well.php create mode 100644 src/Microplate/Scalars/Row96Well.php create mode 100644 src/Microplate/Section.php create mode 100644 src/Microplate/SectionedMicroplate.php create mode 100644 src/Microplate/WellWithCoordinates.php create mode 100644 tests/Microplate/CoordinateSystemTest.php create mode 100644 tests/Microplate/CoordinatesTest.php create mode 100644 tests/Microplate/MicroplateSet/MicroplateSetABCDETest.php create mode 100644 tests/Microplate/MicroplateSet/MicroplateSetABCDTest.php create mode 100644 tests/Microplate/MicroplateSet/MicroplateSetABTest.php create mode 100644 tests/Microplate/MicroplateSetTest.php create mode 100644 tests/Microplate/MicroplateTest.php create mode 100644 tests/Microplate/Scalars/Column96WellTest.php create mode 100644 tests/Microplate/Scalars/Row96WellTest.php create mode 100644 tests/Microplate/SectionedMicroplate/FullColumnSectionTest.php create mode 100644 tests/Microplate/SectionedMicroplate/SectionTest.php create mode 100644 tests/Microplate/SectionedMicroplate/SectionedMicroplateTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 59043ac..33f895e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases). ## Unreleased +### Added + +- Integrate `mll-lab/microplate` + ## v1.12.0 ### Added diff --git a/composer.json b/composer.json index b6055e5..2dac276 100644 --- a/composer.json +++ b/composer.json @@ -18,16 +18,19 @@ "php": "^7.4 || ^8", "ext-calendar": "*", "illuminate/support": "^8.73 || ^9 || ^10", - "mll-lab/microplate": "^6", "mll-lab/str_putcsv": "^1", "nesbot/carbon": "^2.62.1", "thecodingmachine/safe": "^1 || ^2" }, "require-dev": { "ergebnis/composer-normalize": "^2", + "illuminate/database": "^8.73 || ^9 || ^10", "infection/infection": "^0.26 || ^0.27", "jangregor/phpstan-prophecy": "^1", + "mll-lab/graphql-php-scalars": "^6", "mll-lab/php-cs-fixer-config": "^5", + "nunomaduro/larastan": "^1 || ^2", + "orchestra/testbench": "^5 || ^6 || ^7 || ^8", "phpstan/extension-installer": "^1", "phpstan/phpstan": "^1", "phpstan/phpstan-deprecation-rules": "^1", @@ -37,6 +40,9 @@ "rector/rector": "^0.17", "thecodingmachine/phpstan-safe-rule": "^1.2" }, + "suggest": { + "mll-lab/graphql-php-scalars": "To use the provided scalar types for GraphQL servers, requires version ^6" + }, "autoload": { "psr-4": { "MLL\\Utils\\": "src/" diff --git a/src/Microplate/AbstractMicroplate.php b/src/Microplate/AbstractMicroplate.php new file mode 100644 index 0000000..40415bf --- /dev/null +++ b/src/Microplate/AbstractMicroplate.php @@ -0,0 +1,148 @@ + + */ +abstract class AbstractMicroplate +{ + public const EMPTY_WELL = null; + + /** @var TCoordinateSystem */ + public CoordinateSystem $coordinateSystem; + + /** @param TCoordinateSystem $coordinateSystem */ + public function __construct(CoordinateSystem $coordinateSystem) + { + $this->coordinateSystem = $coordinateSystem; + } + + /** @return WellsCollection */ + abstract public function wells(): Collection; + + /** + * @param Coordinates $coordinate + * + * @return TWell|null + */ + public function well(Coordinates $coordinate) + { + return $this->wells()[$coordinate->toString()]; + } + + /** @param Coordinates $coordinate */ + public function isWellEmpty(Coordinates $coordinate): bool + { + return $this->well($coordinate) === self::EMPTY_WELL; + } + + /** @return WellsCollection */ + public function sortedWells(FlowDirection $flowDirection): Collection + { + return $this->wells()->sortBy( + /** + * @param TWell $content + */ + function ($content, string $key) use ($flowDirection): string { + switch ($flowDirection->value) { + case FlowDirection::ROW: + return $key; + case FlowDirection::COLUMN: + $coordinates = Coordinates::fromString($key, $this->coordinateSystem); + + return $coordinates->column . $coordinates->row; + // @codeCoverageIgnoreStart all Enums are listed and this should never happen + default: + throw new UnexpectedFlowDirection($flowDirection); + // @codeCoverageIgnoreEnd + } + }, + SORT_NATURAL + ); + } + + /** @return Collection */ + public function freeWells(): Collection + { + return $this->wells()->filter( + /** + * @param TWell $content + */ + static fn ($content): bool => $content === self::EMPTY_WELL + ); + } + + /** @return Collection */ + public function filledWells(): Collection + { + return $this->wells()->filter( + /** + * @param TWell $content + */ + static fn ($content): bool => $content !== self::EMPTY_WELL + ); + } + + /** @return callable(TWell|null $content, string $coordinatesString): bool */ + public function matchRow(string $row): callable + { + return function ($content, string $coordinatesString) use ($row): bool { + $coordinates = Coordinates::fromString($coordinatesString, $this->coordinateSystem); + + return $coordinates->row === $row; + }; + } + + /** @return callable(TWell|null $content, string $coordinatesString): bool */ + public function matchColumn(int $column): callable + { + return function ($content, string $coordinatesString) use ($column): bool { + $coordinates = Coordinates::fromString($coordinatesString, $this->coordinateSystem); + + return $coordinates->column === $column; + }; + } + + /** + * @deprecated use toWellWithCoordinatesMapper + * + * @return callable(TWell $content, string $coordinatesString): WellWithCoordinates + */ + public function toWellWithCoordinateMapper(): callable + { + return $this->toWellWithCoordinatesMapper(); + } + + /** @return callable(TWell $content, string $coordinatesString): WellWithCoordinates */ + public function toWellWithCoordinatesMapper(): callable + { + return fn ($content, string $coordinatesString): WellWithCoordinates => new WellWithCoordinates( + $content, + Coordinates::fromString($coordinatesString, $this->coordinateSystem) + ); + } + + /** + * Are all filled wells placed in a single connected block without gaps between them? + * + * Returns `false` if all wells are empty. + */ + public function isConsecutive(FlowDirection $flowDirection): bool + { + $positions = $this->filledWells() + ->map( + /** @param TWell $content */ + fn ($content, string $coordinatesString): int => Coordinates::fromString($coordinatesString, $this->coordinateSystem)->position($flowDirection) + ); + + return ($positions->max() - $positions->min() + 1) === $positions->count(); + } +} diff --git a/src/Microplate/AbstractSection.php b/src/Microplate/AbstractSection.php new file mode 100644 index 0000000..f216f25 --- /dev/null +++ b/src/Microplate/AbstractSection.php @@ -0,0 +1,27 @@ + */ + public SectionedMicroplate $sectionedMicroplate; + + /** @var Collection */ + public Collection $sectionItems; + + /** @param SectionedMicroplate $sectionedMicroplate */ + public function __construct(SectionedMicroplate $sectionedMicroplate) + { + $this->sectionedMicroplate = $sectionedMicroplate; + $this->sectionItems = new Collection(); + } + + /** @param TSectionWell $content */ + abstract public function addWell($content): void; +} diff --git a/src/Microplate/Casts/Coordinates96Well.php b/src/Microplate/Casts/Coordinates96Well.php new file mode 100644 index 0000000..510ded1 --- /dev/null +++ b/src/Microplate/Casts/Coordinates96Well.php @@ -0,0 +1,40 @@ +, Coordinates> + */ +final class Coordinates96Well implements CastsAttributes +{ + /** + * @param Model $model + * @param string $key + * @param array $attributes + * + * @return Coordinates + */ + public function get($model, $key, $value, $attributes): Coordinates + { + assert(is_string($value)); + + return Coordinates::fromString($value, new CoordinateSystem96Well()); + } + + /** + * @param Model $model + * @param string $key + * @param array $attributes + */ + public function set($model, $key, $value, $attributes): string + { + assert($value instanceof Coordinates); + + return $value->toString(); + } +} diff --git a/src/Microplate/CoordinateSystem.php b/src/Microplate/CoordinateSystem.php new file mode 100644 index 0000000..f91e144 --- /dev/null +++ b/src/Microplate/CoordinateSystem.php @@ -0,0 +1,128 @@ + */ + abstract public function rows(): array; + + /** @return list */ + abstract public function columns(): array; + + /** + * List of columns, 0-padded to all have the same length. + * + * @return list + */ + public function paddedColumns(): array + { + $paddedColumns = []; + foreach ($this->columns() as $column) { + $paddedColumns[] = $this->padColumn($column); + } + + return $paddedColumns; + } + + /** 0-pad column to be as long as the longest column in the coordinate system. */ + public function padColumn(int $column): string + { + $maxColumnLength = strlen((string) $this->columnsCount()); + + return str_pad((string) $column, $maxColumnLength, '0', STR_PAD_LEFT); + } + + public function rowForRowFlowPosition(int $position): string + { + return $this->rows()[floor(($position - 1) / $this->columnsCount())]; + } + + public function rowForColumnFlowPosition(int $position): string + { + return $this->rows()[($position - 1) % $this->rowsCount()]; + } + + public function columnForRowFlowPosition(int $position): int + { + return $this->columns()[($position - 1) % $this->columnsCount()]; + } + + public function columnForColumnFlowPosition(int $position): int + { + return $this->columns()[floor(($position - 1) / $this->rowsCount())]; + } + + public function positionsCount(): int + { + return $this->columnsCount() * $this->rowsCount(); + } + + /** + * Returns all possible coordinates of the system, ordered by column then row. + * + * e.g. A1, A2, B1, B2 + * + * @return iterable> + */ + public function all(): iterable + { + foreach ($this->columns() as $column) { + foreach ($this->rows() as $row) { + yield new Coordinates($row, $column, $this); + } + } + } + + /** + * Returns the coordinates of the first row and first column. + * + * @return Coordinates<$this> + */ + public function first(): Coordinates + { + $firstRow = Arr::first($this->rows()); + if (! is_string($firstRow)) { + throw new \Exception('First row must be string.'); + } + + $firstColumn = Arr::first($this->columns()); + if (! is_int($firstColumn)) { + throw new \Exception('First column must be string.'); + } + + return new Coordinates($firstRow, $firstColumn, $this); + } + + /** + * Returns the coordinates of the last row and last column. + * + * @return Coordinates<$this> + */ + public function last(): Coordinates + { + $lastRow = Arr::last($this->rows()); + if (! is_string($lastRow)) { + throw new \Exception('Last row must be string.'); + } + + $lastColumn = Arr::last($this->columns()); + if (! is_int($lastColumn)) { + throw new \Exception('Last column must be string.'); + } + + return new Coordinates($lastRow, $lastColumn, $this); + } + + public function rowsCount(): int + { + return count($this->rows()); + } + + public function columnsCount(): int + { + return count($this->columns()); + } +} diff --git a/src/Microplate/CoordinateSystem12Well.php b/src/Microplate/CoordinateSystem12Well.php new file mode 100644 index 0000000..93d2a02 --- /dev/null +++ b/src/Microplate/CoordinateSystem12Well.php @@ -0,0 +1,19 @@ +rows(); + if (! in_array($row, $rows, true)) { + $rowList = \implode(',', $rows); + throw new \InvalidArgumentException("Expected a row with value of {$rowList}, got {$row}."); + } + $this->row = $row; + + $columns = $coordinateSystem->columns(); + if (! in_array($column, $columns, true)) { + $columnsList = \implode(',', $columns); + throw new \InvalidArgumentException("Expected a column with value of {$columnsList}, got {$column}."); + } + $this->column = $column; + + $this->coordinateSystem = $coordinateSystem; + } + + /** + * @template TCoord of CoordinateSystem + * + * @param TCoord $coordinateSystem + * + * @return static + */ + public static function fromString(string $coordinatesString, CoordinateSystem $coordinateSystem): self + { + $rows = $coordinateSystem->rows(); + $rowsOptions = \implode('|', $rows); + + $columns = [ + ...$coordinateSystem->columns(), + ...$coordinateSystem->paddedColumns(), + ]; + $columnsOptions = \implode('|', $columns); + + $valid = preg_match( + "/^({$rowsOptions})({$columnsOptions})\$/", + $coordinatesString, + $matches + ); + + if ($valid === 0) { + $firstValidExample = Arr::first($rows) . Arr::first($columns); + $lastValidExample = Arr::last($rows) . Arr::last($columns); + $coordinateSystemClass = \get_class($coordinateSystem); + throw new \InvalidArgumentException("Expected coordinates between {$firstValidExample} and {$lastValidExample} for {$coordinateSystemClass}, got: {$coordinatesString}."); + } + + return new self($matches[1], (int) $matches[2], $coordinateSystem); + } + + public function toString(): string + { + return $this->row . $this->column; + } + + /** Format the coordinates with the column 0-padded so all are the same length. */ + public function toPaddedString(): string + { + return $this->row . $this->coordinateSystem->padColumn($this->column); + } + + /** + * @param TCoordinateSystem $coordinateSystem + * + * @return static + */ + public static function fromPosition(int $position, FlowDirection $direction, CoordinateSystem $coordinateSystem): self + { + self::assertPositionInRange($coordinateSystem, $position); + + switch ($direction->value) { + case FlowDirection::COLUMN: + return new self( + $coordinateSystem->rowForColumnFlowPosition($position), + $coordinateSystem->columnForColumnFlowPosition($position), + $coordinateSystem + ); + + case FlowDirection::ROW: + return new self( + $coordinateSystem->rowForRowFlowPosition($position), + $coordinateSystem->columnForRowFlowPosition($position), + $coordinateSystem + ); + // @codeCoverageIgnoreStart all Enums are listed and this should never happen + default: + throw new UnexpectedFlowDirection($direction); + // @codeCoverageIgnoreEnd + } + } + + public function position(FlowDirection $direction): int + { + /** @var int $rowIndex Must be found, since __construct enforces $this->row is valid */ + $rowIndex = array_search($this->row, $this->coordinateSystem->rows(), true); + + /** @var int $columnIndex Must be found, since __construct enforces $this->column is valid */ + $columnIndex = array_search($this->column, $this->coordinateSystem->columns(), true); + + switch ($direction->value) { + case FlowDirection::ROW: + return $rowIndex * \count($this->coordinateSystem->columns()) + $columnIndex + 1; + case FlowDirection::COLUMN: + return $columnIndex * \count($this->coordinateSystem->rows()) + $rowIndex + 1; + // @codeCoverageIgnoreStart all Enums are listed and this should never happen + default: + throw new UnexpectedFlowDirection($direction); + // @codeCoverageIgnoreEnd + } + } + + private static function assertPositionInRange(CoordinateSystem $coordinateSystem, int $position): void + { + if (! in_array($position, range(self::MIN_POSITION, $coordinateSystem->positionsCount()), true)) { + throw new \InvalidArgumentException("Expected a position between 1-{$coordinateSystem->positionsCount()}, got: {$position}."); + } + } +} diff --git a/src/Microplate/Enums/FlowDirection.php b/src/Microplate/Enums/FlowDirection.php new file mode 100644 index 0000000..419b166 --- /dev/null +++ b/src/Microplate/Enums/FlowDirection.php @@ -0,0 +1,26 @@ +value = $value; + } + + public static function ROW(): self + { + return new self(self::ROW); + } + + public static function COLUMN(): self + { + return new self(self::COLUMN); + } +} diff --git a/src/Microplate/Exceptions/MicroplateIsFullException.php b/src/Microplate/Exceptions/MicroplateIsFullException.php new file mode 100644 index 0000000..fb436d4 --- /dev/null +++ b/src/Microplate/Exceptions/MicroplateIsFullException.php @@ -0,0 +1,11 @@ +value}."); + } +} diff --git a/src/Microplate/Exceptions/WellNotEmptyException.php b/src/Microplate/Exceptions/WellNotEmptyException.php new file mode 100644 index 0000000..f5db0d6 --- /dev/null +++ b/src/Microplate/Exceptions/WellNotEmptyException.php @@ -0,0 +1,5 @@ + + */ +final class FullColumnSection extends AbstractSection +{ + public function __construct(SectionedMicroplate $sectionedMicroplate) + { + parent::__construct($sectionedMicroplate); + $this->growSection(); + } + + /** + * @param TSectionWell $content + * + * @throws MicroplateIsFullException + * @throws SectionIsFullException + */ + public function addWell($content): void + { + if ($this->sectionedMicroplate->freeWells()->isEmpty()) { + throw new MicroplateIsFullException(); + } + + $nextReservedWell = $this->nextReservedWell(); + if ($nextReservedWell !== false) { + $this->sectionItems[$nextReservedWell] = $content; + + return; + } + + $this->growSection(); + + $nextReservedWell = $this->nextReservedWell(); + assert(is_int($nextReservedWell), 'Guaranteed to be found after we grew the section'); + + $this->sectionItems[$nextReservedWell] = $content; + } + + /** + * Grows the section by initializing a new column with empty wells. + * + * @throws SectionIsFullException + */ + private function growSection(): void + { + if (! $this->sectionCanGrow()) { + throw new SectionIsFullException(); + } + + foreach ($this->sectionedMicroplate->coordinateSystem->rows() as $row) { + $this->sectionItems->push(AbstractMicroplate::EMPTY_WELL); + } + } + + /** @return false|int */ + private function nextReservedWell() + { + $search = $this->sectionItems->search(AbstractMicroplate::EMPTY_WELL); + assert($search === false || is_int($search)); + + return $search; + } + + private function sectionCanGrow(): bool + { + $totalReservedColumns = $this->sectionedMicroplate + ->sections + ->sum(static fn (self $section): int => $section->reservedColumns()); + + $availableColumns = $this->sectionedMicroplate + ->coordinateSystem + ->columnsCount(); + + return $totalReservedColumns < $availableColumns; + } + + private function reservedColumns(): int + { + return (int) ceil($this->sectionItems->count() / $this->sectionedMicroplate->coordinateSystem->rowsCount()); + } +} diff --git a/src/Microplate/Microplate.php b/src/Microplate/Microplate.php new file mode 100644 index 0000000..41a5ea1 --- /dev/null +++ b/src/Microplate/Microplate.php @@ -0,0 +1,129 @@ + + * + * @phpstan-type WellsCollection Collection + */ +final class Microplate extends AbstractMicroplate +{ + /** @var WellsCollection */ + protected Collection $wells; + + /** @param TCoordinateSystem $coordinateSystem */ + public function __construct(CoordinateSystem $coordinateSystem) + { + parent::__construct($coordinateSystem); + + $this->clearWells(); + } + + /** @return WellsCollection */ + public function wells(): Collection + { + return $this->wells; + } + + /** @param Coordinates $coordinates */ + public static function position(Coordinates $coordinates, FlowDirection $direction): int + { + return $coordinates->position($direction); + } + + /** + * @param TWell $content + * @param Coordinates $coordinates + * + * @throws WellNotEmptyException + */ + public function addWell(Coordinates $coordinates, $content): void + { + $this->assertIsWellEmpty($coordinates, $content); + $this->setWell($coordinates, $content); + } + + /** + * Set the well at the given coordinates to the given content. + * + * @param Coordinates $coordinates + * @param TWell $content + */ + public function setWell(Coordinates $coordinates, $content): void + { + $this->wells[$coordinates->toString()] = $content; + } + + /** + * @param Coordinates $coordinates + * @param TWell $content + * + * @throws WellNotEmptyException + */ + private function assertIsWellEmpty(Coordinates $coordinates, $content): void + { + if (! $this->isWellEmpty($coordinates)) { + throw new WellNotEmptyException( + 'Well with coordinates "' . $coordinates->toString() . '" is not empty. Use setWell() to overwrite the coordinate. Well content "' . serialize($content) . '" was not added.' + ); + } + } + + /** Clearing the wells will reinitialize all well position of the coordinate system. */ + public function clearWells(): void + { + /** + * Flow direction is irrelevant during initialization, it is not a property of + * a plate but rather a property of the access to the plate. + */ + + /** @var array $wells */ + $wells = []; + foreach ($this->coordinateSystem->all() as $coordinate) { + $wells[$coordinate->toString()] = self::EMPTY_WELL; + } + + $this->wells = new Collection($wells); + } + + /** + * @param TWell $content + * + *@throws MicroplateIsFullException + * + * @return Coordinates + */ + public function addToNextFreeWell($content, FlowDirection $flowDirection): Coordinates + { + $coordinates = $this->nextFreeWellCoordinates($flowDirection); + $this->wells[$coordinates->toString()] = $content; + + return $coordinates; + } + + /** + *@throws MicroplateIsFullException + * + * @return Coordinates + */ + public function nextFreeWellCoordinates(FlowDirection $flowDirection): Coordinates + { + $coordinatesString = $this->sortedWells($flowDirection) + ->search(self::EMPTY_WELL); + + if (! is_string($coordinatesString)) { + throw new MicroplateIsFullException(); + } + + return Coordinates::fromString($coordinatesString, $this->coordinateSystem); + } +} diff --git a/src/Microplate/MicroplateSet/Location.php b/src/Microplate/MicroplateSet/Location.php new file mode 100644 index 0000000..efe4d09 --- /dev/null +++ b/src/Microplate/MicroplateSet/Location.php @@ -0,0 +1,24 @@ + */ + public Coordinates $coordinates; + + /** @param \MLL\Utils\Microplate\Coordinates $coordinates */ + public function __construct(Coordinates $coordinates, string $plateID) + { + $this->coordinates = $coordinates; + $this->plateID = $plateID; + } +} diff --git a/src/Microplate/MicroplateSet/MicroplateSet.php b/src/Microplate/MicroplateSet/MicroplateSet.php new file mode 100644 index 0000000..da9f7b3 --- /dev/null +++ b/src/Microplate/MicroplateSet/MicroplateSet.php @@ -0,0 +1,53 @@ +coordinateSystem = $coordinateSystem; + } + + /** @return list */ + abstract public function plateIDs(): array; + + public function plateCount(): int + { + return count($this->plateIDs()); + } + + public function positionsCount(): int + { + return $this->coordinateSystem->positionsCount() * $this->plateCount(); + } + + /** @return Location */ + public function locationFromPosition(int $setPosition, FlowDirection $direction): Location + { + $positionsCount = $this->positionsCount(); + if ($setPosition > $positionsCount || $setPosition < Coordinates::MIN_POSITION) { + throw new \OutOfRangeException("Expected a position between 1-{$positionsCount}, got: {$setPosition}."); + } + + $plateIndex = (int) floor(($setPosition - 1) / $this->coordinateSystem->positionsCount()); + $positionOnSinglePlate = $setPosition - ($plateIndex * $this->coordinateSystem->positionsCount()); + + /** @phpstan-ignore-next-line Generic inference is too weak to recognize this code is correct */ + return new Location( + Coordinates::fromPosition($positionOnSinglePlate, $direction, $this->coordinateSystem), + $this->plateIDs()[$plateIndex] + ); + } +} diff --git a/src/Microplate/MicroplateSet/MicroplateSetAB.php b/src/Microplate/MicroplateSet/MicroplateSetAB.php new file mode 100644 index 0000000..cd95022 --- /dev/null +++ b/src/Microplate/MicroplateSet/MicroplateSetAB.php @@ -0,0 +1,21 @@ + + */ +final class MicroplateSetAB extends MicroplateSet +{ + /** Duplicates @see MicroplateSet::plateCount() for static contexts. */ + public const PLATE_COUNT = 2; + + public function plateIDs(): array + { + return ['A', 'B']; + } +} diff --git a/src/Microplate/MicroplateSet/MicroplateSetABCD.php b/src/Microplate/MicroplateSet/MicroplateSetABCD.php new file mode 100644 index 0000000..1b1a06a --- /dev/null +++ b/src/Microplate/MicroplateSet/MicroplateSetABCD.php @@ -0,0 +1,21 @@ + + */ +final class MicroplateSetABCD extends MicroplateSet +{ + /** Duplicates @see MicroplateSet::plateCount() for static contexts. */ + public const PLATE_COUNT = 4; + + public function plateIDs(): array + { + return ['A', 'B', 'C', 'D']; + } +} diff --git a/src/Microplate/MicroplateSet/MicroplateSetABCDE.php b/src/Microplate/MicroplateSet/MicroplateSetABCDE.php new file mode 100644 index 0000000..1a67c36 --- /dev/null +++ b/src/Microplate/MicroplateSet/MicroplateSetABCDE.php @@ -0,0 +1,21 @@ + + */ +final class MicroplateSetABCDE extends MicroplateSet +{ + /** Duplicates @see MicroplateSet::plateCount() for static contexts. */ + public const PLATE_COUNT = 5; + + public function plateIDs(): array + { + return ['A', 'B', 'C', 'D', 'E']; + } +} diff --git a/src/Microplate/Scalars/Column96Well.php b/src/Microplate/Scalars/Column96Well.php new file mode 100644 index 0000000..b4cdf8e --- /dev/null +++ b/src/Microplate/Scalars/Column96Well.php @@ -0,0 +1,56 @@ +isValueInExpectedRange($value)) { + return $value; + } + + $notInRange = Utils::printSafe($value); + throw new \InvalidArgumentException("Value not in range: {$notInRange}."); + } + + public function parseValue($value) + { + if (is_int($value) && $this->isValueInExpectedRange($value)) { + return $value; + } + + $notInRange = Utils::printSafe($value); + throw new Error("Value not in range: {$notInRange}."); + } + + public function parseLiteral(Node $valueNode, ?array $variables = null) + { + if ($valueNode instanceof IntValueNode) { + $value = (int) $valueNode->value; + if ($this->isValueInExpectedRange($value)) { + return $value; + } + } + + $notInRange = Printer::doPrint($valueNode); + throw new Error("Value not in range: {$notInRange}.", $valueNode); + } + + private function isValueInExpectedRange(int $value): bool + { + return $value <= self::MAX_INT && $value >= self::MIN_INT; + } +} diff --git a/src/Microplate/Scalars/Row96Well.php b/src/Microplate/Scalars/Row96Well.php new file mode 100644 index 0000000..736770e --- /dev/null +++ b/src/Microplate/Scalars/Row96Well.php @@ -0,0 +1,15 @@ + + */ +final class Section extends AbstractSection +{ + /** + * @param TSectionWell $content + * + * @throws MicroplateIsFullException + */ + public function addWell($content): void + { + if ($this->sectionedMicroplate->freeWells()->isEmpty()) { + throw new MicroplateIsFullException(); + } + + $this->sectionItems->push($content); + } +} diff --git a/src/Microplate/SectionedMicroplate.php b/src/Microplate/SectionedMicroplate.php new file mode 100644 index 0000000..8111b43 --- /dev/null +++ b/src/Microplate/SectionedMicroplate.php @@ -0,0 +1,72 @@ + + */ +final class SectionedMicroplate extends AbstractMicroplate +{ + /** @var Collection */ + public Collection $sections; + + /** @param TCoordinateSystem $coordinateSystem */ + public function __construct(CoordinateSystem $coordinateSystem) + { + parent::__construct($coordinateSystem); + + $this->clearSections(); + } + + /** + * @template TAddSection of TSection + * + * @param class-string $sectionClass + * + * @return TAddSection + */ + public function addSection(string $sectionClass): AbstractSection + { + // @phpstan-ignore-next-line generic mismatch + return $this->sections[] = new $sectionClass($this); + } + + /** @param TSection $section */ + public function removeSection(AbstractSection $section): void + { + foreach ($this->sections as $i => $s) { + if ($s === $section) { + unset($this->sections[$i]); + } + } + } + + public function wells(): Collection + { + return $this->sections + ->map(fn (AbstractSection $section): Collection => $section->sectionItems) + ->flatten(1) + ->values() + ->zip($this->coordinateSystem->all()) + ->map(fn (Collection $mapping): array => $mapping->all()) + ->mapWithKeys(function (array $mapping): array { + [$sectionItem, $coordinates] = $mapping; + assert($coordinates instanceof Coordinates); + + return [$coordinates->toString() => $sectionItem]; + }); + } + + public function clearSections(): void + { + $this->sections = new Collection(); + } +} diff --git a/src/Microplate/WellWithCoordinates.php b/src/Microplate/WellWithCoordinates.php new file mode 100644 index 0000000..c332c50 --- /dev/null +++ b/src/Microplate/WellWithCoordinates.php @@ -0,0 +1,26 @@ + */ + public Coordinates $coordinates; + + /** + * @param TWell $content + * @param Coordinates $coordinates + */ + public function __construct($content, Coordinates $coordinates) + { + $this->content = $content; + $this->coordinates = $coordinates; + } +} diff --git a/src/QxManager/FilledWell.php b/src/QxManager/FilledWell.php index ff16603..dd5484b 100644 --- a/src/QxManager/FilledWell.php +++ b/src/QxManager/FilledWell.php @@ -2,8 +2,8 @@ namespace MLL\Utils\QxManager; -use Mll\Microplate\Coordinates; -use Mll\Microplate\CoordinateSystem96Well; +use MLL\Utils\Microplate\Coordinates; +use MLL\Utils\Microplate\CoordinateSystem96Well; class FilledWell { diff --git a/src/QxManager/QxManagerSampleSheet.php b/src/QxManager/QxManagerSampleSheet.php index 2eca97b..e445397 100644 --- a/src/QxManager/QxManagerSampleSheet.php +++ b/src/QxManager/QxManagerSampleSheet.php @@ -3,10 +3,10 @@ namespace MLL\Utils\QxManager; use Carbon\CarbonInterface; -use Mll\Microplate\Coordinates; -use Mll\Microplate\CoordinateSystem96Well; -use Mll\Microplate\Enums\FlowDirection; -use Mll\Microplate\Microplate; +use MLL\Utils\Microplate\Coordinates; +use MLL\Utils\Microplate\CoordinateSystem96Well; +use MLL\Utils\Microplate\Enums\FlowDirection; +use MLL\Utils\Microplate\Microplate; use MLL\Utils\StringUtil; class QxManagerSampleSheet diff --git a/tests/Microplate/CoordinateSystemTest.php b/tests/Microplate/CoordinateSystemTest.php new file mode 100644 index 0000000..923b61a --- /dev/null +++ b/tests/Microplate/CoordinateSystemTest.php @@ -0,0 +1,39 @@ +first(); + self::assertSame($expectedFirst, $actualFirst->toString()); + self::assertSame($coordinateSystem, $actualFirst->coordinateSystem); + + $actualLast = $coordinateSystem->last(); + self::assertSame($expectedLast, $actualLast->toString()); + self::assertSame($coordinateSystem, $actualLast->coordinateSystem); + } + + /** @return iterable */ + public static function firstLast(): iterable + { + yield [new CoordinateSystem12Well(), 'A1', 'C4']; + yield [new CoordinateSystem48Well(), 'A1', 'F8']; + yield [new CoordinateSystem96Well(), 'A1', 'H12']; + } + + public function testPositionsCount(): void + { + self::assertSame(CoordinateSystem12Well::POSITIONS_COUNT, (new CoordinateSystem12Well())->positionsCount()); + self::assertSame(CoordinateSystem48Well::POSITIONS_COUNT, (new CoordinateSystem48Well())->positionsCount()); + self::assertSame(CoordinateSystem96Well::POSITIONS_COUNT, (new CoordinateSystem96Well())->positionsCount()); + } +} diff --git a/tests/Microplate/CoordinatesTest.php b/tests/Microplate/CoordinatesTest.php new file mode 100644 index 0000000..8609c70 --- /dev/null +++ b/tests/Microplate/CoordinatesTest.php @@ -0,0 +1,824 @@ +toString()); + } + + /** @dataProvider dataProvider96Well */ + public function testCanConstructFromPosition(string $row, int $column, int $rowFlowPosition, int $columnFlowPosition): void + { + // test for Column-FlowDirection + $coordinates = Coordinates::fromPosition( + $columnFlowPosition, + FlowDirection::COLUMN(), + new CoordinateSystem96Well() + ); + self::assertSame($row, $coordinates->row); + self::assertSame($column, $coordinates->column); + + // test for Row-FlowDirection + $coordinates = Coordinates::fromPosition( + $rowFlowPosition, + FlowDirection::ROW(), + new CoordinateSystem96Well() + ); + self::assertSame($row, $coordinates->row); + self::assertSame($column, $coordinates->column); + } + + /** @dataProvider dataProvider96Well */ + public function testFromCoordinatesString(string $row, int $column, int $rowFlowPosition, int $columnFlowPosition): void + { + $coordinates = Coordinates::fromString($row . $column, new CoordinateSystem96Well()); + self::assertSame($row, $coordinates->row); + self::assertSame($column, $coordinates->column); + } + + /** @dataProvider dataProviderPadded96Well */ + public function testFromPaddedCoordinatesString(string $paddedCoordinates, string $row, int $column): void + { + $coordinatesFromPadded = Coordinates::fromString($paddedCoordinates, new CoordinateSystem96Well()); + self::assertSame($row, $coordinatesFromPadded->row); + self::assertSame($column, $coordinatesFromPadded->column); + self::assertSame($paddedCoordinates, $coordinatesFromPadded->toPaddedString()); + } + + /** + * @return iterable + */ + public static function dataProviderPadded96Well(): iterable + { + yield [ + 'paddedCoordinates' => 'A01', + 'row' => 'A', + 'column' => 1, + ]; + yield [ + 'paddedCoordinates' => 'C05', + 'row' => 'C', + 'column' => 5, + ]; + yield [ + 'paddedCoordinates' => 'H12', + 'row' => 'H', + 'column' => 12, + ]; + yield [ + 'paddedCoordinates' => 'D10', + 'row' => 'D', + 'column' => 10, + ]; + } + + /** @dataProvider dataProvider96Well */ + public function testPosition96Well(string $row, int $column, int $rowFlowPosition, int $columnFlowPosition): void + { + $coordinates = new Coordinates($row, $column, new CoordinateSystem96Well()); + self::assertSame($columnFlowPosition, $coordinates->position(FlowDirection::COLUMN())); + self::assertSame($rowFlowPosition, $coordinates->position(FlowDirection::ROW())); + } + + /** + * @return iterable + */ + public static function dataProvider96Well(): iterable + { + yield [ + 'row' => 'A', + 'column' => 1, + 'rowFlowPosition' => 1, + 'columnFlowPosition' => 1, + ]; + yield [ + 'row' => 'B', + 'column' => 1, + 'rowFlowPosition' => 13, + 'columnFlowPosition' => 2, + ]; + yield [ + 'row' => 'C', + 'column' => 1, + 'rowFlowPosition' => 25, + 'columnFlowPosition' => 3, + ]; + yield [ + 'row' => 'D', + 'column' => 1, + 'rowFlowPosition' => 37, + 'columnFlowPosition' => 4, + ]; + yield [ + 'row' => 'E', + 'column' => 1, + 'rowFlowPosition' => 49, + 'columnFlowPosition' => 5, + ]; + yield [ + 'row' => 'F', + 'column' => 1, + 'rowFlowPosition' => 61, + 'columnFlowPosition' => 6, + ]; + yield [ + 'row' => 'G', + 'column' => 1, + 'rowFlowPosition' => 73, + 'columnFlowPosition' => 7, + ]; + yield [ + 'row' => 'H', + 'column' => 1, + 'rowFlowPosition' => 85, + 'columnFlowPosition' => 8, + ]; + yield [ + 'row' => 'A', + 'column' => 2, + 'rowFlowPosition' => 2, + 'columnFlowPosition' => 9, + ]; + yield [ + 'row' => 'B', + 'column' => 2, + 'rowFlowPosition' => 14, + 'columnFlowPosition' => 10, + ]; + yield [ + 'row' => 'C', + 'column' => 2, + 'rowFlowPosition' => 26, + 'columnFlowPosition' => 11, + ]; + yield [ + 'row' => 'D', + 'column' => 2, + 'rowFlowPosition' => 38, + 'columnFlowPosition' => 12, + ]; + yield [ + 'row' => 'E', + 'column' => 2, + 'rowFlowPosition' => 50, + 'columnFlowPosition' => 13, + ]; + yield [ + 'row' => 'F', + 'column' => 2, + 'rowFlowPosition' => 62, + 'columnFlowPosition' => 14, + ]; + yield [ + 'row' => 'G', + 'column' => 2, + 'rowFlowPosition' => 74, + 'columnFlowPosition' => 15, + ]; + yield [ + 'row' => 'H', + 'column' => 2, + 'rowFlowPosition' => 86, + 'columnFlowPosition' => 16, + ]; + yield [ + 'row' => 'A', + 'column' => 3, + 'rowFlowPosition' => 3, + 'columnFlowPosition' => 17, + ]; + yield [ + 'row' => 'B', + 'column' => 3, + 'rowFlowPosition' => 15, + 'columnFlowPosition' => 18, + ]; + yield [ + 'row' => 'C', + 'column' => 3, + 'rowFlowPosition' => 27, + 'columnFlowPosition' => 19, + ]; + yield [ + 'row' => 'D', + 'column' => 3, + 'rowFlowPosition' => 39, + 'columnFlowPosition' => 20, + ]; + yield [ + 'row' => 'E', + 'column' => 3, + 'rowFlowPosition' => 51, + 'columnFlowPosition' => 21, + ]; + yield [ + 'row' => 'F', + 'column' => 3, + 'rowFlowPosition' => 63, + 'columnFlowPosition' => 22, + ]; + yield [ + 'row' => 'G', + 'column' => 3, + 'rowFlowPosition' => 75, + 'columnFlowPosition' => 23, + ]; + yield [ + 'row' => 'H', + 'column' => 3, + 'rowFlowPosition' => 87, + 'columnFlowPosition' => 24, + ]; + yield [ + 'row' => 'A', + 'column' => 4, + 'rowFlowPosition' => 4, + 'columnFlowPosition' => 25, + ]; + yield [ + 'row' => 'B', + 'column' => 4, + 'rowFlowPosition' => 16, + 'columnFlowPosition' => 26, + ]; + yield [ + 'row' => 'C', + 'column' => 4, + 'rowFlowPosition' => 28, + 'columnFlowPosition' => 27, + ]; + yield [ + 'row' => 'D', + 'column' => 4, + 'rowFlowPosition' => 40, + 'columnFlowPosition' => 28, + ]; + yield [ + 'row' => 'E', + 'column' => 4, + 'rowFlowPosition' => 52, + 'columnFlowPosition' => 29, + ]; + yield [ + 'row' => 'F', + 'column' => 4, + 'rowFlowPosition' => 64, + 'columnFlowPosition' => 30, + ]; + yield [ + 'row' => 'G', + 'column' => 4, + 'rowFlowPosition' => 76, + 'columnFlowPosition' => 31, + ]; + yield [ + 'row' => 'H', + 'column' => 4, + 'rowFlowPosition' => 88, + 'columnFlowPosition' => 32, + ]; + yield [ + 'row' => 'A', + 'column' => 5, + 'rowFlowPosition' => 5, + 'columnFlowPosition' => 33, + ]; + yield [ + 'row' => 'B', + 'column' => 5, + 'rowFlowPosition' => 17, + 'columnFlowPosition' => 34, + ]; + yield [ + 'row' => 'C', + 'column' => 5, + 'rowFlowPosition' => 29, + 'columnFlowPosition' => 35, + ]; + yield [ + 'row' => 'D', + 'column' => 5, + 'rowFlowPosition' => 41, + 'columnFlowPosition' => 36, + ]; + yield [ + 'row' => 'E', + 'column' => 5, + 'rowFlowPosition' => 53, + 'columnFlowPosition' => 37, + ]; + yield [ + 'row' => 'F', + 'column' => 5, + 'rowFlowPosition' => 65, + 'columnFlowPosition' => 38, + ]; + yield [ + 'row' => 'G', + 'column' => 5, + 'rowFlowPosition' => 77, + 'columnFlowPosition' => 39, + ]; + yield [ + 'row' => 'H', + 'column' => 5, + 'rowFlowPosition' => 89, + 'columnFlowPosition' => 40, + ]; + yield [ + 'row' => 'A', + 'column' => 6, + 'rowFlowPosition' => 6, + 'columnFlowPosition' => 41, + ]; + yield [ + 'row' => 'B', + 'column' => 6, + 'rowFlowPosition' => 18, + 'columnFlowPosition' => 42, + ]; + yield [ + 'row' => 'C', + 'column' => 6, + 'rowFlowPosition' => 30, + 'columnFlowPosition' => 43, + ]; + yield [ + 'row' => 'D', + 'column' => 6, + 'rowFlowPosition' => 42, + 'columnFlowPosition' => 44, + ]; + yield [ + 'row' => 'E', + 'column' => 6, + 'rowFlowPosition' => 54, + 'columnFlowPosition' => 45, + ]; + yield [ + 'row' => 'F', + 'column' => 6, + 'rowFlowPosition' => 66, + 'columnFlowPosition' => 46, + ]; + yield [ + 'row' => 'G', + 'column' => 6, + 'rowFlowPosition' => 78, + 'columnFlowPosition' => 47, + ]; + yield [ + 'row' => 'H', + 'column' => 6, + 'rowFlowPosition' => 90, + 'columnFlowPosition' => 48, + ]; + yield [ + 'row' => 'A', + 'column' => 7, + 'rowFlowPosition' => 7, + 'columnFlowPosition' => 49, + ]; + yield [ + 'row' => 'B', + 'column' => 7, + 'rowFlowPosition' => 19, + 'columnFlowPosition' => 50, + ]; + yield [ + 'row' => 'C', + 'column' => 7, + 'rowFlowPosition' => 31, + 'columnFlowPosition' => 51, + ]; + yield [ + 'row' => 'D', + 'column' => 7, + 'rowFlowPosition' => 43, + 'columnFlowPosition' => 52, + ]; + yield [ + 'row' => 'E', + 'column' => 7, + 'rowFlowPosition' => 55, + 'columnFlowPosition' => 53, + ]; + yield [ + 'row' => 'F', + 'column' => 7, + 'rowFlowPosition' => 67, + 'columnFlowPosition' => 54, + ]; + yield [ + 'row' => 'G', + 'column' => 7, + 'rowFlowPosition' => 79, + 'columnFlowPosition' => 55, + ]; + yield [ + 'row' => 'H', + 'column' => 7, + 'rowFlowPosition' => 91, + 'columnFlowPosition' => 56, + ]; + yield [ + 'row' => 'A', + 'column' => 8, + 'rowFlowPosition' => 8, + 'columnFlowPosition' => 57, + ]; + yield [ + 'row' => 'B', + 'column' => 8, + 'rowFlowPosition' => 20, + 'columnFlowPosition' => 58, + ]; + yield [ + 'row' => 'C', + 'column' => 8, + 'rowFlowPosition' => 32, + 'columnFlowPosition' => 59, + ]; + yield [ + 'row' => 'D', + 'column' => 8, + 'rowFlowPosition' => 44, + 'columnFlowPosition' => 60, + ]; + yield [ + 'row' => 'E', + 'column' => 8, + 'rowFlowPosition' => 56, + 'columnFlowPosition' => 61, + ]; + yield [ + 'row' => 'F', + 'column' => 8, + 'rowFlowPosition' => 68, + 'columnFlowPosition' => 62, + ]; + yield [ + 'row' => 'G', + 'column' => 8, + 'rowFlowPosition' => 80, + 'columnFlowPosition' => 63, + ]; + yield [ + 'row' => 'H', + 'column' => 8, + 'rowFlowPosition' => 92, + 'columnFlowPosition' => 64, + ]; + yield [ + 'row' => 'A', + 'column' => 9, + 'rowFlowPosition' => 9, + 'columnFlowPosition' => 65, + ]; + yield [ + 'row' => 'B', + 'column' => 9, + 'rowFlowPosition' => 21, + 'columnFlowPosition' => 66, + ]; + yield [ + 'row' => 'C', + 'column' => 9, + 'rowFlowPosition' => 33, + 'columnFlowPosition' => 67, + ]; + yield [ + 'row' => 'D', + 'column' => 9, + 'rowFlowPosition' => 45, + 'columnFlowPosition' => 68, + ]; + yield [ + 'row' => 'E', + 'column' => 9, + 'rowFlowPosition' => 57, + 'columnFlowPosition' => 69, + ]; + yield [ + 'row' => 'F', + 'column' => 9, + 'rowFlowPosition' => 69, + 'columnFlowPosition' => 70, + ]; + yield [ + 'row' => 'G', + 'column' => 9, + 'rowFlowPosition' => 81, + 'columnFlowPosition' => 71, + ]; + yield [ + 'row' => 'H', + 'column' => 9, + 'rowFlowPosition' => 93, + 'columnFlowPosition' => 72, + ]; + yield [ + 'row' => 'A', + 'column' => 10, + 'rowFlowPosition' => 10, + 'columnFlowPosition' => 73, + ]; + yield [ + 'row' => 'B', + 'column' => 10, + 'rowFlowPosition' => 22, + 'columnFlowPosition' => 74, + ]; + yield [ + 'row' => 'C', + 'column' => 10, + 'rowFlowPosition' => 34, + 'columnFlowPosition' => 75, + ]; + yield [ + 'row' => 'D', + 'column' => 10, + 'rowFlowPosition' => 46, + 'columnFlowPosition' => 76, + ]; + yield [ + 'row' => 'E', + 'column' => 10, + 'rowFlowPosition' => 58, + 'columnFlowPosition' => 77, + ]; + yield [ + 'row' => 'F', + 'column' => 10, + 'rowFlowPosition' => 70, + 'columnFlowPosition' => 78, + ]; + yield [ + 'row' => 'G', + 'column' => 10, + 'rowFlowPosition' => 82, + 'columnFlowPosition' => 79, + ]; + yield [ + 'row' => 'H', + 'column' => 10, + 'rowFlowPosition' => 94, + 'columnFlowPosition' => 80, + ]; + yield [ + 'row' => 'A', + 'column' => 11, + 'rowFlowPosition' => 11, + 'columnFlowPosition' => 81, + ]; + yield [ + 'row' => 'B', + 'column' => 11, + 'rowFlowPosition' => 23, + 'columnFlowPosition' => 82, + ]; + yield [ + 'row' => 'C', + 'column' => 11, + 'rowFlowPosition' => 35, + 'columnFlowPosition' => 83, + ]; + yield [ + 'row' => 'D', + 'column' => 11, + 'rowFlowPosition' => 47, + 'columnFlowPosition' => 84, + ]; + yield [ + 'row' => 'E', + 'column' => 11, + 'rowFlowPosition' => 59, + 'columnFlowPosition' => 85, + ]; + yield [ + 'row' => 'F', + 'column' => 11, + 'rowFlowPosition' => 71, + 'columnFlowPosition' => 86, + ]; + yield [ + 'row' => 'G', + 'column' => 11, + 'rowFlowPosition' => 83, + 'columnFlowPosition' => 87, + ]; + yield [ + 'row' => 'H', + 'column' => 11, + 'rowFlowPosition' => 95, + 'columnFlowPosition' => 88, + ]; + yield [ + 'row' => 'A', + 'column' => 12, + 'rowFlowPosition' => 12, + 'columnFlowPosition' => 89, + ]; + yield [ + 'row' => 'B', + 'column' => 12, + 'rowFlowPosition' => 24, + 'columnFlowPosition' => 90, + ]; + yield [ + 'row' => 'C', + 'column' => 12, + 'rowFlowPosition' => 36, + 'columnFlowPosition' => 91, + ]; + yield [ + 'row' => 'D', + 'column' => 12, + 'rowFlowPosition' => 48, + 'columnFlowPosition' => 92, + ]; + yield [ + 'row' => 'E', + 'column' => 12, + 'rowFlowPosition' => 60, + 'columnFlowPosition' => 93, + ]; + yield [ + 'row' => 'F', + 'column' => 12, + 'rowFlowPosition' => 72, + 'columnFlowPosition' => 94, + ]; + yield [ + 'row' => 'G', + 'column' => 12, + 'rowFlowPosition' => 84, + 'columnFlowPosition' => 95, + ]; + yield [ + 'row' => 'H', + 'column' => 12, + 'rowFlowPosition' => 96, + 'columnFlowPosition' => 96, + ]; + } + + /** @dataProvider dataProvider12Well */ + public function testPosition12Well(string $row, int $column, int $rowFlowPosition, int $columnFlowPosition): void + { + $coordinates = new Coordinates($row, $column, new CoordinateSystem12Well()); + self::assertSame($columnFlowPosition, $coordinates->position(FlowDirection::COLUMN())); + self::assertSame($rowFlowPosition, $coordinates->position(FlowDirection::ROW())); + } + + /** @return list */ + public static function dataProvider12Well(): array + { + return [ + [ + 'row' => 'A', + 'column' => 1, + 'rowFlowPosition' => 1, + 'columnFlowPosition' => 1, + ], + [ + 'row' => 'A', + 'column' => 2, + 'rowFlowPosition' => 2, + 'columnFlowPosition' => 4, + ], + [ + 'row' => 'A', + 'column' => 3, + 'rowFlowPosition' => 3, + 'columnFlowPosition' => 7, + ], + [ + 'row' => 'A', + 'column' => 4, + 'rowFlowPosition' => 4, + 'columnFlowPosition' => 10, + ], + [ + 'row' => 'B', + 'column' => 1, + 'rowFlowPosition' => 5, + 'columnFlowPosition' => 2, + ], + [ + 'row' => 'B', + 'column' => 2, + 'rowFlowPosition' => 6, + 'columnFlowPosition' => 5, + ], + [ + 'row' => 'B', + 'column' => 3, + 'rowFlowPosition' => 7, + 'columnFlowPosition' => 8, + ], + [ + 'row' => 'B', + 'column' => 4, + 'rowFlowPosition' => 8, + 'columnFlowPosition' => 11, + ], + [ + 'row' => 'C', + 'column' => 1, + 'rowFlowPosition' => 9, + 'columnFlowPosition' => 3, + ], + [ + 'row' => 'C', + 'column' => 2, + 'rowFlowPosition' => 10, + 'columnFlowPosition' => 6, + ], + [ + 'row' => 'C', + 'column' => 3, + 'rowFlowPosition' => 11, + 'columnFlowPosition' => 9, + ], + [ + 'row' => 'C', + 'column' => 4, + 'rowFlowPosition' => 12, + 'columnFlowPosition' => 12, + ], + ]; + } + + /** @dataProvider invalidRowsOrColumns */ + public function testThrowsOnInvalidRowsOrColumns(string $row, int $column): void + { + $this->expectException(\InvalidArgumentException::class); + new Coordinates($row, $column, new CoordinateSystem96Well()); + } + + /** @return iterable */ + public static function invalidRowsOrColumns(): iterable + { + yield ['X', 2]; + yield ['B', 0]; + yield ['B', 13]; + yield ['B', -1]; + yield ['B', 1000]; + yield ['rolf', 2]; + } + + /** @dataProvider invalidPositions */ + public function testThrowsOnInvalidPositions(int $position): void + { + $this->expectException(\InvalidArgumentException::class); + Coordinates::fromPosition($position, FlowDirection::COLUMN(), new CoordinateSystem96Well()); + } + + /** @return iterable */ + public static function invalidPositions(): iterable + { + yield [0]; + yield [-1]; + yield [97]; + yield [10000]; + } + + /** @dataProvider invalidCoordinates */ + public function testThrowsOnInvalidCoordinates(string $coordinatesString): void + { + $this->expectException(\InvalidArgumentException::class); + Coordinates::fromString($coordinatesString, new CoordinateSystem96Well()); + } + + /** @return iterable */ + public static function invalidCoordinates(): iterable + { + yield ['A0']; + yield ['A001']; + yield ['X3']; + yield ['rolf']; + yield ['a1']; + } +} diff --git a/tests/Microplate/MicroplateSet/MicroplateSetABCDETest.php b/tests/Microplate/MicroplateSet/MicroplateSetABCDETest.php new file mode 100644 index 0000000..4c7bb0c --- /dev/null +++ b/tests/Microplate/MicroplateSet/MicroplateSetABCDETest.php @@ -0,0 +1,48 @@ +locationFromPosition($setPositionHigherThanMax, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor96WellPlatesOutOfRangeTooLow(): void + { + $microplateSet = new MicroplateSetABCDE(new CoordinateSystem96Well()); + + $setPositionLowerThanMin = 0; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-480, got: {$setPositionLowerThanMin}")); + $microplateSet->locationFromPosition($setPositionLowerThanMin, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor12WellPlatesOutOfRangeTooHigh(): void + { + $microplateSet = new MicroplateSetABCDE(new CoordinateSystem12Well()); + + $setPositionHigherThanMax = 61; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-60, got: {$setPositionHigherThanMax}")); + $microplateSet->locationFromPosition($setPositionHigherThanMax, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor12WellPlatesOutOfRangeTooLow(): void + { + $microplateSet = new MicroplateSetABCDE(new CoordinateSystem12Well()); + + $setPositionLowerThanMin = 0; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-60, got: {$setPositionLowerThanMin}")); + $microplateSet->locationFromPosition($setPositionLowerThanMin, FlowDirection::COLUMN()); + } +} diff --git a/tests/Microplate/MicroplateSet/MicroplateSetABCDTest.php b/tests/Microplate/MicroplateSet/MicroplateSetABCDTest.php new file mode 100644 index 0000000..81fc89b --- /dev/null +++ b/tests/Microplate/MicroplateSet/MicroplateSetABCDTest.php @@ -0,0 +1,153 @@ +locationFromPosition($setPositionHigherThanMax, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor96WellPlatesOutOfRangeTooLow(): void + { + $microplateSet = new MicroplateSetABCD(new CoordinateSystem96Well()); + + $setPositionLowerThanMin = 0; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-384, got: {$setPositionLowerThanMin}")); + $microplateSet->locationFromPosition($setPositionLowerThanMin, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor12WellPlatesOutOfRangeTooHigh(): void + { + $microplateSet = new MicroplateSetABCD(new CoordinateSystem12Well()); + + $setPositionHigherThanMax = 49; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-48, got: {$setPositionHigherThanMax}")); + $microplateSet->locationFromPosition($setPositionHigherThanMax, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor12WellPlatesOutOfRangeTooLow(): void + { + $microplateSet = new MicroplateSetABCD(new CoordinateSystem12Well()); + + $setPositionLowerThanMin = 0; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-48, got: {$setPositionLowerThanMin}")); + $microplateSet->locationFromPosition($setPositionLowerThanMin, FlowDirection::COLUMN()); + } + + /** @dataProvider dataProvider12Well */ + public function testSetLocationFromSetPositionFor12Wells(int $position, string $coordinatesString, string $plateID): void + { + $microplateSet = new MicroplateSetABCD(new CoordinateSystem12Well()); + + $location = $microplateSet->locationFromPosition($position, FlowDirection::COLUMN()); + self::assertSame($location->coordinates->toString(), $coordinatesString); + self::assertSame($location->plateID, $plateID); + } + + /** @return iterable */ + public static function dataProvider12Well(): iterable + { + yield [ + 'position' => 1, + 'coordinatesString' => 'A1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 2, + 'coordinatesString' => 'B1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 3, + 'coordinatesString' => 'C1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 12, + 'coordinatesString' => 'C4', + 'plateID' => 'A', + ]; + yield [ + 'position' => 13, + 'coordinatesString' => 'A1', + 'plateID' => 'B', + ]; + yield [ + 'position' => 48, + 'coordinatesString' => 'C4', + 'plateID' => 'D', + ]; + } + + /** @dataProvider dataProvider96Well */ + public function testSetLocationFromSetPositionFor96Wells(int $position, string $coordinatesString, string $plateID): void + { + $microplateSet = new MicroplateSetABCD(new CoordinateSystem96Well()); + + $location = $microplateSet->locationFromPosition($position, FlowDirection::COLUMN()); + self::assertSame($coordinatesString, $location->coordinates->toString()); + self::assertSame($plateID, $location->plateID); + } + + /** @return iterable */ + public static function dataProvider96Well(): iterable + { + yield [ + 'position' => 1, + 'coordinatesString' => 'A1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 2, + 'coordinatesString' => 'B1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 3, + 'coordinatesString' => 'C1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 12, + 'coordinatesString' => 'D2', + 'plateID' => 'A', + ]; + yield [ + 'position' => 13, + 'coordinatesString' => 'E2', + 'plateID' => 'A', + ]; + yield [ + 'position' => 96, + 'coordinatesString' => 'H12', + 'plateID' => 'A', + ]; + yield [ + 'position' => 97, + 'coordinatesString' => 'A1', + 'plateID' => 'B', + ]; + yield [ + 'position' => 384, + 'coordinatesString' => 'H12', + 'plateID' => 'D', + ]; + yield [ + 'position' => 383, + 'coordinatesString' => 'G12', + 'plateID' => 'D', + ]; + } +} diff --git a/tests/Microplate/MicroplateSet/MicroplateSetABTest.php b/tests/Microplate/MicroplateSet/MicroplateSetABTest.php new file mode 100644 index 0000000..6976d1a --- /dev/null +++ b/tests/Microplate/MicroplateSet/MicroplateSetABTest.php @@ -0,0 +1,159 @@ +locationFromPosition($setPositionHigherThanMax, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor96WellPlatesOutOfRangeTooLow(): void + { + $microplateSet = new MicroplateSetAB(new CoordinateSystem96Well()); + + $setPositionLowerThanMin = 0; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-192, got: {$setPositionLowerThanMin}")); + $microplateSet->locationFromPosition($setPositionLowerThanMin, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor12WellPlatesOutOfRangeTooHigh(): void + { + $microplateSet = new MicroplateSetAB(new CoordinateSystem12Well()); + + $setPositionHigherThanMax = 25; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-24, got: {$setPositionHigherThanMax}")); + $microplateSet->locationFromPosition($setPositionHigherThanMax, FlowDirection::COLUMN()); + } + + public function testSetLocationFromSetPositionFor12WellPlatesOutOfRangeTooLow(): void + { + $microplateSet = new MicroplateSetAB(new CoordinateSystem12Well()); + + $setPositionLowerThanMin = 0; + self::expectExceptionObject(new \OutOfRangeException("Expected a position between 1-24, got: {$setPositionLowerThanMin}")); + $microplateSet->locationFromPosition($setPositionLowerThanMin, FlowDirection::COLUMN()); + } + + /** @dataProvider dataProvider12Well */ + public function testSetLocationFromSetPositionFor12Wells(int $position, string $coordinatesString, string $plateID): void + { + $microplateSet = new MicroplateSetAB(new CoordinateSystem12Well()); + + $location = $microplateSet->locationFromPosition($position, FlowDirection::COLUMN()); + self::assertSame($location->coordinates->toString(), $coordinatesString); + self::assertSame($location->plateID, $plateID); + } + + /** @return iterable */ + public static function dataProvider12Well(): iterable + { + yield [ + 'position' => 1, + 'coordinatesString' => 'A1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 2, + 'coordinatesString' => 'B1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 3, + 'coordinatesString' => 'C1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 12, + 'coordinatesString' => 'C4', + 'plateID' => 'A', + ]; + yield [ + 'position' => 13, + 'coordinatesString' => 'A1', + 'plateID' => 'B', + ]; + yield [ + 'position' => 24, + 'coordinatesString' => 'C4', + 'plateID' => 'B', + ]; + } + + /** @dataProvider dataProvider96Well */ + public function testSetLocationFromSetPositionFor96Wells(int $position, string $coordinatesString, string $plateID): void + { + $microplateSet = new MicroplateSetAB(new CoordinateSystem96Well()); + + $location = $microplateSet->locationFromPosition($position, FlowDirection::COLUMN()); + self::assertSame($coordinatesString, $location->coordinates->toString()); + self::assertSame($plateID, $location->plateID); + } + + /** + * @return iterable + */ + public static function dataProvider96Well(): iterable + { + yield [ + 'position' => 1, + 'coordinatesString' => 'A1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 2, + 'coordinatesString' => 'B1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 3, + 'coordinatesString' => 'C1', + 'plateID' => 'A', + ]; + yield [ + 'position' => 12, + 'coordinatesString' => 'D2', + 'plateID' => 'A', + ]; + yield [ + 'position' => 13, + 'coordinatesString' => 'E2', + 'plateID' => 'A', + ]; + yield [ + 'position' => 96, + 'coordinatesString' => 'H12', + 'plateID' => 'A', + ]; + yield [ + 'position' => 97, + 'coordinatesString' => 'A1', + 'plateID' => 'B', + ]; + yield [ + 'position' => 192, + 'coordinatesString' => 'H12', + 'plateID' => 'B', + ]; + yield [ + 'position' => 191, + 'coordinatesString' => 'G12', + 'plateID' => 'B', + ]; + } +} diff --git a/tests/Microplate/MicroplateSetTest.php b/tests/Microplate/MicroplateSetTest.php new file mode 100644 index 0000000..746b8ff --- /dev/null +++ b/tests/Microplate/MicroplateSetTest.php @@ -0,0 +1,21 @@ +plateCount()); + self::assertSame(MicroplateSetABCD::PLATE_COUNT, (new MicroplateSetABCD($anyCoordinateSystemWillDo))->plateCount()); + self::assertSame(MicroplateSetABCDE::PLATE_COUNT, (new MicroplateSetABCDE($anyCoordinateSystemWillDo))->plateCount()); + } +} diff --git a/tests/Microplate/MicroplateTest.php b/tests/Microplate/MicroplateTest.php new file mode 100644 index 0000000..ef4f90f --- /dev/null +++ b/tests/Microplate/MicroplateTest.php @@ -0,0 +1,273 @@ +addWell($microplateCoordinate1, $wellContent1); + + $wellContent2 = 'bar'; + $microplate->addWell($microplateCoordinate2, $wellContent2); + + self::assertEquals($wellContent1, $microplate->well($microplateCoordinate1)); + self::assertEquals($wellContent2, $microplate->well($microplateCoordinate2)); + + $coordinateWithOtherCoordinateSystem = new Coordinates('B', 2, new CoordinateSystem12Well()); + // @phpstan-ignore-next-line expecting a type error due to mismatching coordinates + $microplate->addWell($coordinateWithOtherCoordinateSystem, 'foo'); + } + + public function testMatchRow(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + + $microplate = new Microplate($coordinateSystem); + + $content = 'foo'; + $coordinates = new Coordinates('A', 1, $coordinateSystem); + $key = $coordinates->toString(); + $microplate->addWell($coordinates, $content); + + $microplate->addWell(new Coordinates('B', 1, $coordinateSystem), 'bar'); + + $wells = $microplate->wells(); + + $a = $wells->filter($microplate->matchRow('A')); + self::assertCount($coordinateSystem->columnsCount(), $a); + self::assertSame($content, $a[$key]); + + $notA = $wells->reject($microplate->matchRow('A')); + self::assertCount($coordinateSystem->positionsCount() - $coordinateSystem->columnsCount(), $notA); + + $noMatch = $microplate->filledWells() + ->filter($microplate->matchRow('C')); + self::assertCount(0, $noMatch); + } + + public function testMatchColumn(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + + $microplate = new Microplate($coordinateSystem); + + $coordinateColumn1 = (new Coordinates('A', 1, $coordinateSystem))->toString(); + $coordinateColumn2 = (new Coordinates('A', 2, $coordinateSystem))->toString(); + + $matchColumn = $microplate->matchColumn(1); + self::assertTrue($matchColumn('foo', $coordinateColumn1)); + self::assertFalse($matchColumn('foo', $coordinateColumn2)); + } + + public function testtoWellWithCoordinatesMapper(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $microplate = new Microplate($coordinateSystem); + + $coordinates = new Coordinates('A', 1, $coordinateSystem); + $content = 'foo'; + $microplate->addWell($coordinates, $content); + + $wellWithCoordinates = $microplate->wells() + ->map($microplate->toWellWithCoordinatesMapper()) + ->first(); + + self::assertInstanceOf(WellWithCoordinates::class, $wellWithCoordinates); + self::assertSame($content, $wellWithCoordinates->content); + self::assertEquals($coordinates, $wellWithCoordinates->coordinates); + } + + public function testSortedWells(): void + { + $microplate = $this->preparePlate(); + + /** @var Collection $keysSortedByRow PHPStan is wrong about what keys() does */ + $keysSortedByRow = $microplate->sortedWells(FlowDirection::ROW())->keys(); + self::assertSame('A1', $keysSortedByRow[0]); + self::assertSame('A2', $keysSortedByRow[1]); + self::assertSame('A3', $keysSortedByRow[2]); + self::assertSame('A4', $keysSortedByRow[3]); + self::assertSame('H11', $keysSortedByRow[94]); + self::assertSame('H12', $keysSortedByRow[95]); + + /** @var Collection $keysSortedByColumn PHPStan is wrong about what keys() does */ + $keysSortedByColumn = $microplate->sortedWells(FlowDirection::COLUMN())->keys(); + self::assertSame('A1', $keysSortedByColumn[0]); + self::assertSame('B1', $keysSortedByColumn[1]); + self::assertSame('C1', $keysSortedByColumn[2]); + self::assertSame('D1', $keysSortedByColumn[3]); + self::assertSame('G12', $keysSortedByColumn[94]); + self::assertSame('H12', $keysSortedByColumn[95]); + } + + public function testFreeWells(): void + { + $microplate = $this->preparePlate(); + + self::assertGreaterThan( + 0, + $microplate->wells() + // @phpstan-ignore-next-line generic false-positive + ->filter(static fn (?string $value): bool => $value !== null) + ->count() + ); + self::assertNotCount(0, $microplate->freeWells()); + } + + /** @phpstan-return Microplate */ + private function preparePlate(): Microplate + { + $coordinateSystem = new CoordinateSystem96Well(); + + $microplate = new Microplate($coordinateSystem); + + $data96Wells = iterator_to_array(CoordinatesTest::dataProvider96Well()); + \Safe\shuffle($data96Wells); + foreach ($data96Wells as $well) { + $microplateCoordinates = new Coordinates($well['row'], $well['column'], new CoordinateSystem96Well()); + + $randomNumber = rand(1, 100); + $randomNumberOrNull = $randomNumber > 50 ? $randomNumber : null; + + $microplate->addWell($microplateCoordinates, $randomNumberOrNull); + } + + return $microplate; + } + + public function testNextFreeWellAddingAndGetting(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $microplate = new Microplate($coordinateSystem); + + $wellData = [ + 'A1' => 'foo', + 'B1' => 'bar', + 'A2' => 'foobar', + 'A3' => 'barfoo', + ]; + + $coordinatesString1 = array_keys($wellData)[0]; + $microplateCoordinate1 = Coordinates::fromString($coordinatesString1, $coordinateSystem); + self::assertEquals($microplateCoordinate1, $microplate->nextFreeWellCoordinates(FlowDirection::COLUMN())); + $microplate->addToNextFreeWell($wellData[$coordinatesString1], FlowDirection::COLUMN()); + + $coordinatesString2 = array_keys($wellData)[1]; + $microplateCoordinate2 = Coordinates::fromString($coordinatesString2, $coordinateSystem); + self::assertEquals($microplateCoordinate2, $microplate->nextFreeWellCoordinates(FlowDirection::COLUMN())); + $microplate->addToNextFreeWell($wellData[$coordinatesString2], FlowDirection::COLUMN()); + + $microplateCoordinate3 = Coordinates::fromString('C1', $coordinateSystem); + self::assertEquals($microplateCoordinate3, $microplate->nextFreeWellCoordinates(FlowDirection::COLUMN())); + + $coordinatesString4 = array_keys($wellData)[2]; + $microplateCoordinate4 = Coordinates::fromString($coordinatesString4, $coordinateSystem); + self::assertEquals($microplateCoordinate4, $microplate->addToNextFreeWell($wellData[$coordinatesString4], FlowDirection::ROW())); + + $coordinatesString5 = array_keys($wellData)[3]; + $microplateCoordinate5 = Coordinates::fromString($coordinatesString5, $coordinateSystem); + self::assertEquals($microplateCoordinate5, $microplate->addToNextFreeWell($wellData[$coordinatesString5], FlowDirection::ROW())); + + self::assertSame($wellData, $microplate->filledWells()->toArray()); + } + + public function testThrowsPlateFullException(): void + { + $coordinateSystem = new CoordinateSystem12Well(); + $microplate = new Microplate($coordinateSystem); + + $dataProvider12Well = CoordinatesTest::dataProvider12Well(); + foreach ($dataProvider12Well as $wellData) { + $microplateCoordinates = new Coordinates($wellData['row'], $wellData['column'], $coordinateSystem); + // check that it does not throw before the plate is full + self::assertEquals($microplateCoordinates, $microplate->nextFreeWellCoordinates(FlowDirection::ROW())); + $microplate->addWell($microplateCoordinates, rand(1, 100)); + } + + $this->expectException(MicroplateIsFullException::class); + $microplate->nextFreeWellCoordinates(FlowDirection::ROW()); + } + + public function testIsConsecutiveForColumn(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $microplate = new Microplate($coordinateSystem); + + $data = [ + 'A1', 'B1', 'C1', + ]; + foreach ($data as $wellData) { + $microplateCoordinates = Coordinates::fromString($wellData, $coordinateSystem); + $microplate->addWell($microplateCoordinates, 'test'); + } + + self::assertTrue($microplate->isConsecutive(FlowDirection::COLUMN())); + self::assertFalse($microplate->isConsecutive(FlowDirection::ROW())); + + // is not consecutive anymore after adding a gap at E1 + $microplate->addWell(Coordinates::fromString('E1', $coordinateSystem), 'test'); + self::assertFalse($microplate->isConsecutive(FlowDirection::COLUMN())); + } + + public function testIsConsecutiveForRow(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $microplate = new Microplate($coordinateSystem); + + $data = [ + 'A1', 'A2', 'A3', + ]; + foreach ($data as $wellData) { + $microplateCoordinates = Coordinates::fromString($wellData, $coordinateSystem); + $microplate->addWell($microplateCoordinates, 'test'); + } + + self::assertTrue($microplate->isConsecutive(FlowDirection::ROW())); + self::assertFalse($microplate->isConsecutive(FlowDirection::COLUMN())); + + // is not consecutive anymore after adding a gap at A5 + $microplate->addWell(Coordinates::fromString('A5', $coordinateSystem), 'test'); + self::assertFalse($microplate->isConsecutive(FlowDirection::ROW())); + } + + public function testIsConsecutiveForEmptyPlate(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $microplate = new Microplate($coordinateSystem); + + self::assertFalse($microplate->isConsecutive(FlowDirection::ROW())); + self::assertFalse($microplate->isConsecutive(FlowDirection::COLUMN())); + } + + public function testIsConsecutiveForFullPlate(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $microplate = new Microplate($coordinateSystem); + + foreach ($coordinateSystem->all() as $coordinates) { + $microplate->addWell($coordinates, 'test'); + } + + self::assertTrue($microplate->isConsecutive(FlowDirection::ROW())); + self::assertTrue($microplate->isConsecutive(FlowDirection::COLUMN())); + } +} diff --git a/tests/Microplate/Scalars/Column96WellTest.php b/tests/Microplate/Scalars/Column96WellTest.php new file mode 100644 index 0000000..bd37d1a --- /dev/null +++ b/tests/Microplate/Scalars/Column96WellTest.php @@ -0,0 +1,59 @@ +expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Value not in range: "12".'); + + (new Column96Well())->serialize('12'); + } + + public function testSerializeThrowsIfColumn96WellIsInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Value not in range: 13.'); + + (new Column96Well())->serialize(13); + } + + public function testSerializePassesWhenColumn96WellIsValid(): void + { + $serializedResult = (new Column96Well())->serialize(12); + + self::assertSame(12, $serializedResult); + } + + public function testParseValueThrowsIfColumn96WellIsInvalid(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('Value not in range: 13.'); + + (new Column96Well())->parseValue(13); + } + + public function testParseValuePassesIfColumn96WellIsValid(): void + { + self::assertSame( + 12, + (new Column96Well())->parseValue(12) + ); + } +} diff --git a/tests/Microplate/Scalars/Row96WellTest.php b/tests/Microplate/Scalars/Row96WellTest.php new file mode 100644 index 0000000..610a9cd --- /dev/null +++ b/tests/Microplate/Scalars/Row96WellTest.php @@ -0,0 +1,68 @@ +expectException(InvariantViolation::class); + $this->expectExceptionMessage('The given value "I" did not match the regex /^[A-H]$/.'); + + (new Row96Well())->serialize('I'); + } + + public function testSerializeThrowsIfRow96WellIsNonCapital(): void + { + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('The given value "h" did not match the regex /^[A-H]$/.'); + + (new Row96Well())->serialize('h'); + } + + public function testSerializePassesWhenRow96WellIsValid(): void + { + $serializedResult = (new Row96Well())->serialize('H'); + + self::assertSame('H', $serializedResult); + } + + public function testParseValueThrowsIfRow96WellIsInvalid(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('The given value "I" did not match the regex /^[A-H]$/.'); + + (new Row96Well())->parseValue('I'); + } + + public function testParseValueThrowsIfRow96WellIsNonCapital(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('The given value "h" did not match the regex /^[A-H]$/.'); + + (new Row96Well())->parseValue('h'); + } + + public function testParseValuePassesIfRow96WellIsValid(): void + { + self::assertSame( + 'H', + (new Row96Well())->parseValue('H') + ); + } +} diff --git a/tests/Microplate/SectionedMicroplate/FullColumnSectionTest.php b/tests/Microplate/SectionedMicroplate/FullColumnSectionTest.php new file mode 100644 index 0000000..3bf95a6 --- /dev/null +++ b/tests/Microplate/SectionedMicroplate/FullColumnSectionTest.php @@ -0,0 +1,140 @@ +sections); + + $section = $sectionedMicroplate->addSection(FullColumnSection::class); + self::assertCount(1, $sectionedMicroplate->sections); + self::assertCount(96, $sectionedMicroplate->freeWells()); + + foreach ($coordinateSystem->all() as $i => $coordinate) { + $section->addWell('column' . $i); + self::assertCount($i + 1, $sectionedMicroplate->filledWells()); + } + + self::assertCount(0, $sectionedMicroplate->freeWells()); + $this->expectExceptionObject(new MicroplateIsFullException()); + + $section->addWell('foo'); + } + + public function testCanNotAddFullColumnSectionIfAllColumnsAreReserved(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $sectionedMicroplate = new SectionedMicroplate($coordinateSystem); + + foreach (range(1, $coordinateSystem->columnsCount()) as $i) { + $sectionedMicroplate->addSection(FullColumnSection::class); + } + + $this->expectExceptionObject(new SectionIsFullException()); + $sectionedMicroplate->addSection(FullColumnSection::class); + } + + public function testCanNotGrowFullColumnSectionIfNoColumnsAreLeft(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $sectionedMicroplate = new SectionedMicroplate($coordinateSystem); + + foreach (range(1, $coordinateSystem->columnsCount() - 1) as $i) { + $sectionedMicroplate->addSection(FullColumnSection::class); + } + + $lastSection = $sectionedMicroplate->addSection(FullColumnSection::class); + foreach (range(1, $coordinateSystem->rowsCount()) as $i) { + $lastSection->addWell('foo'); + } + + $this->expectExceptionObject(new SectionIsFullException()); + $lastSection->addWell('bar'); + } + + public function testFullColumnSection(): void + { + $coordinateSystem = new CoordinateSystem96Well(); + $sectionedMicroplate = new SectionedMicroplate($coordinateSystem); + self::assertCount(0, $sectionedMicroplate->sections); + + $section1 = $sectionedMicroplate->addSection(FullColumnSection::class); + self::assertCount(1, $sectionedMicroplate->sections); + self::assertCount(96, $sectionedMicroplate->freeWells()); + + foreach (range(1, 4) as $ignored1) { + $section1->addWell('section1'); + } + + $section2 = $sectionedMicroplate->addSection(FullColumnSection::class); + $emptyCoordinateInSection1 = new Coordinates('E', 1, $coordinateSystem); + self::assertNull($sectionedMicroplate->well($emptyCoordinateInSection1)); + + foreach (range(1, 5) as $ignored1) { + $section2->addWell('section2'); + } + self::assertNull($sectionedMicroplate->well($emptyCoordinateInSection1)); + + self::assertSame([ + 'A1' => 'section1', + 'B1' => 'section1', + 'C1' => 'section1', + 'D1' => 'section1', + 'A2' => 'section2', + 'B2' => 'section2', + 'C2' => 'section2', + 'D2' => 'section2', + 'E2' => 'section2', + ], $sectionedMicroplate->filledWells()->toArray()); + + foreach (range(1, 16) as $ignored1) { + $section1->addWell('section1'); + } + + self::assertSame([ + 'A1' => 'section1', + 'B1' => 'section1', + 'C1' => 'section1', + 'D1' => 'section1', + 'E1' => 'section1', + 'F1' => 'section1', + 'G1' => 'section1', + 'H1' => 'section1', + 'A2' => 'section1', + 'B2' => 'section1', + 'C2' => 'section1', + 'D2' => 'section1', + 'E2' => 'section1', + 'F2' => 'section1', + 'G2' => 'section1', + 'H2' => 'section1', + 'A3' => 'section1', + 'B3' => 'section1', + 'C3' => 'section1', + 'D3' => 'section1', + 'A4' => 'section2', + 'B4' => 'section2', + 'C4' => 'section2', + 'D4' => 'section2', + 'E4' => 'section2', + ], $sectionedMicroplate->filledWells()->toArray()); + + $this->expectExceptionObject(new SectionIsFullException()); + + foreach (range(1, 100) as $ignored1) { + $section1->addWell('section1'); + } + } +} diff --git a/tests/Microplate/SectionedMicroplate/SectionTest.php b/tests/Microplate/SectionedMicroplate/SectionTest.php new file mode 100644 index 0000000..50d998f --- /dev/null +++ b/tests/Microplate/SectionedMicroplate/SectionTest.php @@ -0,0 +1,33 @@ +sections); + + $section = $sectionedMicroplate->addSection(Section::class); + self::assertCount(1, $sectionedMicroplate->sections); + self::assertCount(96, $sectionedMicroplate->freeWells()); + + foreach ($coordinateSystem->all() as $i => $coordinate) { + $section->addWell('column' . $i); + self::assertCount($i + 1, $sectionedMicroplate->filledWells()); + } + + self::assertCount(0, $sectionedMicroplate->freeWells()); + $this->expectExceptionObject(new MicroplateIsFullException()); + + $section->addWell('foo'); + } +} diff --git a/tests/Microplate/SectionedMicroplate/SectionedMicroplateTest.php b/tests/Microplate/SectionedMicroplate/SectionedMicroplateTest.php new file mode 100644 index 0000000..a1122cc --- /dev/null +++ b/tests/Microplate/SectionedMicroplate/SectionedMicroplateTest.php @@ -0,0 +1,48 @@ +sections); + + $section1 = $sectionedMicroplate->addSection(Section::class); + self::assertCount(1, $sectionedMicroplate->sections); + + $section2 = $sectionedMicroplate->addSection(Section::class); + self::assertCount(2, $sectionedMicroplate->sections); + + self::assertCount(0, $sectionedMicroplate->filledWells()); + self::assertCount(96, $sectionedMicroplate->freeWells()); + + $content1 = 'content1'; + $section1->addWell($content1); + $content2 = 'content2'; + $content3 = 'content3'; + $section2->addWell($content2); + $section2->addWell($content3); + + self::assertCount(3, $sectionedMicroplate->filledWells()); + self::assertCount(93, $sectionedMicroplate->freeWells()); + + self::assertSame($content1, $section1->sectionItems->first()); + + self::assertSame($content2, $section2->sectionItems->first()); + self::assertSame($content3, $section2->sectionItems->last()); + + $sectionedMicroplate->removeSection($section1); + self::assertCount(1, $sectionedMicroplate->sections); + + self::assertCount(2, $sectionedMicroplate->filledWells()); + self::assertCount(94, $sectionedMicroplate->freeWells()); + } +} diff --git a/tests/QxManager/FilledWellTest.php b/tests/QxManager/FilledWellTest.php index 05bcefa..4fabfaf 100644 --- a/tests/QxManager/FilledWellTest.php +++ b/tests/QxManager/FilledWellTest.php @@ -2,8 +2,8 @@ namespace MLL\Utils\Tests\Unit\QxManager; -use Mll\Microplate\Coordinates; -use Mll\Microplate\CoordinateSystem96Well; +use MLL\Utils\Microplate\Coordinates; +use MLL\Utils\Microplate\CoordinateSystem96Well; use MLL\Utils\QxManager\FilledRow; use MLL\Utils\QxManager\FilledWell; use PHPUnit\Framework\TestCase; diff --git a/tests/QxManager/QxManagerSampleSheetTest.php b/tests/QxManager/QxManagerSampleSheetTest.php index 635105c..b354b8b 100644 --- a/tests/QxManager/QxManagerSampleSheetTest.php +++ b/tests/QxManager/QxManagerSampleSheetTest.php @@ -3,9 +3,9 @@ namespace MLL\Utils\Tests\Unit\QxManager; use Carbon\Carbon; -use Mll\Microplate\Coordinates; -use Mll\Microplate\CoordinateSystem96Well; -use Mll\Microplate\Microplate; +use MLL\Utils\Microplate\Coordinates; +use MLL\Utils\Microplate\CoordinateSystem96Well; +use MLL\Utils\Microplate\Microplate; use MLL\Utils\QxManager\FilledRow; use MLL\Utils\QxManager\FilledWell; use MLL\Utils\QxManager\QxManagerSampleSheet; From 82373bd1666850b69b48e3fbc49c61c9c1c6c32d Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 10:04:31 +0200 Subject: [PATCH 02/16] clean up phpdocs --- src/CSVArray.php | 4 +--- src/Microplate/AbstractMicroplate.php | 12 +++--------- src/Microplate/AbstractSection.php | 4 +--- src/Microplate/Casts/Coordinates96Well.php | 4 +--- src/Microplate/Coordinates.php | 4 +--- src/Microplate/FullColumnSection.php | 5 +++-- src/Microplate/Microplate.php | 10 ++++------ src/Microplate/MicroplateSet/Location.php | 4 +--- src/Microplate/MicroplateSet/MicroplateSet.php | 4 +--- tests/CSVArrayTest.php | 4 +--- 10 files changed, 17 insertions(+), 38 deletions(-) diff --git a/src/CSVArray.php b/src/CSVArray.php index fe7fc7f..d01410c 100644 --- a/src/CSVArray.php +++ b/src/CSVArray.php @@ -4,9 +4,7 @@ use Illuminate\Support\Arr; -/** - * @phpstan-type CSVPrimitive bool|float|int|string|\Stringable|null - */ +/** @phpstan-type CSVPrimitive bool|float|int|string|\Stringable|null */ final class CSVArray { /** diff --git a/src/Microplate/AbstractMicroplate.php b/src/Microplate/AbstractMicroplate.php index 40415bf..eeff865 100644 --- a/src/Microplate/AbstractMicroplate.php +++ b/src/Microplate/AbstractMicroplate.php @@ -48,9 +48,7 @@ public function isWellEmpty(Coordinates $coordinate): bool public function sortedWells(FlowDirection $flowDirection): Collection { return $this->wells()->sortBy( - /** - * @param TWell $content - */ + /** @param TWell $content */ function ($content, string $key) use ($flowDirection): string { switch ($flowDirection->value) { case FlowDirection::ROW: @@ -73,9 +71,7 @@ function ($content, string $key) use ($flowDirection): string { public function freeWells(): Collection { return $this->wells()->filter( - /** - * @param TWell $content - */ + /** @param TWell $content */ static fn ($content): bool => $content === self::EMPTY_WELL ); } @@ -84,9 +80,7 @@ public function freeWells(): Collection public function filledWells(): Collection { return $this->wells()->filter( - /** - * @param TWell $content - */ + /** @param TWell $content */ static fn ($content): bool => $content !== self::EMPTY_WELL ); } diff --git a/src/Microplate/AbstractSection.php b/src/Microplate/AbstractSection.php index f216f25..8b66b8f 100644 --- a/src/Microplate/AbstractSection.php +++ b/src/Microplate/AbstractSection.php @@ -4,9 +4,7 @@ use Illuminate\Support\Collection; -/** - * @template TSectionWell - */ +/** @template TSectionWell */ abstract class AbstractSection { /** @var SectionedMicroplate */ diff --git a/src/Microplate/Casts/Coordinates96Well.php b/src/Microplate/Casts/Coordinates96Well.php index 510ded1..794de72 100644 --- a/src/Microplate/Casts/Coordinates96Well.php +++ b/src/Microplate/Casts/Coordinates96Well.php @@ -7,9 +7,7 @@ use MLL\Utils\Microplate\Coordinates; use MLL\Utils\Microplate\CoordinateSystem96Well; -/** - * @implements CastsAttributes, Coordinates> - */ +/** @implements CastsAttributes, Coordinates> */ final class Coordinates96Well implements CastsAttributes { /** diff --git a/src/Microplate/Coordinates.php b/src/Microplate/Coordinates.php index d49e244..e877b84 100644 --- a/src/Microplate/Coordinates.php +++ b/src/Microplate/Coordinates.php @@ -8,9 +8,7 @@ use function Safe\preg_match; -/** - * @template TCoordinateSystem of CoordinateSystem - */ +/** @template TCoordinateSystem of CoordinateSystem */ final class Coordinates { public const MIN_POSITION = 1; diff --git a/src/Microplate/FullColumnSection.php b/src/Microplate/FullColumnSection.php index d2bd7d9..582b417 100644 --- a/src/Microplate/FullColumnSection.php +++ b/src/Microplate/FullColumnSection.php @@ -6,8 +6,9 @@ use MLL\Utils\Microplate\Exceptions\SectionIsFullException; /** - * A Section that occupies all wells of a column if one sample exists in this column. Samples of other sections are - * not allowed in this occupied wells. Occupied wells can still be filled with samples of the same type. + * A section that occupies all wells of a column if one sample exists in this column. + * Samples of other sections are not allowed in this occupied wells. + * Occupied wells can still be filled with samples of the same type. * * @template TSectionWell * diff --git a/src/Microplate/Microplate.php b/src/Microplate/Microplate.php index 41a5ea1..1c65b25 100644 --- a/src/Microplate/Microplate.php +++ b/src/Microplate/Microplate.php @@ -81,10 +81,8 @@ private function assertIsWellEmpty(Coordinates $coordinates, $content): void /** Clearing the wells will reinitialize all well position of the coordinate system. */ public function clearWells(): void { - /** - * Flow direction is irrelevant during initialization, it is not a property of - * a plate but rather a property of the access to the plate. - */ + // Flow direction is irrelevant during initialization, it is not a property of + // plate but rather a property of the access to the plate. /** @var array $wells */ $wells = []; @@ -98,7 +96,7 @@ public function clearWells(): void /** * @param TWell $content * - *@throws MicroplateIsFullException + * @throws MicroplateIsFullException * * @return Coordinates */ @@ -111,7 +109,7 @@ public function addToNextFreeWell($content, FlowDirection $flowDirection): Coord } /** - *@throws MicroplateIsFullException + * @throws MicroplateIsFullException * * @return Coordinates */ diff --git a/src/Microplate/MicroplateSet/Location.php b/src/Microplate/MicroplateSet/Location.php index efe4d09..2ae44e4 100644 --- a/src/Microplate/MicroplateSet/Location.php +++ b/src/Microplate/MicroplateSet/Location.php @@ -5,9 +5,7 @@ use MLL\Utils\Microplate\Coordinates; use MLL\Utils\Microplate\CoordinateSystem; -/** - * @template TCoordinateSystem of CoordinateSystem - */ +/** @template TCoordinateSystem of CoordinateSystem */ final class Location { public string $plateID; diff --git a/src/Microplate/MicroplateSet/MicroplateSet.php b/src/Microplate/MicroplateSet/MicroplateSet.php index da9f7b3..61d1d6b 100644 --- a/src/Microplate/MicroplateSet/MicroplateSet.php +++ b/src/Microplate/MicroplateSet/MicroplateSet.php @@ -6,9 +6,7 @@ use MLL\Utils\Microplate\CoordinateSystem; use MLL\Utils\Microplate\Enums\FlowDirection; -/** - * @template TCoordinateSystem of CoordinateSystem - */ +/** @template TCoordinateSystem of CoordinateSystem */ abstract class MicroplateSet { /** @var TCoordinateSystem */ diff --git a/tests/CSVArrayTest.php b/tests/CSVArrayTest.php index 87500f9..6ce0500 100644 --- a/tests/CSVArrayTest.php +++ b/tests/CSVArrayTest.php @@ -5,9 +5,7 @@ use MLL\Utils\CSVArray; use PHPUnit\Framework\TestCase; -/** - * @phpstan-import-type CSVPrimitive from CSVArray - */ +/** @phpstan-import-type CSVPrimitive from CSVArray */ final class CSVArrayTest extends TestCase { /** @return iterable>}> */ From 5ad92f64c8af4c39b22d1effe541a50614ed851f Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 10:08:24 +0200 Subject: [PATCH 03/16] fix install --- .github/workflows/validate.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0095c47..877ccf2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -74,6 +74,9 @@ jobs: extensions: mbstring php-version: "${{ matrix.php-version }}" + - if: "! startsWith(matrix.php-version, 8)" + run: composer remove --dev --no-update mll-lab/graphql-php-scalars + - run: composer require "illuminate/support:${{ matrix.illuminate }}" --no-interaction --no-update - if: matrix.dependencies == 'lowest' From dbff90e451a0499e68f5150f3195934a6bc61a59 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 10:09:22 +0200 Subject: [PATCH 04/16] do not fail fast --- .github/workflows/validate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 877ccf2..fa81f7e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -43,6 +43,7 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: php-version: - "7.4" From e2213ebe1e7ba7a6e741636495cc45849052df67 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 10:11:31 +0200 Subject: [PATCH 05/16] new version --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f895e..50c8e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases). ## Unreleased +## v1.13.0 + ### Added - Integrate `mll-lab/microplate` From 116d1feda66cea9e94f54b156d879d142e4f3e67 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 15:50:37 +0200 Subject: [PATCH 06/16] Include mll-lab/liquid-handling-robotics --- CHANGELOG.md | 2 +- src/FluidXPlate/FluidXPlate.php | 68 ++++ src/FluidXPlate/FluidXPlateException.php | 5 + src/FluidXPlate/FluidXScanner.php | 103 ++++++ src/FluidXPlate/InvalidRackIdException.php | 11 + .../InvalidTubeBarcodeException.php | 11 + src/FluidXPlate/Scalars/FluidXBarcode.php | 16 + src/FluidXPlate/Scalars/FrameStarBarcode.php | 17 + src/FluidXPlate/ScanFluidXPlateException.php | 5 + src/FluidXPlate/TestPlate.txt | 96 ++++++ src/Meta.php | 8 + src/Tecan/BasicCommands/Aspirate.php | 26 ++ .../AspirateAndDispenseParameters.php | 34 ++ .../BasicPipettingActionCommand.php | 44 +++ src/Tecan/BasicCommands/BreakCommand.php | 11 + src/Tecan/BasicCommands/Command.php | 9 + src/Tecan/BasicCommands/Comment.php | 18 ++ src/Tecan/BasicCommands/Dispense.php | 26 ++ .../BasicCommands/ReagentDistribution.php | 79 +++++ src/Tecan/BasicCommands/UsesTipMask.php | 8 + src/Tecan/BasicCommands/Wash.php | 11 + .../CustomCommands/AspirateParameters.php | 31 ++ .../CustomCommands/DispenseParameters.php | 31 ++ .../CustomCommands/MllReagentDistribution.php | 61 ++++ .../CustomCommands/TransferWithAutoWash.php | 40 +++ src/Tecan/LiquidClass/CustomLiquidClass.php | 18 ++ src/Tecan/LiquidClass/LiquidClass.php | 8 + src/Tecan/LiquidClass/MllLiquidClass.php | 49 +++ src/Tecan/Location/BarcodeLocation.php | 57 ++++ src/Tecan/Location/Location.php | 19 ++ src/Tecan/Location/PositionLocation.php | 57 ++++ src/Tecan/Rack/CustomRack.php | 46 +++ src/Tecan/Rack/MllLabWareRack.php | 116 +++++++ src/Tecan/Rack/Rack.php | 18 ++ .../ReagentDistributionDirection.php | 26 ++ src/Tecan/TecanException.php | 5 + src/Tecan/TecanProtocol.php | 99 ++++++ src/Tecan/TipMask/TipMask.php | 57 ++++ src/TecanScanner/NoRackIdException.php | 11 + src/TecanScanner/TecanScanEmptyException.php | 11 + src/TecanScanner/TecanScanException.php | 5 + src/TecanScanner/TecanScanner.php | 80 +++++ src/TecanScanner/WrongNumberOfWells.php | 11 + tests/FluidXPlate/FluidXPlateTest.php | 69 ++++ tests/FluidXPlate/FluidXScannerTest.php | 22 ++ .../FluidXPlate/Scalars/FluidXBarcodeTest.php | 43 +++ .../Scalars/FrameStarBarcodeTest.php | 44 +++ tests/QxManager/FilledRowTest.php | 2 +- tests/QxManager/FilledWellTest.php | 2 +- tests/QxManager/QxManagerSampleSheetTest.php | 2 +- tests/Tecan/BasicCommands/AspirateTest.php | 34 ++ tests/Tecan/BasicCommands/BreakTest.php | 14 + tests/Tecan/BasicCommands/CommentTest.php | 17 + tests/Tecan/BasicCommands/DispenseTest.php | 34 ++ .../BasicCommands/ReagentDistributionTest.php | 66 ++++ tests/Tecan/BasicCommands/WashTest.php | 14 + .../MllReagentDistributionTest.php | 41 +++ .../TransferWithAutoWashTest.php | 32 ++ .../Tecan/LiquidClass/MllLiquidClassTest.php | 18 ++ tests/Tecan/Rack/CustomRackTest.php | 24 ++ tests/Tecan/Rack/MllLabWareRackTest.php | 35 ++ tests/Tecan/TecanProtocolTest.php | 305 ++++++++++++++++++ tests/TecanScanner/TecanScannerTest.php | 72 +++++ 63 files changed, 2350 insertions(+), 4 deletions(-) create mode 100644 src/FluidXPlate/FluidXPlate.php create mode 100644 src/FluidXPlate/FluidXPlateException.php create mode 100644 src/FluidXPlate/FluidXScanner.php create mode 100644 src/FluidXPlate/InvalidRackIdException.php create mode 100644 src/FluidXPlate/InvalidTubeBarcodeException.php create mode 100644 src/FluidXPlate/Scalars/FluidXBarcode.php create mode 100644 src/FluidXPlate/Scalars/FrameStarBarcode.php create mode 100644 src/FluidXPlate/ScanFluidXPlateException.php create mode 100644 src/FluidXPlate/TestPlate.txt create mode 100644 src/Meta.php create mode 100644 src/Tecan/BasicCommands/Aspirate.php create mode 100644 src/Tecan/BasicCommands/AspirateAndDispenseParameters.php create mode 100644 src/Tecan/BasicCommands/BasicPipettingActionCommand.php create mode 100644 src/Tecan/BasicCommands/BreakCommand.php create mode 100644 src/Tecan/BasicCommands/Command.php create mode 100644 src/Tecan/BasicCommands/Comment.php create mode 100644 src/Tecan/BasicCommands/Dispense.php create mode 100644 src/Tecan/BasicCommands/ReagentDistribution.php create mode 100644 src/Tecan/BasicCommands/UsesTipMask.php create mode 100644 src/Tecan/BasicCommands/Wash.php create mode 100644 src/Tecan/CustomCommands/AspirateParameters.php create mode 100644 src/Tecan/CustomCommands/DispenseParameters.php create mode 100644 src/Tecan/CustomCommands/MllReagentDistribution.php create mode 100644 src/Tecan/CustomCommands/TransferWithAutoWash.php create mode 100644 src/Tecan/LiquidClass/CustomLiquidClass.php create mode 100644 src/Tecan/LiquidClass/LiquidClass.php create mode 100644 src/Tecan/LiquidClass/MllLiquidClass.php create mode 100644 src/Tecan/Location/BarcodeLocation.php create mode 100644 src/Tecan/Location/Location.php create mode 100644 src/Tecan/Location/PositionLocation.php create mode 100644 src/Tecan/Rack/CustomRack.php create mode 100644 src/Tecan/Rack/MllLabWareRack.php create mode 100644 src/Tecan/Rack/Rack.php create mode 100644 src/Tecan/ReagentDistribution/ReagentDistributionDirection.php create mode 100644 src/Tecan/TecanException.php create mode 100644 src/Tecan/TecanProtocol.php create mode 100644 src/Tecan/TipMask/TipMask.php create mode 100644 src/TecanScanner/NoRackIdException.php create mode 100644 src/TecanScanner/TecanScanEmptyException.php create mode 100644 src/TecanScanner/TecanScanException.php create mode 100644 src/TecanScanner/TecanScanner.php create mode 100644 src/TecanScanner/WrongNumberOfWells.php create mode 100644 tests/FluidXPlate/FluidXPlateTest.php create mode 100644 tests/FluidXPlate/FluidXScannerTest.php create mode 100644 tests/FluidXPlate/Scalars/FluidXBarcodeTest.php create mode 100644 tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php create mode 100644 tests/Tecan/BasicCommands/AspirateTest.php create mode 100644 tests/Tecan/BasicCommands/BreakTest.php create mode 100644 tests/Tecan/BasicCommands/CommentTest.php create mode 100644 tests/Tecan/BasicCommands/DispenseTest.php create mode 100644 tests/Tecan/BasicCommands/ReagentDistributionTest.php create mode 100644 tests/Tecan/BasicCommands/WashTest.php create mode 100644 tests/Tecan/CustomCommands/MllReagentDistributionTest.php create mode 100644 tests/Tecan/CustomCommands/TransferWithAutoWashTest.php create mode 100644 tests/Tecan/LiquidClass/MllLiquidClassTest.php create mode 100644 tests/Tecan/Rack/CustomRackTest.php create mode 100644 tests/Tecan/Rack/MllLabWareRackTest.php create mode 100644 tests/Tecan/TecanProtocolTest.php create mode 100644 tests/TecanScanner/TecanScannerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 50c8e5c..5f93797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases). ### Added -- Integrate `mll-lab/microplate` +- Integrate `mll-lab/microplate` and `mll-lab/liquid-handling-robotics` ## v1.12.0 diff --git a/src/FluidXPlate/FluidXPlate.php b/src/FluidXPlate/FluidXPlate.php new file mode 100644 index 0000000..037fcc2 --- /dev/null +++ b/src/FluidXPlate/FluidXPlate.php @@ -0,0 +1,68 @@ + */ + private Microplate $microplate; + + public function __construct(string $rackId) + { + if (\Safe\preg_match(self::FLUIDX_BARCODE_REGEX, $rackId) === 0) { + throw new InvalidRackIdException($rackId); + } + $this->rackId = $rackId; + $this->microplate = new Microplate(self::coordinateSystem()); + } + + public static function coordinateSystem(): CoordinateSystem96Well + { + return new CoordinateSystem96Well(); + } + + /** @param Coordinates $coordinates */ + public function addWell(Coordinates $coordinates, string $barcode): void + { + if (\Safe\preg_match(self::FLUIDX_BARCODE_REGEX, $barcode) === 0) { + throw new InvalidTubeBarcodeException($barcode); + } + + $this->microplate->addWell($coordinates, $barcode); + } + + /** @return Coordinates */ + public function addToNextFreeWell(string $content, FlowDirection $flowDirection): Coordinates + { + return $this->microplate->addToNextFreeWell($content, $flowDirection); + } + + /** @return Collection */ + public function wells(): Collection + { + return $this->microplate->wells(); + } + + /** @return Collection */ + public function freeWells(): Collection + { + return $this->microplate->freeWells(); + } + + /** @return Collection */ + public function filledWells(): Collection + { + return $this->microplate->filledWells(); + } +} diff --git a/src/FluidXPlate/FluidXPlateException.php b/src/FluidXPlate/FluidXPlateException.php new file mode 100644 index 0000000..d688fe0 --- /dev/null +++ b/src/FluidXPlate/FluidXPlateException.php @@ -0,0 +1,5 @@ +returnTestPlate(); + } + + if ($ip === '') { + throw new ScanFluidXPlateException('Cannot start scan request without an IP address.'); + } + + try { + $socket = \Safe\fsockopen($ip, 8001, $errno, $errstr, 30); + } catch (\Throwable $e) { + throw new ScanFluidXPlateException("Cannot reach FluidX Scanner {$ip}: {$e->getMessage()}. Verify that the FluidX Scanner is turned on and the FluidX software is started.", 0, $e); + } + + \Safe\fwrite($socket, "get\r\n"); + + $answer = ''; + do { + $content = fgets($socket); + $answer .= $content; + } while (is_string($content) && ! Str::contains($content, 'H12')); + + \Safe\fclose($socket); + + return self::parseRawContent($answer); + } + + public static function parseRawContent(string $rawContent): FluidXPlate + { + if ($rawContent === '') { + throw new ScanFluidXPlateException('Der Scanner lieferte ein leeres Ergebnis zurück.'); + } + + $lines = StringUtil::splitLines($rawContent); + $barcodes = []; + $id = null; + foreach ($lines as $line) { + if ($line === '' || $line === self::READING || $line === self::XTR_96_CONNECTED) { + continue; + } + $content = explode(', ', $line); + if (count($content) <= 3) { + continue; + } + + // All valid lines contain the same plate barcode + $id = $content[3]; + if ($id === FluidXScanner::NO_READ && isset($content[4])) { + $id = $content[4]; + } + + $barcodeScanResult = $content[1]; + $coordinatesString = $content[0]; + if ($barcodeScanResult !== self::NO_READ && $barcodeScanResult !== self::NO_TUBE) { + $barcodes[$coordinatesString] = $barcodeScanResult; + } + } + + if (is_null($id)) { + throw new ScanFluidXPlateException('Der Scanner lieferte keinen Plattenbarcode zurück.'); + } + + if ($id === FluidXScanner::NO_READ) { + throw new ScanFluidXPlateException($barcodes === [] + ? 'Weder Platten-Barcode noch Tube-Barcodes konnten gescannt werden. Bitte überprüfen Sie, dass die Platte korrekt in den FluidX-Scanner eingelegt wurde.' + : 'Platten-Barcode konnte nicht gescannt werden. Bitte überprüfen Sie, dass die Platte mit der korrekten Orientierung in den FluidX-Scanner eingelegt wurde.'); + } + + $plate = new FluidXPlate($id); + foreach ($barcodes as $coordinates => $barcode) { + $plate->addWell(Coordinates::fromString($coordinates, new CoordinateSystem96Well()), $barcode); + } + + return $plate; + } + + private function returnTestPlate(): FluidXPlate + { + return self::parseRawContent(\Safe\file_get_contents(__DIR__ . '/TestPlate.txt')); + } +} diff --git a/src/FluidXPlate/InvalidRackIdException.php b/src/FluidXPlate/InvalidRackIdException.php new file mode 100644 index 0000000..245de53 --- /dev/null +++ b/src/FluidXPlate/InvalidRackIdException.php @@ -0,0 +1,11 @@ +volume = $volume; + $this->location = $location; + $this->liquidClass = $liquidClass; + } + + public static function commandLetter(): string + { + return 'A'; + } +} diff --git a/src/Tecan/BasicCommands/AspirateAndDispenseParameters.php b/src/Tecan/BasicCommands/AspirateAndDispenseParameters.php new file mode 100644 index 0000000..7e6f21d --- /dev/null +++ b/src/Tecan/BasicCommands/AspirateAndDispenseParameters.php @@ -0,0 +1,34 @@ +rack = $rack; + $this->startPosition = $startPosition; + $this->endPosition = $endPosition; + } + + /** Serializes the aspirate and dispense parameters as part of a reagent distribution according the gwl file format. */ + public function toString(): string + { + return implode( + ';', + [ + $this->rack->toString(), + $this->startPosition, + $this->endPosition, + ] + ); + } +} diff --git a/src/Tecan/BasicCommands/BasicPipettingActionCommand.php b/src/Tecan/BasicCommands/BasicPipettingActionCommand.php new file mode 100644 index 0000000..01f4609 --- /dev/null +++ b/src/Tecan/BasicCommands/BasicPipettingActionCommand.php @@ -0,0 +1,44 @@ +location->toString(), + $this->volume, + $this->liquidClass->name(), + null, // tipType + $this->getTipMask(), + ] + ); + } + + protected function getTipMask(): string + { + return $this->tipMask; + } + + public function setTipMask(int $tipMask): void + { + $this->tipMask = (string) $tipMask; + } +} diff --git a/src/Tecan/BasicCommands/BreakCommand.php b/src/Tecan/BasicCommands/BreakCommand.php new file mode 100644 index 0000000..4fdbbcd --- /dev/null +++ b/src/Tecan/BasicCommands/BreakCommand.php @@ -0,0 +1,11 @@ +comment = $comment; + } + + public function toString(): string + { + return "C;{$this->comment}"; + } +} diff --git a/src/Tecan/BasicCommands/Dispense.php b/src/Tecan/BasicCommands/Dispense.php new file mode 100644 index 0000000..1e1bc27 --- /dev/null +++ b/src/Tecan/BasicCommands/Dispense.php @@ -0,0 +1,26 @@ +volume = $volume; + $this->location = $location; + $this->liquidClass = $liquidClass; + } + + public static function commandLetter(): string + { + return 'D'; + } +} diff --git a/src/Tecan/BasicCommands/ReagentDistribution.php b/src/Tecan/BasicCommands/ReagentDistribution.php new file mode 100644 index 0000000..02d9160 --- /dev/null +++ b/src/Tecan/BasicCommands/ReagentDistribution.php @@ -0,0 +1,79 @@ +|null */ + private ?array $excludedTargetWells; + + /** + * @param int|null $numberOfDitiReuses optional maximum number of DiTi reuses allowed (default 1 = no DiTi reuse) + * @param int|null $numberOfMultiDisp optional maximum number of dispenses in a multidispense sequence (default 1 = no multi-dispense) + * @param ReagentDistributionDirection|null $direction optional pipetting direction (default = LEFT_TO_RIGHT) + * @param array|null $excludedTargetWells Optional list of wells in destination labware to be excluded from pipetting + */ + public function __construct( + AspirateAndDispenseParameters $source, + AspirateAndDispenseParameters $target, + float $dispenseVolume, + LiquidClass $liquidClass, + ?int $numberOfDitiReuses = null, + ?int $numberOfMultiDisp = null, + ?ReagentDistributionDirection $direction = null, + ?array $excludedTargetWells = null + ) { + $this->source = $source; + $this->target = $target; + $this->volume = $dispenseVolume; + $this->liquidClass = $liquidClass; + $this->numberOfDitiReuses = $numberOfDitiReuses; + $this->numberOfMultiDisp = $numberOfMultiDisp; + $this->direction = $direction ?? ReagentDistributionDirection::LEFT_TO_RIGHT(); + $this->excludedTargetWells = $excludedTargetWells; + } + + public function toString(): string + { + return implode( + ';', + [ + 'R', + $this->source->toString(), + $this->target->toString(), + $this->volume, + $this->liquidClass->name(), + $this->numberOfDitiReuses, + $this->numberOfMultiDisp, + $this->direction->value, + $this->excludedWells(), + ] + ); + } + + private function excludedWells(): string + { + if (is_null($this->excludedTargetWells)) { + return ''; + } + + return implode(';', $this->excludedTargetWells); + } +} diff --git a/src/Tecan/BasicCommands/UsesTipMask.php b/src/Tecan/BasicCommands/UsesTipMask.php new file mode 100644 index 0000000..dedbd42 --- /dev/null +++ b/src/Tecan/BasicCommands/UsesTipMask.php @@ -0,0 +1,8 @@ +rack = $rack; + $this->sourcePosition = $sourcePosition; + } + + public function formatToAspirateAndDispenseParameters(): AspirateAndDispenseParameters + { + // Since the aspiration position is in almost all use-cases a single position + // and not a range of positions, this class uses only one $sourcePosition + // as startPosition and endPosition for convenience. + return new AspirateAndDispenseParameters( + $this->rack, + $this->sourcePosition, + $this->sourcePosition + ); + } +} diff --git a/src/Tecan/CustomCommands/DispenseParameters.php b/src/Tecan/CustomCommands/DispenseParameters.php new file mode 100644 index 0000000..d147b50 --- /dev/null +++ b/src/Tecan/CustomCommands/DispenseParameters.php @@ -0,0 +1,31 @@ + */ + public array $dispensePositions; + + /** @param array $dispensePositions */ + public function __construct(Rack $rack, array $dispensePositions) + { + $this->rack = $rack; + $this->dispensePositions = $dispensePositions; + } + + public function formatToAspirateAndDispenseParameters(): AspirateAndDispenseParameters + { + // We use min and max of the dispense position as start and end. + // Exclusion of the not excluded wells will happen in the calling class. + $startPosition = min($this->dispensePositions); + $endPosition = max($this->dispensePositions); + + return new AspirateAndDispenseParameters($this->rack, $startPosition, $endPosition); + } +} diff --git a/src/Tecan/CustomCommands/MllReagentDistribution.php b/src/Tecan/CustomCommands/MllReagentDistribution.php new file mode 100644 index 0000000..63c516a --- /dev/null +++ b/src/Tecan/CustomCommands/MllReagentDistribution.php @@ -0,0 +1,61 @@ +source = $source; + $this->target = $target; + $this->volume = $dispenseVolume; + $this->liquidClass = $liquidClass; + } + + public function toString(): string + { + $reagentDistribution = new ReagentDistribution( + $this->source->formatToAspirateAndDispenseParameters(), + $this->target->formatToAspirateAndDispenseParameters(), + $this->volume, + $this->liquidClass, + self::NUMBER_OF_DITI_REUSES, + self::NUMBER_OF_MULTI_DISP, + ReagentDistributionDirection::LEFT_TO_RIGHT(), + $this->excludedWells(), + ); + + return $reagentDistribution->toString(); + } + + /** @return array */ + private function excludedWells(): array + { + $min = min($this->target->dispensePositions); + $max = max($this->target->dispensePositions); + + $allWellsFromStartToEnd = range($min, $max); + + return array_diff($allWellsFromStartToEnd, $this->target->dispensePositions); + } +} diff --git a/src/Tecan/CustomCommands/TransferWithAutoWash.php b/src/Tecan/CustomCommands/TransferWithAutoWash.php new file mode 100644 index 0000000..d40cf07 --- /dev/null +++ b/src/Tecan/CustomCommands/TransferWithAutoWash.php @@ -0,0 +1,40 @@ +aspirate = new Aspirate($volume, $aspirateLocation, $liquidClass); + $this->dispense = new Dispense($volume, $dispenseLocation, $liquidClass); + } + + public function toString(): string + { + return implode(TecanProtocol::WINDOWS_NEW_LINE, [ + $this->aspirate->toString(), + $this->dispense->toString(), + (new Wash())->toString(), + ]); + } + + public function setTipMask(int $tipMask): void + { + $this->aspirate->setTipMask($tipMask); + $this->dispense->setTipMask($tipMask); + } +} diff --git a/src/Tecan/LiquidClass/CustomLiquidClass.php b/src/Tecan/LiquidClass/CustomLiquidClass.php new file mode 100644 index 0000000..09ba698 --- /dev/null +++ b/src/Tecan/LiquidClass/CustomLiquidClass.php @@ -0,0 +1,18 @@ +name = $name; + } + + public function name(): string + { + return $this->name; + } +} diff --git a/src/Tecan/LiquidClass/LiquidClass.php b/src/Tecan/LiquidClass/LiquidClass.php new file mode 100644 index 0000000..f167ffb --- /dev/null +++ b/src/Tecan/LiquidClass/LiquidClass.php @@ -0,0 +1,8 @@ +value = $value; + } + + public static function DNA_DILUTION(): self + { + return new self(self::DNA_DILUTION); + } + + public static function DNA_DILUTION_WATER(): self + { + return new self(self::DNA_DILUTION_WATER); + } + + public static function TRANSFER_PCR_PRODUKT(): self + { + return new self(self::TRANSFER_PCR_PRODUKT); + } + + public static function TRANSFER_MASTERMIX_MP(): self + { + return new self(self::TRANSFER_MASTERMIX_MP); + } + + public static function TRANSFER_TEMPLATE(): self + { + return new self(self::TRANSFER_TEMPLATE); + } + + public function name(): string + { + return $this->value; + } +} diff --git a/src/Tecan/Location/BarcodeLocation.php b/src/Tecan/Location/BarcodeLocation.php new file mode 100644 index 0000000..a134e48 --- /dev/null +++ b/src/Tecan/Location/BarcodeLocation.php @@ -0,0 +1,57 @@ +barcode = $barcode; + $this->rack = $rack; + } + + public function tubeId(): string + { + return $this->barcode; + } + + public function position(): ?string + { + return null; + } + + public function rackName(): ?string + { + return null; + } + + public function rackType(): string + { + return $this->rack->type(); + } + + public function rackId(): ?string + { + return null; + } + + public function toString(): string + { + return implode( + ';', + [ + $this->rackName(), + $this->rackId(), + $this->rackType(), + $this->position(), + $this->tubeId(), + ] + ); + } +} diff --git a/src/Tecan/Location/Location.php b/src/Tecan/Location/Location.php new file mode 100644 index 0000000..831915c --- /dev/null +++ b/src/Tecan/Location/Location.php @@ -0,0 +1,19 @@ +position = $position; + $this->rack = $rack; + } + + public function tubeId(): ?string + { + return null; + } + + public function position(): string + { + return (string) $this->position; + } + + public function rackName(): ?string + { + return $this->rack->name(); + } + + public function rackType(): string + { + return $this->rack->type(); + } + + public function rackId(): ?string + { + return null; + } + + public function toString(): string + { + return implode( + ';', + [ + $this->rackName(), + $this->rackId(), + $this->rackType(), + $this->position(), + $this->tubeId(), + ] + ); + } +} diff --git a/src/Tecan/Rack/CustomRack.php b/src/Tecan/Rack/CustomRack.php new file mode 100644 index 0000000..dece89b --- /dev/null +++ b/src/Tecan/Rack/CustomRack.php @@ -0,0 +1,46 @@ +type = $type; + $this->name = $name; + $this->barcode = $barcode; + } + + public function name(): string + { + return $this->name; + } + + public function type(): string + { + return $this->type; + } + + public function id(): ?string + { + return $this->barcode; + } + + public function toString(): string + { + return implode( + ';', + [ + $this->name(), + $this->id(), + $this->type(), + ] + ); + } +} diff --git a/src/Tecan/Rack/MllLabWareRack.php b/src/Tecan/Rack/MllLabWareRack.php new file mode 100644 index 0000000..1c4591c --- /dev/null +++ b/src/Tecan/Rack/MllLabWareRack.php @@ -0,0 +1,116 @@ +value = $value; + } + + public static function A(): self + { + return new self(self::A); + } + + public static function MP_CDNA(): self + { + return new self(self::MP_CDNA); + } + + public static function MP_SAMPLE(): self + { + return new self(self::MP_SAMPLE); + } + + public static function MP_WATER(): self + { + return new self(self::MP_WATER); + } + + public static function FLUID_X(): self + { + return new self(self::FLUID_X); + } + + public static function MM(): self + { + return new self(self::MM); + } + + public static function DEST_LC(): self + { + return new self(self::DEST_LC); + } + + public static function DEST_PCR(): self + { + return new self(self::DEST_PCR); + } + + public static function DEST_TAQMAN(): self + { + return new self(self::DEST_TAQMAN); + } + + public function id(): ?string + { + return null; + } + + public function name(): string + { + return $this->value; + } + + public function type(): string + { + switch ($this->value) { + case self::A: + return 'Eppis 24x0.5 ml Cooled'; + case self::MP_CDNA: + return 'MP cDNA'; + case self::MP_SAMPLE: + return 'MP Microplate'; + case self::MP_WATER: + return 'Trough 300ml MCA Portrait'; + case self::FLUID_X: + return '96FluidX'; + case self::MM: + return 'Eppis 32x1.5 ml Cooled'; + case self::DEST_LC: + return '96 Well MP LightCycler480'; + case self::DEST_PCR: + return '96 Well PCR ABI semi-skirted'; + case self::DEST_TAQMAN: + return '96 Well PCR TaqMan'; + default: + throw new \Exception('Type not defined for ' . $this->value); + } + } + + public function toString(): string + { + return implode( + ';', + [ + $this->name(), + $this->id(), + $this->type(), + ] + ); + } +} diff --git a/src/Tecan/Rack/Rack.php b/src/Tecan/Rack/Rack.php new file mode 100644 index 0000000..f827c5f --- /dev/null +++ b/src/Tecan/Rack/Rack.php @@ -0,0 +1,18 @@ +value = $value; + } + + public static function LEFT_TO_RIGHT(): self + { + return new self(self::LEFT_TO_RIGHT); + } + + public static function RIGHT_TO_LEFT(): self + { + return new self(self::RIGHT_TO_LEFT); + } +} diff --git a/src/Tecan/TecanException.php b/src/Tecan/TecanException.php new file mode 100644 index 0000000..0c9d16c --- /dev/null +++ b/src/Tecan/TecanException.php @@ -0,0 +1,5 @@ + */ + private Collection $commands; + + private TipMask $tipMask; + + private string $protocolName; + + public function __construct(TipMask $tipMask, ?string $protocolName = null, ?string $userName = null) + { + $this->protocolName = $protocolName ?? Str::uuid()->toString(); + $this->tipMask = $tipMask; + + $this->commands = $this->initHeader($userName, $protocolName); + } + + public function addCommand(Command $command): void + { + $this->commands->add($command); + } + + /** @param Command&UsesTipMask $command */ + public function addCommandCurrentTip(Command $command): void + { + $command->setTipMask($this->tipMask->currentTip ?? TipMask::firstTip()); + + $this->commands->add($command); + } + + /** @param Command&UsesTipMask $command */ + public function addCommandForNextTip(Command $command): void + { + if ($this->tipMask->isLastTip()) { + $this->commands->add(new BreakCommand()); + } + + $command->setTipMask($this->tipMask->nextTip()); + + $this->commands->add($command); + } + + public function buildProtocol(): string + { + return $this->commands + ->map(fn (Command $command): string => $command->toString()) + ->join(self::WINDOWS_NEW_LINE) + . self::WINDOWS_NEW_LINE; + } + + public function fileName(): string + { + return $this->protocolName . self::GEMINI_WORKLIST_FILENAME_SUFFIX; + } + + /** @return Collection */ + private function initHeader(?string $userName, ?string $protocolName): Collection + { + $package = Meta::PACKAGE_NAME; + $version = InstalledVersions::getPrettyVersion($package); + + $now = Carbon::now(); + + /** @var Collection $commentCommands necessary due to contravariance issues with the generic collection */ + $commentCommands = new Collection([ + new Comment("Created by {$package} {$version}"), + new Comment("Date: {$now}"), + ]); + + if ($userName !== null) { + $commentCommands->add(new Comment("User: {$userName}")); + } + if ($protocolName !== null) { + $commentCommands->add(new Comment("Protocol name: {$protocolName}")); + } + + return $commentCommands; + } +} diff --git a/src/Tecan/TipMask/TipMask.php b/src/Tecan/TipMask/TipMask.php new file mode 100644 index 0000000..5cc9608 --- /dev/null +++ b/src/Tecan/TipMask/TipMask.php @@ -0,0 +1,57 @@ +value = $value; + } + + public static function FOUR_TIPS(): self + { + return new self(self::FOUR_TIPS); + } + + public static function EIGHT_TIPS(): self + { + return new self(self::EIGHT_TIPS); + } + + public static function firstTip(): int + { + return 1; + } + + public function isLastTip(): bool + { + switch ($this->value) { + case self::FOUR_TIPS: + return $this->currentTip === 8; + case self::EIGHT_TIPS: + return $this->currentTip === 128; + default: + throw new TecanException("isLastTip not defined for {$this->value}."); + } + } + + public function nextTip(): int + { + $this->currentTip = $this->currentTip === null || $this->isLastTip() + ? self::firstTip() + // due to the bitwise nature we can simply multiply the current tip by 2 if we want to specify the next tip. + : $this->currentTip * 2; + + return $this->currentTip; + } +} diff --git a/src/TecanScanner/NoRackIdException.php b/src/TecanScanner/NoRackIdException.php new file mode 100644 index 0000000..ea9fce3 --- /dev/null +++ b/src/TecanScanner/NoRackIdException.php @@ -0,0 +1,11 @@ +shift(); + + if (! is_string($firstLineWithRackId) || ! Str::startsWith($firstLineWithRackId, self::RACKID_IDENTIFIER)) { + throw new NoRackIdException(); + } + $rackId = Str::substr($firstLineWithRackId, strlen(self::RACKID_IDENTIFIER)); + + $expectedCount = FluidXPlate::coordinateSystem()->positionsCount(); + $actualCount = $lines->count(); + if ($expectedCount !== $actualCount) { + throw new WrongNumberOfWells($expectedCount, $actualCount); + } + + $plate = new FluidXPlate($rackId); + + foreach ($lines as $line) { + $barcode = Str::after($line, ','); + + if ($barcode !== self::NO_READ) { + $coordinatesString = Str::before($line, ','); + + $plate->addWell( + Coordinates::fromString( + $coordinatesString, + $plate::coordinateSystem() + ), + $barcode + ); + } + } + + return $plate; + } + + /** Checks if a string can be parsed into a FluidXPlate. */ + public static function isValidRawContent(string $rawContent): bool + { + $lines = StringUtil::splitLines($rawContent); + + if (count($lines) !== 97) { + return false; + } + if (\Safe\preg_match(/* @lang RegExp */ '/^' . self::RACKID_IDENTIFIER . FluidXPlate::FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER . '$/', array_shift($lines)) === 0) { + return false; + } + foreach ($lines as $line) { + if (\Safe\preg_match(/* @lang RegExp */ '/^[A-H][1-12],' . FluidXPlate::FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER . '|' . self::NO_READ . '$/', $line) !== 1) { + return false; + } + } + + return true; + } +} diff --git a/src/TecanScanner/WrongNumberOfWells.php b/src/TecanScanner/WrongNumberOfWells.php new file mode 100644 index 0000000..9537c74 --- /dev/null +++ b/src/TecanScanner/WrongNumberOfWells.php @@ -0,0 +1,11 @@ +expectExceptionObject(new InvalidRackIdException($rackId)); + new FluidXPlate($rackId); + } + + public function testCreateWithRandomNameAndReturnsIt(): void + { + $rackId = 'testInvalidRackId'; + $this->expectExceptionObject(new InvalidRackIdException($rackId)); + new FluidXPlate($rackId); + } + + public function testCreatesSuccessfulWithValidBarCode(): void + { + $rackId = 'AB12345678'; + $fluidXPlate = new FluidXPlate($rackId); + self::assertSame($rackId, $fluidXPlate->rackId); + self::assertCount(96, $fluidXPlate->wells()); + self::assertCount(96, $fluidXPlate->freeWells()); + self::assertCount(0, $fluidXPlate->filledWells()); + } + + public function testCanNotAddInvalidBarcode(): void + { + $barcode = 'testWrongBarcode'; + $rackId = 'AB12345678'; + $fluidXPlate = new FluidXPlate($rackId); + $coordinates = Coordinates::fromString('A1', new CoordinateSystem96Well()); + + $this->expectExceptionObject(new InvalidTubeBarcodeException($barcode)); + $fluidXPlate->addWell($coordinates, $barcode); + } + + public function testCanOnlyAddStringAsBarcode(): void + { + $rackId = 'AB12345678'; + $fluidXPlate = new FluidXPlate($rackId); + $coordinates = Coordinates::fromString('A1', new CoordinateSystem96Well()); + + $this->expectException(\TypeError::class); + // @phpstan-ignore-next-line intentionally wrong + $fluidXPlate->addWell($coordinates, []); + } + + public function testCanAddToNextFreeWell(): void + { + $rackId = 'AB12345678'; + $fluidXPlate = new FluidXPlate($rackId); + $expectedCoordinates = Coordinates::fromString('A1', new CoordinateSystem96Well()); + $addToNextFreeWell = $fluidXPlate->addToNextFreeWell('test', FlowDirection::COLUMN()); + self::assertEquals($expectedCoordinates, $addToNextFreeWell); + } +} diff --git a/tests/FluidXPlate/FluidXScannerTest.php b/tests/FluidXPlate/FluidXScannerTest.php new file mode 100644 index 0000000..e6cb425 --- /dev/null +++ b/tests/FluidXPlate/FluidXScannerTest.php @@ -0,0 +1,22 @@ +scanPlate(FluidXScanner::LOCALHOST); + + self::assertSame('SA00826894', $fluidXPlate->rackId); + $filledWells = $fluidXPlate->filledWells(); + self::assertCount(3, $filledWells); + self::assertSame('FD20024619', $filledWells->get('A1')); + self::assertSame('FD20024698', $filledWells->get('A2')); + self::assertSame('FD20024711', $filledWells->get('A3')); + } +} diff --git a/tests/FluidXPlate/Scalars/FluidXBarcodeTest.php b/tests/FluidXPlate/Scalars/FluidXBarcodeTest.php new file mode 100644 index 0000000..f518aa1 --- /dev/null +++ b/tests/FluidXPlate/Scalars/FluidXBarcodeTest.php @@ -0,0 +1,43 @@ +expectExceptionObject(new InvariantViolation('The given value "foo123445" did not match the regex /[A-Z]{2}(\d){8}/.')); + + (new FluidXBarcode())->serialize('foo123445'); + } + + public function testSerializePassesWhenIsValid(): void + { + $input = 'AZ12345678'; + $serializedResult = (new FluidXBarcode())->serialize($input); + + self::assertSame($input, $serializedResult); + } + + public function testParseValueThrowsIfIsInvalid(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('The given value "foo123445" did not match the regex /[A-Z]{2}(\d){8}/.'); + + (new FluidXBarcode())->parseValue('foo123445'); + } + + public function testParseValuePassesIfIsValid(): void + { + $input = 'AZ12345678'; + self::assertSame( + $input, + (new FluidXBarcode())->parseValue($input) + ); + } +} diff --git a/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php b/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php new file mode 100644 index 0000000..0b8c14e --- /dev/null +++ b/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php @@ -0,0 +1,44 @@ +expectException(InvariantViolation::class); + $this->expectExceptionMessage('The given value "foo" did not match the regex /[A-Z]{2}(\d){6}/.'); + + (new FrameStarBarcode())->serialize('foo'); + } + + public function testSerializePassesWhenIsValid(): void + { + $input = 'AZ123456'; + $serializedResult = (new FrameStarBarcode())->serialize($input); + + self::assertSame($input, $serializedResult); + } + + public function testParseValueThrowsIfIsInvalid(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('The given value "bar" did not match the regex /[A-Z]{2}(\d){6}/.'); + + (new FrameStarBarcode())->parseValue('bar'); + } + + public function testParseValuePassesIfIsValid(): void + { + $input = 'AZ123456'; + self::assertSame( + $input, + (new FrameStarBarcode())->parseValue($input) + ); + } +} diff --git a/tests/QxManager/FilledRowTest.php b/tests/QxManager/FilledRowTest.php index db6ded4..4d5e79f 100644 --- a/tests/QxManager/FilledRowTest.php +++ b/tests/QxManager/FilledRowTest.php @@ -1,6 +1,6 @@ location->tubeId()); + self::assertNull($aspirate->location->position()); + self::assertSame('A;;;TestRackType;;barcode;100;TestLiquidClassName;;', $aspirate->toString()); + } + + public function testAspirateWithPositionLocation(): void + { + $position = 7; + $volume = 2.2; + $aspirate = new Aspirate($volume, new PositionLocation($position, MllLabWareRack::DEST_PCR()), MllLiquidClass::TRANSFER_TEMPLATE()); + self::assertNull($aspirate->location->tubeId()); + self::assertSame((string) $position, $aspirate->location->position()); + self::assertSame('A;DestPCR;;96 Well PCR ABI semi-skirted;' . $position . ';;2.2;Transfer_Template;;', $aspirate->toString()); + } +} diff --git a/tests/Tecan/BasicCommands/BreakTest.php b/tests/Tecan/BasicCommands/BreakTest.php new file mode 100644 index 0000000..4982ca0 --- /dev/null +++ b/tests/Tecan/BasicCommands/BreakTest.php @@ -0,0 +1,14 @@ +toString()); + } +} diff --git a/tests/Tecan/BasicCommands/CommentTest.php b/tests/Tecan/BasicCommands/CommentTest.php new file mode 100644 index 0000000..78d6d17 --- /dev/null +++ b/tests/Tecan/BasicCommands/CommentTest.php @@ -0,0 +1,17 @@ +toString()); + } +} diff --git a/tests/Tecan/BasicCommands/DispenseTest.php b/tests/Tecan/BasicCommands/DispenseTest.php new file mode 100644 index 0000000..a627269 --- /dev/null +++ b/tests/Tecan/BasicCommands/DispenseTest.php @@ -0,0 +1,34 @@ +location->tubeId()); + self::assertNull($aspirate->location->position()); + self::assertSame('D;;;TestRackType;;barcode;100;TestLiquidClassName;;', $aspirate->toString()); + } + + public function testDispenseWithPositionLocation(): void + { + $position = 7; + $volume = 2.2; + $aspirate = new Dispense($volume, new PositionLocation($position, MllLabWareRack::DEST_LC()), MllLiquidClass::TRANSFER_TEMPLATE()); + self::assertNull($aspirate->location->tubeId()); + self::assertSame((string) $position, $aspirate->location->position()); + self::assertSame("D;DestLC;;96 Well MP LightCycler480;{$position};;{$volume};Transfer_Template;;", $aspirate->toString()); + } +} diff --git a/tests/Tecan/BasicCommands/ReagentDistributionTest.php b/tests/Tecan/BasicCommands/ReagentDistributionTest.php new file mode 100644 index 0000000..cc9d547 --- /dev/null +++ b/tests/Tecan/BasicCommands/ReagentDistributionTest.php @@ -0,0 +1,66 @@ +name()};;{$sourceRack->type()};{$sourceStartPosition};{$sourceEndPosition};{$targetRack->name()};;{$targetRack->type()};{$targetStartPosition};{$targetEndPosition};{$dispenseVolume};{$liquidClass->name()};{$numberOfDitiReuses};{$numberOfMultiDisp};{$direction->value};"; + self::assertSame( + $commandStringWithoutExcludedWells, + $commandWithoutExcludedWells->toString() + ); + + $excludedWells = [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71]; + $commandWithExcludedWells = new ReagentDistribution( + $source, + $target, + $dispenseVolume, + $liquidClass, + $numberOfDitiReuses, + $numberOfMultiDisp, + $direction, + $excludedWells + ); + + self::assertSame( + "{$commandStringWithoutExcludedWells}50;51;52;53;54;55;56;57;58;59;60;61;62;63;64;65;66;67;68;69;70;71", + $commandWithExcludedWells->toString() + ); + } +} diff --git a/tests/Tecan/BasicCommands/WashTest.php b/tests/Tecan/BasicCommands/WashTest.php new file mode 100644 index 0000000..d714837 --- /dev/null +++ b/tests/Tecan/BasicCommands/WashTest.php @@ -0,0 +1,14 @@ +toString()); + } +} diff --git a/tests/Tecan/CustomCommands/MllReagentDistributionTest.php b/tests/Tecan/CustomCommands/MllReagentDistributionTest.php new file mode 100644 index 0000000..2fd753a --- /dev/null +++ b/tests/Tecan/CustomCommands/MllReagentDistributionTest.php @@ -0,0 +1,41 @@ +name()};;{$sourceRack->type()};{$sourceStartPosition};{$sourceStartPosition};{$targetRack->name()};;{$targetRack->type()};48;73;{$dispenseVolume};{$liquidClass->name()};{$numberOfDitiReuses};{$numberOfMultiDisp};0;50;51;52;53;54;55;56;57;58;59;60;61;62;63;64;65;66;67;68;69;70;71", + $command->toString() + ); + } +} diff --git a/tests/Tecan/CustomCommands/TransferWithAutoWashTest.php b/tests/Tecan/CustomCommands/TransferWithAutoWashTest.php new file mode 100644 index 0000000..6da8c51 --- /dev/null +++ b/tests/Tecan/CustomCommands/TransferWithAutoWashTest.php @@ -0,0 +1,32 @@ +toString() + ); + } +} diff --git a/tests/Tecan/LiquidClass/MllLiquidClassTest.php b/tests/Tecan/LiquidClass/MllLiquidClassTest.php new file mode 100644 index 0000000..2cb6c2e --- /dev/null +++ b/tests/Tecan/LiquidClass/MllLiquidClassTest.php @@ -0,0 +1,18 @@ +name()); + self::assertSame('DNA_Dilution_Water', MllLiquidClass::DNA_DILUTION_WATER()->name()); + self::assertSame('Transfer_PCR_Produkt', MllLiquidClass::TRANSFER_PCR_PRODUKT()->name()); + self::assertSame('Transfer_Mastermix_MP', MllLiquidClass::TRANSFER_MASTERMIX_MP()->name()); + self::assertSame('Transfer_Template', MllLiquidClass::TRANSFER_TEMPLATE()->name()); + } +} diff --git a/tests/Tecan/Rack/CustomRackTest.php b/tests/Tecan/Rack/CustomRackTest.php new file mode 100644 index 0000000..945a552 --- /dev/null +++ b/tests/Tecan/Rack/CustomRackTest.php @@ -0,0 +1,24 @@ +name()); + self::assertSame($type, $customRack->type()); + self::assertSame($barcode, $customRack->id()); + + self::assertSame('name;barcode;type', $customRack->toString()); + } +} diff --git a/tests/Tecan/Rack/MllLabWareRackTest.php b/tests/Tecan/Rack/MllLabWareRackTest.php new file mode 100644 index 0000000..eeacf4d --- /dev/null +++ b/tests/Tecan/Rack/MllLabWareRackTest.php @@ -0,0 +1,35 @@ +name()); + self::assertSame('MPCDNA', MllLabWareRack::MP_CDNA()->name()); + self::assertSame('MPSample', MllLabWareRack::MP_SAMPLE()->name()); + self::assertSame('MPWasser', MllLabWareRack::MP_WATER()->name()); + self::assertSame('FluidX', MllLabWareRack::FLUID_X()->name()); + self::assertSame('MM', MllLabWareRack::MM()->name()); + self::assertSame('DestLC', MllLabWareRack::DEST_LC()->name()); + self::assertSame('DestPCR', MllLabWareRack::DEST_PCR()->name()); + self::assertSame('DestTaqMan', MllLabWareRack::DEST_TAQMAN()->name()); + } + + public function testValueOfEnum(): void + { + self::assertSame('Eppis 24x0.5 ml Cooled', MllLabWareRack::A()->type()); + self::assertSame('MP cDNA', MllLabWareRack::MP_CDNA()->type()); + self::assertSame('MP Microplate', MllLabWareRack::MP_SAMPLE()->type()); + self::assertSame('Trough 300ml MCA Portrait', MllLabWareRack::MP_WATER()->type()); + self::assertSame('96FluidX', MllLabWareRack::FLUID_X()->type()); + self::assertSame('Eppis 32x1.5 ml Cooled', MllLabWareRack::MM()->type()); + self::assertSame('96 Well MP LightCycler480', MllLabWareRack::DEST_LC()->type()); + self::assertSame('96 Well PCR ABI semi-skirted', MllLabWareRack::DEST_PCR()->type()); + self::assertSame('96 Well PCR TaqMan', MllLabWareRack::DEST_TAQMAN()->type()); + } +} diff --git a/tests/Tecan/TecanProtocolTest.php b/tests/Tecan/TecanProtocolTest.php new file mode 100644 index 0000000..30b6fce --- /dev/null +++ b/tests/Tecan/TecanProtocolTest.php @@ -0,0 +1,305 @@ +fileName()); + self::assertSame( + StringUtil::normalizeLineEndings( + <<initComment()}C;User: username +C;Protocol name: testProtocol + +CSV + ), + $tecanProtocol->buildProtocol() + ); + } + + public function testProtocolUuidName(): void + { + $tecanProtocol = new TecanProtocol(TipMask::FOUR_TIPS()); + + $fileName = Str::before($tecanProtocol->fileName(), TecanProtocol::GEMINI_WORKLIST_FILENAME_SUFFIX); + self::assertTrue(Str::isUuid($fileName)); + + $fileSuffix = Str::after($tecanProtocol->fileName(), $fileName); + self::assertSame($fileSuffix, TecanProtocol::GEMINI_WORKLIST_FILENAME_SUFFIX); + } + + public function testProtocolWithForFourTips(): void + { + $tecanProtocol = new TecanProtocol(TipMask::FOUR_TIPS()); + + $liquidClass = new CustomLiquidClass('TestLiquidClassName'); + $rack = new CustomRack('TestRackName', 'TestRackType'); + $aspirateLocation = new BarcodeLocation('barcode', $rack); + $dispenseLocation = new BarcodeLocation('barcode1', $rack); + + foreach (range(1, 5) as $_) { + $tecanProtocol->addCommandForNextTip( + new TransferWithAutoWash(100, $liquidClass, $aspirateLocation, $dispenseLocation) + ); + } + self::assertSame( + StringUtil::normalizeLineEndings( + <<initComment()}A;;;TestRackType;;barcode;100;TestLiquidClassName;;1 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;1 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;2 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;2 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;4 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;4 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;8 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;8 +W; +B; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;1 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;1 +W; + +CSV + ), + $tecanProtocol->buildProtocol() + ); + } + + public function testProtocolWithForForTipsAndManualWash(): void + { + $tecanProtocol = new TecanProtocol(TipMask::FOUR_TIPS()); + + $liquidClass = new CustomLiquidClass('TestLiquidClassName'); + $rack = new CustomRack('TestRackName', 'TestRackType'); + $aspirateLocation = new BarcodeLocation('barcode', $rack); + $dispenseLocation = new BarcodeLocation('barcode1', $rack); + + foreach (range(1, 5) as $_) { + $tecanProtocol->addCommandForNextTip( + new Aspirate(100, $aspirateLocation, $liquidClass), + ); + $tecanProtocol->addCommandCurrentTip( + new Dispense(100, $dispenseLocation, $liquidClass) + ); + $tecanProtocol->addCommand( + new Wash() + ); + } + + self::assertSame( + StringUtil::normalizeLineEndings( + <<initComment()}A;;;TestRackType;;barcode;100;TestLiquidClassName;;1 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;1 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;2 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;2 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;4 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;4 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;8 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;8 +W; +B; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;1 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;1 +W; + +CSV + ), + $tecanProtocol->buildProtocol() + ); + } + + public function testProtocolWithForEightTips(): void + { + $tecanProtocol = new TecanProtocol(TipMask::EIGHT_TIPS()); + + $liquidClass = new CustomLiquidClass('TestLiquidClassName'); + $rack = new CustomRack('TestRackName', 'TestRackType'); + $aspirateLocation = new BarcodeLocation('barcode', $rack); + $dispenseLocation = new BarcodeLocation('barcode1', $rack); + + foreach (range(1, 10) as $_) { + $tecanProtocol->addCommandForNextTip( + new TransferWithAutoWash(100, $liquidClass, $aspirateLocation, $dispenseLocation) + ); + } + + self::assertSame( + StringUtil::normalizeLineEndings( + <<initComment()}A;;;TestRackType;;barcode;100;TestLiquidClassName;;1 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;1 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;2 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;2 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;4 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;4 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;8 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;8 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;16 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;16 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;32 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;32 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;64 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;64 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;128 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;128 +W; +B; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;1 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;1 +W; +A;;;TestRackType;;barcode;100;TestLiquidClassName;;2 +D;;;TestRackType;;barcode1;100;TestLiquidClassName;;2 +W; + +CSV + ), + $tecanProtocol->buildProtocol() + ); + } + + public function testReagentDistributionProtocol(): void + { + $sourceRack = MllLabWareRack::MM(); + $targetRack = MllLabWareRack::DEST_PCR(); + $dispenseVolume = 24; + $liquidClass = MllLiquidClass::TRANSFER_MASTERMIX_MP(); + + $tecanProtocol = new TecanProtocol(TipMask::FOUR_TIPS()); + + $dispensePositions = [1, 2, 3, 4, 5, 57]; + $tecanProtocol->addCommand( + new MllReagentDistribution( + new AspirateParameters($sourceRack, 1), + new DispenseParameters($targetRack, $dispensePositions), + $dispenseVolume, + $liquidClass, + ) + ); + + $dispensePositions1 = [6, 7, 50, 58, 74, 75]; + $tecanProtocol->addCommand( + new MllReagentDistribution( + new AspirateParameters($sourceRack, 2), + new DispenseParameters($targetRack, $dispensePositions1), + $dispenseVolume, + $liquidClass, + ) + ); + + $dispensePositions2 = [8, 10, 51, 59]; + $tecanProtocol->addCommand( + new MllReagentDistribution( + new AspirateParameters($sourceRack, 3), + new DispenseParameters($targetRack, $dispensePositions2), + $dispenseVolume, + $liquidClass, + ) + ); + + $dispensePositions3 = [11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 52, 60]; + $tecanProtocol->addCommand( + new MllReagentDistribution( + new AspirateParameters($sourceRack, 4), + new DispenseParameters($targetRack, $dispensePositions3), + $dispenseVolume, + $liquidClass, + ) + ); + + $dispensePositions4 = [24, 25, 26, 27, 28, 29, 30, 53, 61]; + $tecanProtocol->addCommand( + new MllReagentDistribution( + new AspirateParameters($sourceRack, 5), + new DispenseParameters($targetRack, $dispensePositions4), + $dispenseVolume, + $liquidClass, + ) + ); + $dispensePositions5 = [1, 2, 3, 4, 5]; + $tecanProtocol->addCommand( + new MllReagentDistribution( + new AspirateParameters($sourceRack, 5), + new DispenseParameters($targetRack, $dispensePositions5), + $dispenseVolume, + $liquidClass, + ) + ); + + self::assertSame( + StringUtil::normalizeLineEndings( + <<initComment()}R;MM;;Eppis 32x1.5 ml Cooled;1;1;DestPCR;;96 Well PCR ABI semi-skirted;1;57;24;Transfer_Mastermix_MP;6;1;0;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22;23;24;25;26;27;28;29;30;31;32;33;34;35;36;37;38;39;40;41;42;43;44;45;46;47;48;49;50;51;52;53;54;55;56 +R;MM;;Eppis 32x1.5 ml Cooled;2;2;DestPCR;;96 Well PCR ABI semi-skirted;6;75;24;Transfer_Mastermix_MP;6;1;0;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22;23;24;25;26;27;28;29;30;31;32;33;34;35;36;37;38;39;40;41;42;43;44;45;46;47;48;49;51;52;53;54;55;56;57;59;60;61;62;63;64;65;66;67;68;69;70;71;72;73 +R;MM;;Eppis 32x1.5 ml Cooled;3;3;DestPCR;;96 Well PCR ABI semi-skirted;8;59;24;Transfer_Mastermix_MP;6;1;0;9;11;12;13;14;15;16;17;18;19;20;21;22;23;24;25;26;27;28;29;30;31;32;33;34;35;36;37;38;39;40;41;42;43;44;45;46;47;48;49;50;52;53;54;55;56;57;58 +R;MM;;Eppis 32x1.5 ml Cooled;4;4;DestPCR;;96 Well PCR ABI semi-skirted;11;60;24;Transfer_Mastermix_MP;6;1;0;12;23;24;25;26;27;28;29;30;31;32;33;34;35;36;37;38;39;40;41;42;43;44;45;46;47;48;49;50;51;53;54;55;56;57;58;59 +R;MM;;Eppis 32x1.5 ml Cooled;5;5;DestPCR;;96 Well PCR ABI semi-skirted;24;61;24;Transfer_Mastermix_MP;6;1;0;31;32;33;34;35;36;37;38;39;40;41;42;43;44;45;46;47;48;49;50;51;52;54;55;56;57;58;59;60 +R;MM;;Eppis 32x1.5 ml Cooled;5;5;DestPCR;;96 Well PCR ABI semi-skirted;1;5;24;Transfer_Mastermix_MP;6;1;0; + +CSV + ), + $tecanProtocol->buildProtocol() + ); + } + + private function initComment(): string + { + $version = InstalledVersions::getPrettyVersion(Meta::PACKAGE_NAME); + + return "C;Created by mll-lab/php-utils {$version} +C;Date: 2022-10-05 13:34:32 +"; + } +} diff --git a/tests/TecanScanner/TecanScannerTest.php b/tests/TecanScanner/TecanScannerTest.php new file mode 100644 index 0000000..f6532f9 --- /dev/null +++ b/tests/TecanScanner/TecanScannerTest.php @@ -0,0 +1,72 @@ +expectExceptionObject(new TecanScanEmptyException()); + TecanScanner::parseRawContent($rawContent); + } + + public function testCreateFromUnexpectedLineCount(): void + { + $rawContent = "rackid,SA00411242\nA1,FD13945423\nB1,FD32807353"; + self::assertFalse(TecanScanner::isValidRawContent($rawContent)); + + $this->expectExceptionObject(new WrongNumberOfWells(96, 2)); + TecanScanner::parseRawContent($rawContent); + } + + public function testMicroplateHandlesDuplicateCoordinates(): void + { + $rawContent = "rackid,SA00411242\nA1,FD13945423\nA1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"; + self::assertTrue(TecanScanner::isValidRawContent($rawContent)); + + $this->expectExceptionObject(new WellNotEmptyException('Well with coordinates "A1" is not empty. Use setWell() to overwrite the coordinate. Well content "s:10:"FD32807353";" was not added.')); + TecanScanner::parseRawContent($rawContent); + } + + public function testNoBarcode(): void + { + $rawContent = "A1,FD13945423\nB1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"; + self::assertFalse(TecanScanner::isValidRawContent($rawContent)); + + $this->expectExceptionObject(new NoRackIdException()); + TecanScanner::parseRawContent($rawContent); + } + + public function testSuccess(): void + { + $rawContent = "rackid,SA00411242\nA1,FD13945423\nB1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"; + + $fluidXPlate = TecanScanner::parseRawContent($rawContent); + self::assertCount(96, $fluidXPlate->wells()); + self::assertCount(94, $fluidXPlate->freeWells()); + self::assertSame('SA00411242', $fluidXPlate->rackId); + + self::assertSame([ + 'A1' => 'FD13945423', + 'B1' => 'FD32807353', + ], $fluidXPlate->filledWells()->toArray()); + self::assertTrue(TecanScanner::isValidRawContent($rawContent)); + } + + public function testSuccessForWindowsNewLine(): void + { + $rawContent = "rackid,SA00411242\r\nA1,FD13945423\r\nB1,FD32807353\r\nC1,NO READ\r\nD1,NO READ\r\nE1,NO READ\r\nF1,NO READ\r\nG1,NO READ\r\nH1,NO READ\r\nA2,NO READ\r\nB2,NO READ\r\nC2,NO READ\r\nD2,NO READ\r\nE2,NO READ\r\nF2,NO READ\r\nG2,NO READ\r\nH2,NO READ\r\nA3,NO READ\r\nB3,NO READ\r\nC3,NO READ\r\nD3,NO READ\r\nE3,NO READ\r\nF3,NO READ\r\nG3,NO READ\r\nH3,NO READ\r\nA4,NO READ\r\nB4,NO READ\r\nC4,NO READ\r\nD4,NO READ\r\nE4,NO READ\r\nF4,NO READ\r\nG4,NO READ\r\nH4,NO READ\r\nA5,NO READ\r\nB5,NO READ\r\nC5,NO READ\r\nD5,NO READ\r\nE5,NO READ\r\nF5,NO READ\r\nG5,NO READ\r\nH5,NO READ\r\nA6,NO READ\r\nB6,NO READ\r\nC6,NO READ\r\nD6,NO READ\r\nE6,NO READ\r\nF6,NO READ\r\nG6,NO READ\r\nH6,NO READ\r\nA7,NO READ\r\nB7,NO READ\r\nC7,NO READ\r\nD7,NO READ\r\nE7,NO READ\r\nF7,NO READ\r\nG7,NO READ\r\nH7,NO READ\r\nA8,NO READ\r\nB8,NO READ\r\nC8,NO READ\r\nD8,NO READ\r\nE8,NO READ\r\nF8,NO READ\r\nG8,NO READ\r\nH8,NO READ\r\nA9,NO READ\r\nB9,NO READ\r\nC9,NO READ\r\nD9,NO READ\r\nE9,NO READ\r\nF9,NO READ\r\nG9,NO READ\r\nH9,NO READ\r\nA10,NO READ\r\nB10,NO READ\r\nC10,NO READ\r\nD10,NO READ\r\nE10,NO READ\r\nF10,NO READ\r\nG10,NO READ\r\nH10,NO READ\r\nA11,NO READ\r\nB11,NO READ\r\nC11,NO READ\r\nD11,NO READ\r\nE11,NO READ\r\nF11,NO READ\r\nG11,NO READ\r\nH11,NO READ\r\nA12,NO READ\r\nB12,NO READ\r\nC12,NO READ\r\nD12,NO READ\r\nE12,NO READ\r\nF12,NO READ\r\nG12,NO READ\r\nH12,NO READ"; + + self::assertTrue(TecanScanner::isValidRawContent($rawContent)); + } +} From 8f19b1ad61227f44765559e9a7af2d6f2915d594 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 15:55:44 +0200 Subject: [PATCH 07/16] ID --- src/FluidXPlate/FluidXPlate.php | 10 +++---- src/FluidXPlate/InvalidRackIDException.php | 11 ++++++++ src/Tecan/Location/BarcodeLocation.php | 8 +++--- src/Tecan/Location/Location.php | 4 +-- src/Tecan/Location/PositionLocation.php | 8 +++--- src/TecanScanner/NoRackIDException.php | 11 ++++++++ src/TecanScanner/TecanScanner.php | 10 +++---- tests/FluidXPlate/FluidXPlateTest.php | 32 +++++++++++----------- tests/FluidXPlate/FluidXScannerTest.php | 2 +- tests/StringUtilTest.php | 2 +- tests/StringUtilTestData/windows-1252.csv | 2 +- tests/Tecan/BasicCommands/AspirateTest.php | 4 +-- tests/Tecan/BasicCommands/DispenseTest.php | 4 +-- tests/TecanScanner/TecanScannerTest.php | 6 ++-- 14 files changed, 68 insertions(+), 46 deletions(-) create mode 100644 src/FluidXPlate/InvalidRackIDException.php create mode 100644 src/TecanScanner/NoRackIDException.php diff --git a/src/FluidXPlate/FluidXPlate.php b/src/FluidXPlate/FluidXPlate.php index 037fcc2..98370dc 100644 --- a/src/FluidXPlate/FluidXPlate.php +++ b/src/FluidXPlate/FluidXPlate.php @@ -13,17 +13,17 @@ final class FluidXPlate public const FLUIDX_BARCODE_REGEX = /* @lang RegExp */ '/' . self::FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER . '/'; public const FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER = '[A-Z]{2}(\d){8}'; - public string $rackId; + public string $rackID; /** @var Microplate */ private Microplate $microplate; - public function __construct(string $rackId) + public function __construct(string $rackID) { - if (\Safe\preg_match(self::FLUIDX_BARCODE_REGEX, $rackId) === 0) { - throw new InvalidRackIdException($rackId); + if (\Safe\preg_match(self::FLUIDX_BARCODE_REGEX, $rackID) === 0) { + throw new InvalidRackIDException($rackID); } - $this->rackId = $rackId; + $this->rackID = $rackID; $this->microplate = new Microplate(self::coordinateSystem()); } diff --git a/src/FluidXPlate/InvalidRackIDException.php b/src/FluidXPlate/InvalidRackIDException.php new file mode 100644 index 0000000..5434799 --- /dev/null +++ b/src/FluidXPlate/InvalidRackIDException.php @@ -0,0 +1,11 @@ +rack = $rack; } - public function tubeId(): string + public function tubeID(): string { return $this->barcode; } @@ -36,7 +36,7 @@ public function rackType(): string return $this->rack->type(); } - public function rackId(): ?string + public function rackID(): ?string { return null; } @@ -47,10 +47,10 @@ public function toString(): string ';', [ $this->rackName(), - $this->rackId(), + $this->rackID(), $this->rackType(), $this->position(), - $this->tubeId(), + $this->tubeID(), ] ); } diff --git a/src/Tecan/Location/Location.php b/src/Tecan/Location/Location.php index 831915c..5cc64dd 100644 --- a/src/Tecan/Location/Location.php +++ b/src/Tecan/Location/Location.php @@ -4,7 +4,7 @@ interface Location { - public function tubeId(): ?string; + public function tubeID(): ?string; public function position(): ?string; @@ -12,7 +12,7 @@ public function rackName(): ?string; public function rackType(): string; - public function rackId(): ?string; + public function rackID(): ?string; /** Serializes the location parameters as part of a pipetting instruction according the gwl file format. */ public function toString(): string; diff --git a/src/Tecan/Location/PositionLocation.php b/src/Tecan/Location/PositionLocation.php index 4e6b53c..13b0b4b 100644 --- a/src/Tecan/Location/PositionLocation.php +++ b/src/Tecan/Location/PositionLocation.php @@ -16,7 +16,7 @@ public function __construct(int $position, Rack $rack) $this->rack = $rack; } - public function tubeId(): ?string + public function tubeID(): ?string { return null; } @@ -36,7 +36,7 @@ public function rackType(): string return $this->rack->type(); } - public function rackId(): ?string + public function rackID(): ?string { return null; } @@ -47,10 +47,10 @@ public function toString(): string ';', [ $this->rackName(), - $this->rackId(), + $this->rackID(), $this->rackType(), $this->position(), - $this->tubeId(), + $this->tubeID(), ] ); } diff --git a/src/TecanScanner/NoRackIDException.php b/src/TecanScanner/NoRackIDException.php new file mode 100644 index 0000000..a77393c --- /dev/null +++ b/src/TecanScanner/NoRackIDException.php @@ -0,0 +1,11 @@ +shift(); + $firstLineWithRackID = $lines->shift(); - if (! is_string($firstLineWithRackId) || ! Str::startsWith($firstLineWithRackId, self::RACKID_IDENTIFIER)) { - throw new NoRackIdException(); + if (! is_string($firstLineWithRackID) || ! Str::startsWith($firstLineWithRackID, self::RACKID_IDENTIFIER)) { + throw new NoRackIDException(); } - $rackId = Str::substr($firstLineWithRackId, strlen(self::RACKID_IDENTIFIER)); + $rackID = Str::substr($firstLineWithRackID, strlen(self::RACKID_IDENTIFIER)); $expectedCount = FluidXPlate::coordinateSystem()->positionsCount(); $actualCount = $lines->count(); @@ -37,7 +37,7 @@ public static function parseRawContent(string $rawContent): FluidXPlate throw new WrongNumberOfWells($expectedCount, $actualCount); } - $plate = new FluidXPlate($rackId); + $plate = new FluidXPlate($rackID); foreach ($lines as $line) { $barcode = Str::after($line, ','); diff --git a/tests/FluidXPlate/FluidXPlateTest.php b/tests/FluidXPlate/FluidXPlateTest.php index dbce26b..dda0886 100644 --- a/tests/FluidXPlate/FluidXPlateTest.php +++ b/tests/FluidXPlate/FluidXPlateTest.php @@ -3,7 +3,7 @@ namespace MLL\Utils\Tests\FluidXPlate; use MLL\Utils\FluidXPlate\FluidXPlate; -use MLL\Utils\FluidXPlate\InvalidRackIdException; +use MLL\Utils\FluidXPlate\InvalidRackIDException; use MLL\Utils\FluidXPlate\InvalidTubeBarcodeException; use MLL\Utils\Microplate\Coordinates; use MLL\Utils\Microplate\CoordinateSystem96Well; @@ -14,23 +14,23 @@ final class FluidXPlateTest extends TestCase { public function testCreateFromStringEmpty(): void { - $rackId = ''; - $this->expectExceptionObject(new InvalidRackIdException($rackId)); - new FluidXPlate($rackId); + $rackID = ''; + $this->expectExceptionObject(new InvalidRackIDException($rackID)); + new FluidXPlate($rackID); } public function testCreateWithRandomNameAndReturnsIt(): void { - $rackId = 'testInvalidRackId'; - $this->expectExceptionObject(new InvalidRackIdException($rackId)); - new FluidXPlate($rackId); + $rackID = 'testInvalidRackID'; + $this->expectExceptionObject(new InvalidRackIDException($rackID)); + new FluidXPlate($rackID); } public function testCreatesSuccessfulWithValidBarCode(): void { - $rackId = 'AB12345678'; - $fluidXPlate = new FluidXPlate($rackId); - self::assertSame($rackId, $fluidXPlate->rackId); + $rackID = 'AB12345678'; + $fluidXPlate = new FluidXPlate($rackID); + self::assertSame($rackID, $fluidXPlate->rackID); self::assertCount(96, $fluidXPlate->wells()); self::assertCount(96, $fluidXPlate->freeWells()); self::assertCount(0, $fluidXPlate->filledWells()); @@ -39,8 +39,8 @@ public function testCreatesSuccessfulWithValidBarCode(): void public function testCanNotAddInvalidBarcode(): void { $barcode = 'testWrongBarcode'; - $rackId = 'AB12345678'; - $fluidXPlate = new FluidXPlate($rackId); + $rackID = 'AB12345678'; + $fluidXPlate = new FluidXPlate($rackID); $coordinates = Coordinates::fromString('A1', new CoordinateSystem96Well()); $this->expectExceptionObject(new InvalidTubeBarcodeException($barcode)); @@ -49,8 +49,8 @@ public function testCanNotAddInvalidBarcode(): void public function testCanOnlyAddStringAsBarcode(): void { - $rackId = 'AB12345678'; - $fluidXPlate = new FluidXPlate($rackId); + $rackID = 'AB12345678'; + $fluidXPlate = new FluidXPlate($rackID); $coordinates = Coordinates::fromString('A1', new CoordinateSystem96Well()); $this->expectException(\TypeError::class); @@ -60,8 +60,8 @@ public function testCanOnlyAddStringAsBarcode(): void public function testCanAddToNextFreeWell(): void { - $rackId = 'AB12345678'; - $fluidXPlate = new FluidXPlate($rackId); + $rackID = 'AB12345678'; + $fluidXPlate = new FluidXPlate($rackID); $expectedCoordinates = Coordinates::fromString('A1', new CoordinateSystem96Well()); $addToNextFreeWell = $fluidXPlate->addToNextFreeWell('test', FlowDirection::COLUMN()); self::assertEquals($expectedCoordinates, $addToNextFreeWell); diff --git a/tests/FluidXPlate/FluidXScannerTest.php b/tests/FluidXPlate/FluidXScannerTest.php index e6cb425..b4a5b25 100644 --- a/tests/FluidXPlate/FluidXScannerTest.php +++ b/tests/FluidXPlate/FluidXScannerTest.php @@ -12,7 +12,7 @@ public function testCreateFromStringEmpty(): void $fluidXScanner = new FluidXScanner(); $fluidXPlate = $fluidXScanner->scanPlate(FluidXScanner::LOCALHOST); - self::assertSame('SA00826894', $fluidXPlate->rackId); + self::assertSame('SA00826894', $fluidXPlate->rackID); $filledWells = $fluidXPlate->filledWells(); self::assertCount(3, $filledWells); self::assertSame('FD20024619', $filledWells->get('A1')); diff --git a/tests/StringUtilTest.php b/tests/StringUtilTest.php index fa88e78..9a1bd9c 100644 --- a/tests/StringUtilTest.php +++ b/tests/StringUtilTest.php @@ -110,7 +110,7 @@ public function testUTF16LE(): void public function testWindows1252(): void { $expectedUTF8 = <<location->tubeId()); + self::assertSame($barcode, $aspirate->location->tubeID()); self::assertNull($aspirate->location->position()); self::assertSame('A;;;TestRackType;;barcode;100;TestLiquidClassName;;', $aspirate->toString()); } @@ -27,7 +27,7 @@ public function testAspirateWithPositionLocation(): void $position = 7; $volume = 2.2; $aspirate = new Aspirate($volume, new PositionLocation($position, MllLabWareRack::DEST_PCR()), MllLiquidClass::TRANSFER_TEMPLATE()); - self::assertNull($aspirate->location->tubeId()); + self::assertNull($aspirate->location->tubeID()); self::assertSame((string) $position, $aspirate->location->position()); self::assertSame('A;DestPCR;;96 Well PCR ABI semi-skirted;' . $position . ';;2.2;Transfer_Template;;', $aspirate->toString()); } diff --git a/tests/Tecan/BasicCommands/DispenseTest.php b/tests/Tecan/BasicCommands/DispenseTest.php index a627269..2a6b35e 100644 --- a/tests/Tecan/BasicCommands/DispenseTest.php +++ b/tests/Tecan/BasicCommands/DispenseTest.php @@ -17,7 +17,7 @@ public function testDispenseWithBarcodeLocation(): void { $barcode = 'barcode'; $aspirate = new Dispense(100, new BarcodeLocation($barcode, new CustomRack('TestRackName', 'TestRackType')), new CustomLiquidClass('TestLiquidClassName')); - self::assertSame($barcode, $aspirate->location->tubeId()); + self::assertSame($barcode, $aspirate->location->tubeID()); self::assertNull($aspirate->location->position()); self::assertSame('D;;;TestRackType;;barcode;100;TestLiquidClassName;;', $aspirate->toString()); } @@ -27,7 +27,7 @@ public function testDispenseWithPositionLocation(): void $position = 7; $volume = 2.2; $aspirate = new Dispense($volume, new PositionLocation($position, MllLabWareRack::DEST_LC()), MllLiquidClass::TRANSFER_TEMPLATE()); - self::assertNull($aspirate->location->tubeId()); + self::assertNull($aspirate->location->tubeID()); self::assertSame((string) $position, $aspirate->location->position()); self::assertSame("D;DestLC;;96 Well MP LightCycler480;{$position};;{$volume};Transfer_Template;;", $aspirate->toString()); } diff --git a/tests/TecanScanner/TecanScannerTest.php b/tests/TecanScanner/TecanScannerTest.php index f6532f9..0979e5f 100644 --- a/tests/TecanScanner/TecanScannerTest.php +++ b/tests/TecanScanner/TecanScannerTest.php @@ -3,7 +3,7 @@ namespace MLL\Utils\Tests\TecanScanner; use MLL\Utils\Microplate\Exceptions\WellNotEmptyException; -use MLL\Utils\TecanScanner\NoRackIdException; +use MLL\Utils\TecanScanner\NoRackIDException; use MLL\Utils\TecanScanner\TecanScanEmptyException; use MLL\Utils\TecanScanner\TecanScanner; use MLL\Utils\TecanScanner\WrongNumberOfWells; @@ -43,7 +43,7 @@ public function testNoBarcode(): void $rawContent = "A1,FD13945423\nB1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"; self::assertFalse(TecanScanner::isValidRawContent($rawContent)); - $this->expectExceptionObject(new NoRackIdException()); + $this->expectExceptionObject(new NoRackIDException()); TecanScanner::parseRawContent($rawContent); } @@ -54,7 +54,7 @@ public function testSuccess(): void $fluidXPlate = TecanScanner::parseRawContent($rawContent); self::assertCount(96, $fluidXPlate->wells()); self::assertCount(94, $fluidXPlate->freeWells()); - self::assertSame('SA00411242', $fluidXPlate->rackId); + self::assertSame('SA00411242', $fluidXPlate->rackID); self::assertSame([ 'A1' => 'FD13945423', From 88525a42356d41cd0b2caff27282b222e844d8da Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 15:57:00 +0200 Subject: [PATCH 08/16] MLL --- src/FluidXPlate/InvalidRackIdException.php | 11 -- .../CustomCommands/MLLReagentDistribution.php | 61 +++++++++ src/Tecan/LiquidClass/MLLLiquidClass.php | 49 ++++++++ src/Tecan/Rack/MLLLabWareRack.php | 116 ++++++++++++++++++ src/TecanScanner/NoRackIdException.php | 11 -- tests/Tecan/BasicCommands/AspirateTest.php | 6 +- tests/Tecan/BasicCommands/DispenseTest.php | 6 +- .../BasicCommands/ReagentDistributionTest.php | 10 +- .../MLLReagentDistributionTest.php | 41 +++++++ .../Tecan/LiquidClass/MLLLiquidClassTest.php | 18 +++ tests/Tecan/Rack/MLLLabWareRackTest.php | 35 ++++++ tests/Tecan/TecanProtocolTest.php | 24 ++-- 12 files changed, 343 insertions(+), 45 deletions(-) delete mode 100644 src/FluidXPlate/InvalidRackIdException.php create mode 100644 src/Tecan/CustomCommands/MLLReagentDistribution.php create mode 100644 src/Tecan/LiquidClass/MLLLiquidClass.php create mode 100644 src/Tecan/Rack/MLLLabWareRack.php delete mode 100644 src/TecanScanner/NoRackIdException.php create mode 100644 tests/Tecan/CustomCommands/MLLReagentDistributionTest.php create mode 100644 tests/Tecan/LiquidClass/MLLLiquidClassTest.php create mode 100644 tests/Tecan/Rack/MLLLabWareRackTest.php diff --git a/src/FluidXPlate/InvalidRackIdException.php b/src/FluidXPlate/InvalidRackIdException.php deleted file mode 100644 index 245de53..0000000 --- a/src/FluidXPlate/InvalidRackIdException.php +++ /dev/null @@ -1,11 +0,0 @@ -source = $source; + $this->target = $target; + $this->volume = $dispenseVolume; + $this->liquidClass = $liquidClass; + } + + public function toString(): string + { + $reagentDistribution = new ReagentDistribution( + $this->source->formatToAspirateAndDispenseParameters(), + $this->target->formatToAspirateAndDispenseParameters(), + $this->volume, + $this->liquidClass, + self::NUMBER_OF_DITI_REUSES, + self::NUMBER_OF_MULTI_DISP, + ReagentDistributionDirection::LEFT_TO_RIGHT(), + $this->excludedWells(), + ); + + return $reagentDistribution->toString(); + } + + /** @return array */ + private function excludedWells(): array + { + $min = min($this->target->dispensePositions); + $max = max($this->target->dispensePositions); + + $allWellsFromStartToEnd = range($min, $max); + + return array_diff($allWellsFromStartToEnd, $this->target->dispensePositions); + } +} diff --git a/src/Tecan/LiquidClass/MLLLiquidClass.php b/src/Tecan/LiquidClass/MLLLiquidClass.php new file mode 100644 index 0000000..78693f3 --- /dev/null +++ b/src/Tecan/LiquidClass/MLLLiquidClass.php @@ -0,0 +1,49 @@ +value = $value; + } + + public static function DNA_DILUTION(): self + { + return new self(self::DNA_DILUTION); + } + + public static function DNA_DILUTION_WATER(): self + { + return new self(self::DNA_DILUTION_WATER); + } + + public static function TRANSFER_PCR_PRODUKT(): self + { + return new self(self::TRANSFER_PCR_PRODUKT); + } + + public static function TRANSFER_MASTERMIX_MP(): self + { + return new self(self::TRANSFER_MASTERMIX_MP); + } + + public static function TRANSFER_TEMPLATE(): self + { + return new self(self::TRANSFER_TEMPLATE); + } + + public function name(): string + { + return $this->value; + } +} diff --git a/src/Tecan/Rack/MLLLabWareRack.php b/src/Tecan/Rack/MLLLabWareRack.php new file mode 100644 index 0000000..b435642 --- /dev/null +++ b/src/Tecan/Rack/MLLLabWareRack.php @@ -0,0 +1,116 @@ +value = $value; + } + + public static function A(): self + { + return new self(self::A); + } + + public static function MP_CDNA(): self + { + return new self(self::MP_CDNA); + } + + public static function MP_SAMPLE(): self + { + return new self(self::MP_SAMPLE); + } + + public static function MP_WATER(): self + { + return new self(self::MP_WATER); + } + + public static function FLUID_X(): self + { + return new self(self::FLUID_X); + } + + public static function MM(): self + { + return new self(self::MM); + } + + public static function DEST_LC(): self + { + return new self(self::DEST_LC); + } + + public static function DEST_PCR(): self + { + return new self(self::DEST_PCR); + } + + public static function DEST_TAQMAN(): self + { + return new self(self::DEST_TAQMAN); + } + + public function id(): ?string + { + return null; + } + + public function name(): string + { + return $this->value; + } + + public function type(): string + { + switch ($this->value) { + case self::A: + return 'Eppis 24x0.5 ml Cooled'; + case self::MP_CDNA: + return 'MP cDNA'; + case self::MP_SAMPLE: + return 'MP Microplate'; + case self::MP_WATER: + return 'Trough 300ml MCA Portrait'; + case self::FLUID_X: + return '96FluidX'; + case self::MM: + return 'Eppis 32x1.5 ml Cooled'; + case self::DEST_LC: + return '96 Well MP LightCycler480'; + case self::DEST_PCR: + return '96 Well PCR ABI semi-skirted'; + case self::DEST_TAQMAN: + return '96 Well PCR TaqMan'; + default: + throw new \Exception('Type not defined for ' . $this->value); + } + } + + public function toString(): string + { + return implode( + ';', + [ + $this->name(), + $this->id(), + $this->type(), + ] + ); + } +} diff --git a/src/TecanScanner/NoRackIdException.php b/src/TecanScanner/NoRackIdException.php deleted file mode 100644 index ea9fce3..0000000 --- a/src/TecanScanner/NoRackIdException.php +++ /dev/null @@ -1,11 +0,0 @@ -location->tubeID()); self::assertSame((string) $position, $aspirate->location->position()); self::assertSame('A;DestPCR;;96 Well PCR ABI semi-skirted;' . $position . ';;2.2;Transfer_Template;;', $aspirate->toString()); diff --git a/tests/Tecan/BasicCommands/DispenseTest.php b/tests/Tecan/BasicCommands/DispenseTest.php index 2a6b35e..64915c3 100644 --- a/tests/Tecan/BasicCommands/DispenseTest.php +++ b/tests/Tecan/BasicCommands/DispenseTest.php @@ -4,11 +4,11 @@ use MLL\Utils\Tecan\BasicCommands\Dispense; use MLL\Utils\Tecan\LiquidClass\CustomLiquidClass; -use MLL\Utils\Tecan\LiquidClass\MllLiquidClass; +use MLL\Utils\Tecan\LiquidClass\MLLLiquidClass; use MLL\Utils\Tecan\Location\BarcodeLocation; use MLL\Utils\Tecan\Location\PositionLocation; use MLL\Utils\Tecan\Rack\CustomRack; -use MLL\Utils\Tecan\Rack\MllLabWareRack; +use MLL\Utils\Tecan\Rack\MLLLabWareRack; use PHPUnit\Framework\TestCase; final class DispenseTest extends TestCase @@ -26,7 +26,7 @@ public function testDispenseWithPositionLocation(): void { $position = 7; $volume = 2.2; - $aspirate = new Dispense($volume, new PositionLocation($position, MllLabWareRack::DEST_LC()), MllLiquidClass::TRANSFER_TEMPLATE()); + $aspirate = new Dispense($volume, new PositionLocation($position, MLLLabWareRack::DEST_LC()), MLLLiquidClass::TRANSFER_TEMPLATE()); self::assertNull($aspirate->location->tubeID()); self::assertSame((string) $position, $aspirate->location->position()); self::assertSame("D;DestLC;;96 Well MP LightCycler480;{$position};;{$volume};Transfer_Template;;", $aspirate->toString()); diff --git a/tests/Tecan/BasicCommands/ReagentDistributionTest.php b/tests/Tecan/BasicCommands/ReagentDistributionTest.php index cc9d547..a01ff5e 100644 --- a/tests/Tecan/BasicCommands/ReagentDistributionTest.php +++ b/tests/Tecan/BasicCommands/ReagentDistributionTest.php @@ -4,8 +4,8 @@ use MLL\Utils\Tecan\BasicCommands\AspirateAndDispenseParameters; use MLL\Utils\Tecan\BasicCommands\ReagentDistribution; -use MLL\Utils\Tecan\LiquidClass\MllLiquidClass; -use MLL\Utils\Tecan\Rack\MllLabWareRack; +use MLL\Utils\Tecan\LiquidClass\MLLLiquidClass; +use MLL\Utils\Tecan\Rack\MLLLabWareRack; use MLL\Utils\Tecan\ReagentDistribution\ReagentDistributionDirection; use PHPUnit\Framework\TestCase; @@ -15,16 +15,16 @@ public function testFormatToString(): void { $sourceStartPosition = 13; $sourceEndPosition = 13; - $sourceRack = MllLabWareRack::MM(); + $sourceRack = MLLLabWareRack::MM(); $source = new AspirateAndDispenseParameters($sourceRack, $sourceStartPosition, $sourceEndPosition); $targetStartPosition = 48; $targetEndPosition = 73; - $targetRack = MllLabWareRack::DEST_PCR(); + $targetRack = MLLLabWareRack::DEST_PCR(); $target = new AspirateAndDispenseParameters($targetRack, $targetStartPosition, $targetEndPosition); $dispenseVolume = 24; - $liquidClass = MllLiquidClass::TRANSFER_MASTERMIX_MP(); + $liquidClass = MLLLiquidClass::TRANSFER_MASTERMIX_MP(); $numberOfDitiReuses = 6; $numberOfMultiDisp = 1; diff --git a/tests/Tecan/CustomCommands/MLLReagentDistributionTest.php b/tests/Tecan/CustomCommands/MLLReagentDistributionTest.php new file mode 100644 index 0000000..92c79b0 --- /dev/null +++ b/tests/Tecan/CustomCommands/MLLReagentDistributionTest.php @@ -0,0 +1,41 @@ +name()};;{$sourceRack->type()};{$sourceStartPosition};{$sourceStartPosition};{$targetRack->name()};;{$targetRack->type()};48;73;{$dispenseVolume};{$liquidClass->name()};{$numberOfDitiReuses};{$numberOfMultiDisp};0;50;51;52;53;54;55;56;57;58;59;60;61;62;63;64;65;66;67;68;69;70;71", + $command->toString() + ); + } +} diff --git a/tests/Tecan/LiquidClass/MLLLiquidClassTest.php b/tests/Tecan/LiquidClass/MLLLiquidClassTest.php new file mode 100644 index 0000000..73d59a1 --- /dev/null +++ b/tests/Tecan/LiquidClass/MLLLiquidClassTest.php @@ -0,0 +1,18 @@ +name()); + self::assertSame('DNA_Dilution_Water', MLLLiquidClass::DNA_DILUTION_WATER()->name()); + self::assertSame('Transfer_PCR_Produkt', MLLLiquidClass::TRANSFER_PCR_PRODUKT()->name()); + self::assertSame('Transfer_Mastermix_MP', MLLLiquidClass::TRANSFER_MASTERMIX_MP()->name()); + self::assertSame('Transfer_Template', MLLLiquidClass::TRANSFER_TEMPLATE()->name()); + } +} diff --git a/tests/Tecan/Rack/MLLLabWareRackTest.php b/tests/Tecan/Rack/MLLLabWareRackTest.php new file mode 100644 index 0000000..0d0bb59 --- /dev/null +++ b/tests/Tecan/Rack/MLLLabWareRackTest.php @@ -0,0 +1,35 @@ +name()); + self::assertSame('MPCDNA', MLLLabWareRack::MP_CDNA()->name()); + self::assertSame('MPSample', MLLLabWareRack::MP_SAMPLE()->name()); + self::assertSame('MPWasser', MLLLabWareRack::MP_WATER()->name()); + self::assertSame('FluidX', MLLLabWareRack::FLUID_X()->name()); + self::assertSame('MM', MLLLabWareRack::MM()->name()); + self::assertSame('DestLC', MLLLabWareRack::DEST_LC()->name()); + self::assertSame('DestPCR', MLLLabWareRack::DEST_PCR()->name()); + self::assertSame('DestTaqMan', MLLLabWareRack::DEST_TAQMAN()->name()); + } + + public function testValueOfEnum(): void + { + self::assertSame('Eppis 24x0.5 ml Cooled', MLLLabWareRack::A()->type()); + self::assertSame('MP cDNA', MLLLabWareRack::MP_CDNA()->type()); + self::assertSame('MP Microplate', MLLLabWareRack::MP_SAMPLE()->type()); + self::assertSame('Trough 300ml MCA Portrait', MLLLabWareRack::MP_WATER()->type()); + self::assertSame('96FluidX', MLLLabWareRack::FLUID_X()->type()); + self::assertSame('Eppis 32x1.5 ml Cooled', MLLLabWareRack::MM()->type()); + self::assertSame('96 Well MP LightCycler480', MLLLabWareRack::DEST_LC()->type()); + self::assertSame('96 Well PCR ABI semi-skirted', MLLLabWareRack::DEST_PCR()->type()); + self::assertSame('96 Well PCR TaqMan', MLLLabWareRack::DEST_TAQMAN()->type()); + } +} diff --git a/tests/Tecan/TecanProtocolTest.php b/tests/Tecan/TecanProtocolTest.php index 30b6fce..1da90e3 100644 --- a/tests/Tecan/TecanProtocolTest.php +++ b/tests/Tecan/TecanProtocolTest.php @@ -12,13 +12,13 @@ use MLL\Utils\Tecan\BasicCommands\Wash; use MLL\Utils\Tecan\CustomCommands\AspirateParameters; use MLL\Utils\Tecan\CustomCommands\DispenseParameters; -use MLL\Utils\Tecan\CustomCommands\MllReagentDistribution; +use MLL\Utils\Tecan\CustomCommands\MLLReagentDistribution; use MLL\Utils\Tecan\CustomCommands\TransferWithAutoWash; use MLL\Utils\Tecan\LiquidClass\CustomLiquidClass; -use MLL\Utils\Tecan\LiquidClass\MllLiquidClass; +use MLL\Utils\Tecan\LiquidClass\MLLLiquidClass; use MLL\Utils\Tecan\Location\BarcodeLocation; use MLL\Utils\Tecan\Rack\CustomRack; -use MLL\Utils\Tecan\Rack\MllLabWareRack; +use MLL\Utils\Tecan\Rack\MLLLabWareRack; use MLL\Utils\Tecan\TecanProtocol; use MLL\Utils\Tecan\TipMask\TipMask; use PHPUnit\Framework\TestCase; @@ -212,16 +212,16 @@ public function testProtocolWithForEightTips(): void public function testReagentDistributionProtocol(): void { - $sourceRack = MllLabWareRack::MM(); - $targetRack = MllLabWareRack::DEST_PCR(); + $sourceRack = MLLLabWareRack::MM(); + $targetRack = MLLLabWareRack::DEST_PCR(); $dispenseVolume = 24; - $liquidClass = MllLiquidClass::TRANSFER_MASTERMIX_MP(); + $liquidClass = MLLLiquidClass::TRANSFER_MASTERMIX_MP(); $tecanProtocol = new TecanProtocol(TipMask::FOUR_TIPS()); $dispensePositions = [1, 2, 3, 4, 5, 57]; $tecanProtocol->addCommand( - new MllReagentDistribution( + new MLLReagentDistribution( new AspirateParameters($sourceRack, 1), new DispenseParameters($targetRack, $dispensePositions), $dispenseVolume, @@ -231,7 +231,7 @@ public function testReagentDistributionProtocol(): void $dispensePositions1 = [6, 7, 50, 58, 74, 75]; $tecanProtocol->addCommand( - new MllReagentDistribution( + new MLLReagentDistribution( new AspirateParameters($sourceRack, 2), new DispenseParameters($targetRack, $dispensePositions1), $dispenseVolume, @@ -241,7 +241,7 @@ public function testReagentDistributionProtocol(): void $dispensePositions2 = [8, 10, 51, 59]; $tecanProtocol->addCommand( - new MllReagentDistribution( + new MLLReagentDistribution( new AspirateParameters($sourceRack, 3), new DispenseParameters($targetRack, $dispensePositions2), $dispenseVolume, @@ -251,7 +251,7 @@ public function testReagentDistributionProtocol(): void $dispensePositions3 = [11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 52, 60]; $tecanProtocol->addCommand( - new MllReagentDistribution( + new MLLReagentDistribution( new AspirateParameters($sourceRack, 4), new DispenseParameters($targetRack, $dispensePositions3), $dispenseVolume, @@ -261,7 +261,7 @@ public function testReagentDistributionProtocol(): void $dispensePositions4 = [24, 25, 26, 27, 28, 29, 30, 53, 61]; $tecanProtocol->addCommand( - new MllReagentDistribution( + new MLLReagentDistribution( new AspirateParameters($sourceRack, 5), new DispenseParameters($targetRack, $dispensePositions4), $dispenseVolume, @@ -270,7 +270,7 @@ public function testReagentDistributionProtocol(): void ); $dispensePositions5 = [1, 2, 3, 4, 5]; $tecanProtocol->addCommand( - new MllReagentDistribution( + new MLLReagentDistribution( new AspirateParameters($sourceRack, 5), new DispenseParameters($targetRack, $dispensePositions5), $dispenseVolume, From a1a76a77d297fbe66007fafc337309bd8e767fca Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 15:57:10 +0200 Subject: [PATCH 09/16] MLL --- .../CustomCommands/MllReagentDistribution.php | 61 --------- src/Tecan/LiquidClass/MllLiquidClass.php | 49 -------- src/Tecan/Rack/MllLabWareRack.php | 116 ------------------ .../MllReagentDistributionTest.php | 41 ------- .../Tecan/LiquidClass/MllLiquidClassTest.php | 18 --- tests/Tecan/Rack/MllLabWareRackTest.php | 35 ------ 6 files changed, 320 deletions(-) delete mode 100644 src/Tecan/CustomCommands/MllReagentDistribution.php delete mode 100644 src/Tecan/LiquidClass/MllLiquidClass.php delete mode 100644 src/Tecan/Rack/MllLabWareRack.php delete mode 100644 tests/Tecan/CustomCommands/MllReagentDistributionTest.php delete mode 100644 tests/Tecan/LiquidClass/MllLiquidClassTest.php delete mode 100644 tests/Tecan/Rack/MllLabWareRackTest.php diff --git a/src/Tecan/CustomCommands/MllReagentDistribution.php b/src/Tecan/CustomCommands/MllReagentDistribution.php deleted file mode 100644 index 63c516a..0000000 --- a/src/Tecan/CustomCommands/MllReagentDistribution.php +++ /dev/null @@ -1,61 +0,0 @@ -source = $source; - $this->target = $target; - $this->volume = $dispenseVolume; - $this->liquidClass = $liquidClass; - } - - public function toString(): string - { - $reagentDistribution = new ReagentDistribution( - $this->source->formatToAspirateAndDispenseParameters(), - $this->target->formatToAspirateAndDispenseParameters(), - $this->volume, - $this->liquidClass, - self::NUMBER_OF_DITI_REUSES, - self::NUMBER_OF_MULTI_DISP, - ReagentDistributionDirection::LEFT_TO_RIGHT(), - $this->excludedWells(), - ); - - return $reagentDistribution->toString(); - } - - /** @return array */ - private function excludedWells(): array - { - $min = min($this->target->dispensePositions); - $max = max($this->target->dispensePositions); - - $allWellsFromStartToEnd = range($min, $max); - - return array_diff($allWellsFromStartToEnd, $this->target->dispensePositions); - } -} diff --git a/src/Tecan/LiquidClass/MllLiquidClass.php b/src/Tecan/LiquidClass/MllLiquidClass.php deleted file mode 100644 index 6763e9e..0000000 --- a/src/Tecan/LiquidClass/MllLiquidClass.php +++ /dev/null @@ -1,49 +0,0 @@ -value = $value; - } - - public static function DNA_DILUTION(): self - { - return new self(self::DNA_DILUTION); - } - - public static function DNA_DILUTION_WATER(): self - { - return new self(self::DNA_DILUTION_WATER); - } - - public static function TRANSFER_PCR_PRODUKT(): self - { - return new self(self::TRANSFER_PCR_PRODUKT); - } - - public static function TRANSFER_MASTERMIX_MP(): self - { - return new self(self::TRANSFER_MASTERMIX_MP); - } - - public static function TRANSFER_TEMPLATE(): self - { - return new self(self::TRANSFER_TEMPLATE); - } - - public function name(): string - { - return $this->value; - } -} diff --git a/src/Tecan/Rack/MllLabWareRack.php b/src/Tecan/Rack/MllLabWareRack.php deleted file mode 100644 index 1c4591c..0000000 --- a/src/Tecan/Rack/MllLabWareRack.php +++ /dev/null @@ -1,116 +0,0 @@ -value = $value; - } - - public static function A(): self - { - return new self(self::A); - } - - public static function MP_CDNA(): self - { - return new self(self::MP_CDNA); - } - - public static function MP_SAMPLE(): self - { - return new self(self::MP_SAMPLE); - } - - public static function MP_WATER(): self - { - return new self(self::MP_WATER); - } - - public static function FLUID_X(): self - { - return new self(self::FLUID_X); - } - - public static function MM(): self - { - return new self(self::MM); - } - - public static function DEST_LC(): self - { - return new self(self::DEST_LC); - } - - public static function DEST_PCR(): self - { - return new self(self::DEST_PCR); - } - - public static function DEST_TAQMAN(): self - { - return new self(self::DEST_TAQMAN); - } - - public function id(): ?string - { - return null; - } - - public function name(): string - { - return $this->value; - } - - public function type(): string - { - switch ($this->value) { - case self::A: - return 'Eppis 24x0.5 ml Cooled'; - case self::MP_CDNA: - return 'MP cDNA'; - case self::MP_SAMPLE: - return 'MP Microplate'; - case self::MP_WATER: - return 'Trough 300ml MCA Portrait'; - case self::FLUID_X: - return '96FluidX'; - case self::MM: - return 'Eppis 32x1.5 ml Cooled'; - case self::DEST_LC: - return '96 Well MP LightCycler480'; - case self::DEST_PCR: - return '96 Well PCR ABI semi-skirted'; - case self::DEST_TAQMAN: - return '96 Well PCR TaqMan'; - default: - throw new \Exception('Type not defined for ' . $this->value); - } - } - - public function toString(): string - { - return implode( - ';', - [ - $this->name(), - $this->id(), - $this->type(), - ] - ); - } -} diff --git a/tests/Tecan/CustomCommands/MllReagentDistributionTest.php b/tests/Tecan/CustomCommands/MllReagentDistributionTest.php deleted file mode 100644 index 2fd753a..0000000 --- a/tests/Tecan/CustomCommands/MllReagentDistributionTest.php +++ /dev/null @@ -1,41 +0,0 @@ -name()};;{$sourceRack->type()};{$sourceStartPosition};{$sourceStartPosition};{$targetRack->name()};;{$targetRack->type()};48;73;{$dispenseVolume};{$liquidClass->name()};{$numberOfDitiReuses};{$numberOfMultiDisp};0;50;51;52;53;54;55;56;57;58;59;60;61;62;63;64;65;66;67;68;69;70;71", - $command->toString() - ); - } -} diff --git a/tests/Tecan/LiquidClass/MllLiquidClassTest.php b/tests/Tecan/LiquidClass/MllLiquidClassTest.php deleted file mode 100644 index 2cb6c2e..0000000 --- a/tests/Tecan/LiquidClass/MllLiquidClassTest.php +++ /dev/null @@ -1,18 +0,0 @@ -name()); - self::assertSame('DNA_Dilution_Water', MllLiquidClass::DNA_DILUTION_WATER()->name()); - self::assertSame('Transfer_PCR_Produkt', MllLiquidClass::TRANSFER_PCR_PRODUKT()->name()); - self::assertSame('Transfer_Mastermix_MP', MllLiquidClass::TRANSFER_MASTERMIX_MP()->name()); - self::assertSame('Transfer_Template', MllLiquidClass::TRANSFER_TEMPLATE()->name()); - } -} diff --git a/tests/Tecan/Rack/MllLabWareRackTest.php b/tests/Tecan/Rack/MllLabWareRackTest.php deleted file mode 100644 index eeacf4d..0000000 --- a/tests/Tecan/Rack/MllLabWareRackTest.php +++ /dev/null @@ -1,35 +0,0 @@ -name()); - self::assertSame('MPCDNA', MllLabWareRack::MP_CDNA()->name()); - self::assertSame('MPSample', MllLabWareRack::MP_SAMPLE()->name()); - self::assertSame('MPWasser', MllLabWareRack::MP_WATER()->name()); - self::assertSame('FluidX', MllLabWareRack::FLUID_X()->name()); - self::assertSame('MM', MllLabWareRack::MM()->name()); - self::assertSame('DestLC', MllLabWareRack::DEST_LC()->name()); - self::assertSame('DestPCR', MllLabWareRack::DEST_PCR()->name()); - self::assertSame('DestTaqMan', MllLabWareRack::DEST_TAQMAN()->name()); - } - - public function testValueOfEnum(): void - { - self::assertSame('Eppis 24x0.5 ml Cooled', MllLabWareRack::A()->type()); - self::assertSame('MP cDNA', MllLabWareRack::MP_CDNA()->type()); - self::assertSame('MP Microplate', MllLabWareRack::MP_SAMPLE()->type()); - self::assertSame('Trough 300ml MCA Portrait', MllLabWareRack::MP_WATER()->type()); - self::assertSame('96FluidX', MllLabWareRack::FLUID_X()->type()); - self::assertSame('Eppis 32x1.5 ml Cooled', MllLabWareRack::MM()->type()); - self::assertSame('96 Well MP LightCycler480', MllLabWareRack::DEST_LC()->type()); - self::assertSame('96 Well PCR ABI semi-skirted', MllLabWareRack::DEST_PCR()->type()); - self::assertSame('96 Well PCR TaqMan', MllLabWareRack::DEST_TAQMAN()->type()); - } -} From b194187a4297eacf151a61e086189bf74cae12db Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 15:58:20 +0200 Subject: [PATCH 10/16] phpdoc --- src/FluidXPlate/FluidXScanner.php | 4 +--- src/TecanScanner/TecanScanner.php | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/FluidXPlate/FluidXScanner.php b/src/FluidXPlate/FluidXScanner.php index 47037c5..d6087f5 100644 --- a/src/FluidXPlate/FluidXScanner.php +++ b/src/FluidXPlate/FluidXScanner.php @@ -7,9 +7,7 @@ use MLL\Utils\Microplate\CoordinateSystem96Well; use MLL\Utils\StringUtil; -/** - * Communicates with a FluidX scanner device and fetches results from it. - */ +/** Communicates with a FluidX scanner device and fetches results from it. */ class FluidXScanner { private const READING = 'Reading...'; diff --git a/src/TecanScanner/TecanScanner.php b/src/TecanScanner/TecanScanner.php index f443761..923308e 100644 --- a/src/TecanScanner/TecanScanner.php +++ b/src/TecanScanner/TecanScanner.php @@ -8,9 +8,7 @@ use MLL\Utils\Microplate\Coordinates; use MLL\Utils\StringUtil; -/** - * The plate scanner on a tecan worktable. - */ +/** The plate scanner on a tecan worktable. */ final class TecanScanner { public const NO_READ = 'NO READ'; From 4d63fb6b067f282892cc172e73e42d9baf12f291 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 15:59:55 +0200 Subject: [PATCH 11/16] not final --- src/CSVArray.php | 2 +- src/FluidXPlate/FluidXPlate.php | 2 +- src/FluidXPlate/InvalidRackIDException.php | 2 +- src/FluidXPlate/InvalidTubeBarcodeException.php | 2 +- src/FluidXPlate/Scalars/FluidXBarcode.php | 2 +- src/FluidXPlate/Scalars/FrameStarBarcode.php | 2 +- src/FluidXPlate/ScanFluidXPlateException.php | 2 +- src/Microplate/Casts/Coordinates96Well.php | 2 +- src/Microplate/CoordinateSystem12Well.php | 2 +- src/Microplate/CoordinateSystem48Well.php | 2 +- src/Microplate/CoordinateSystem96Well.php | 2 +- src/Microplate/Coordinates.php | 2 +- src/Microplate/Enums/FlowDirection.php | 2 +- src/Microplate/Exceptions/MicroplateIsFullException.php | 2 +- src/Microplate/Exceptions/SectionDoesNotExistException.php | 2 +- src/Microplate/Exceptions/SectionIsFullException.php | 2 +- src/Microplate/Exceptions/UnexpectedFlowDirection.php | 2 +- src/Microplate/Exceptions/WellNotEmptyException.php | 2 +- src/Microplate/FullColumnSection.php | 2 +- src/Microplate/Microplate.php | 2 +- src/Microplate/MicroplateSet/Location.php | 2 +- src/Microplate/MicroplateSet/MicroplateSetAB.php | 2 +- src/Microplate/MicroplateSet/MicroplateSetABCD.php | 2 +- src/Microplate/MicroplateSet/MicroplateSetABCDE.php | 2 +- src/Microplate/Scalars/Column96Well.php | 2 +- src/Microplate/Scalars/Row96Well.php | 2 +- src/Microplate/Section.php | 2 +- src/Microplate/SectionedMicroplate.php | 2 +- src/Microplate/WellWithCoordinates.php | 2 +- src/Number.php | 2 +- src/StringUtil.php | 2 +- src/Tecan/BasicCommands/Aspirate.php | 2 +- src/Tecan/BasicCommands/AspirateAndDispenseParameters.php | 2 +- src/Tecan/BasicCommands/BreakCommand.php | 2 +- src/Tecan/BasicCommands/Comment.php | 2 +- src/Tecan/BasicCommands/Dispense.php | 2 +- src/Tecan/BasicCommands/ReagentDistribution.php | 2 +- src/Tecan/BasicCommands/Wash.php | 2 +- src/Tecan/CustomCommands/AspirateParameters.php | 2 +- src/Tecan/CustomCommands/DispenseParameters.php | 2 +- src/Tecan/CustomCommands/MLLReagentDistribution.php | 2 +- src/Tecan/CustomCommands/TransferWithAutoWash.php | 2 +- src/Tecan/LiquidClass/CustomLiquidClass.php | 2 +- src/Tecan/Location/BarcodeLocation.php | 2 +- src/Tecan/Location/PositionLocation.php | 2 +- src/Tecan/Rack/CustomRack.php | 2 +- src/Tecan/TecanException.php | 2 +- src/Tecan/TecanProtocol.php | 2 +- src/TecanScanner/NoRackIDException.php | 2 +- src/TecanScanner/TecanScanEmptyException.php | 2 +- src/TecanScanner/TecanScanner.php | 2 +- src/TecanScanner/WrongNumberOfWells.php | 2 +- 52 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/CSVArray.php b/src/CSVArray.php index d01410c..61b950e 100644 --- a/src/CSVArray.php +++ b/src/CSVArray.php @@ -5,7 +5,7 @@ use Illuminate\Support\Arr; /** @phpstan-type CSVPrimitive bool|float|int|string|\Stringable|null */ -final class CSVArray +class CSVArray { /** * TODO: fix parsing multiline-content in csv. diff --git a/src/FluidXPlate/FluidXPlate.php b/src/FluidXPlate/FluidXPlate.php index 98370dc..0762f23 100644 --- a/src/FluidXPlate/FluidXPlate.php +++ b/src/FluidXPlate/FluidXPlate.php @@ -8,7 +8,7 @@ use MLL\Utils\Microplate\Enums\FlowDirection; use MLL\Utils\Microplate\Microplate; -final class FluidXPlate +class FluidXPlate { public const FLUIDX_BARCODE_REGEX = /* @lang RegExp */ '/' . self::FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER . '/'; public const FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER = '[A-Z]{2}(\d){8}'; diff --git a/src/FluidXPlate/InvalidRackIDException.php b/src/FluidXPlate/InvalidRackIDException.php index 5434799..4476aa5 100644 --- a/src/FluidXPlate/InvalidRackIDException.php +++ b/src/FluidXPlate/InvalidRackIDException.php @@ -2,7 +2,7 @@ namespace MLL\Utils\FluidXPlate; -final class InvalidRackIDException extends FluidXPlateException +class InvalidRackIDException extends FluidXPlateException { public function __construct(string $rackID) { diff --git a/src/FluidXPlate/InvalidTubeBarcodeException.php b/src/FluidXPlate/InvalidTubeBarcodeException.php index 15e2c05..0d9eef6 100644 --- a/src/FluidXPlate/InvalidTubeBarcodeException.php +++ b/src/FluidXPlate/InvalidTubeBarcodeException.php @@ -2,7 +2,7 @@ namespace MLL\Utils\FluidXPlate; -final class InvalidTubeBarcodeException extends FluidXPlateException +class InvalidTubeBarcodeException extends FluidXPlateException { public function __construct(string $tubeBarcode) { diff --git a/src/FluidXPlate/Scalars/FluidXBarcode.php b/src/FluidXPlate/Scalars/FluidXBarcode.php index ddc44b3..13457c6 100644 --- a/src/FluidXPlate/Scalars/FluidXBarcode.php +++ b/src/FluidXPlate/Scalars/FluidXBarcode.php @@ -5,7 +5,7 @@ use MLL\GraphQLScalars\Regex; use MLL\Utils\FluidXPlate\FluidXPlate; -final class FluidXBarcode extends Regex +class FluidXBarcode extends Regex { public ?string $description = 'A valid barcode for FluidX-Tubes or FluidX-Plates represented as a string, e.g. `XR12345678`.'; diff --git a/src/FluidXPlate/Scalars/FrameStarBarcode.php b/src/FluidXPlate/Scalars/FrameStarBarcode.php index d7cca8a..89bf55e 100644 --- a/src/FluidXPlate/Scalars/FrameStarBarcode.php +++ b/src/FluidXPlate/Scalars/FrameStarBarcode.php @@ -4,7 +4,7 @@ use MLL\GraphQLScalars\Regex; -final class FrameStarBarcode extends Regex +class FrameStarBarcode extends Regex { public const FRAME_STAR_BARCODE_REGEX = /* @lang RegExp */ '/[A-Z]{2}(\d){6}/'; diff --git a/src/FluidXPlate/ScanFluidXPlateException.php b/src/FluidXPlate/ScanFluidXPlateException.php index 05f1eea..a6f9640 100644 --- a/src/FluidXPlate/ScanFluidXPlateException.php +++ b/src/FluidXPlate/ScanFluidXPlateException.php @@ -2,4 +2,4 @@ namespace MLL\Utils\FluidXPlate; -final class ScanFluidXPlateException extends FluidXPlateException {} +class ScanFluidXPlateException extends FluidXPlateException {} diff --git a/src/Microplate/Casts/Coordinates96Well.php b/src/Microplate/Casts/Coordinates96Well.php index 794de72..b43788a 100644 --- a/src/Microplate/Casts/Coordinates96Well.php +++ b/src/Microplate/Casts/Coordinates96Well.php @@ -8,7 +8,7 @@ use MLL\Utils\Microplate\CoordinateSystem96Well; /** @implements CastsAttributes, Coordinates> */ -final class Coordinates96Well implements CastsAttributes +class Coordinates96Well implements CastsAttributes { /** * @param Model $model diff --git a/src/Microplate/CoordinateSystem12Well.php b/src/Microplate/CoordinateSystem12Well.php index 93d2a02..047fe9c 100644 --- a/src/Microplate/CoordinateSystem12Well.php +++ b/src/Microplate/CoordinateSystem12Well.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Microplate; -final class CoordinateSystem12Well extends CoordinateSystem +class CoordinateSystem12Well extends CoordinateSystem { /** Duplicates @see CoordinateSystem::positionsCount() for static contexts. */ public const POSITIONS_COUNT = 12; diff --git a/src/Microplate/CoordinateSystem48Well.php b/src/Microplate/CoordinateSystem48Well.php index c81c9bb..465e1a0 100644 --- a/src/Microplate/CoordinateSystem48Well.php +++ b/src/Microplate/CoordinateSystem48Well.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Microplate; -final class CoordinateSystem48Well extends CoordinateSystem +class CoordinateSystem48Well extends CoordinateSystem { /** Duplicates @see CoordinateSystem::positionsCount() for static contexts. */ public const POSITIONS_COUNT = 48; diff --git a/src/Microplate/CoordinateSystem96Well.php b/src/Microplate/CoordinateSystem96Well.php index 0b18e3a..a3073ef 100644 --- a/src/Microplate/CoordinateSystem96Well.php +++ b/src/Microplate/CoordinateSystem96Well.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Microplate; -final class CoordinateSystem96Well extends CoordinateSystem +class CoordinateSystem96Well extends CoordinateSystem { /** Duplicates @see CoordinateSystem::positionsCount() for static contexts. */ public const POSITIONS_COUNT = 96; diff --git a/src/Microplate/Coordinates.php b/src/Microplate/Coordinates.php index e877b84..aa5d038 100644 --- a/src/Microplate/Coordinates.php +++ b/src/Microplate/Coordinates.php @@ -9,7 +9,7 @@ use function Safe\preg_match; /** @template TCoordinateSystem of CoordinateSystem */ -final class Coordinates +class Coordinates { public const MIN_POSITION = 1; diff --git a/src/Microplate/Enums/FlowDirection.php b/src/Microplate/Enums/FlowDirection.php index 419b166..1a821ef 100644 --- a/src/Microplate/Enums/FlowDirection.php +++ b/src/Microplate/Enums/FlowDirection.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Microplate\Enums; -final class FlowDirection +class FlowDirection { public const ROW = 'ROW'; public const COLUMN = 'COLUMN'; diff --git a/src/Microplate/Exceptions/MicroplateIsFullException.php b/src/Microplate/Exceptions/MicroplateIsFullException.php index fb436d4..efada7a 100644 --- a/src/Microplate/Exceptions/MicroplateIsFullException.php +++ b/src/Microplate/Exceptions/MicroplateIsFullException.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Microplate\Exceptions; -final class MicroplateIsFullException extends \UnexpectedValueException +class MicroplateIsFullException extends \UnexpectedValueException { public function __construct() { diff --git a/src/Microplate/Exceptions/SectionDoesNotExistException.php b/src/Microplate/Exceptions/SectionDoesNotExistException.php index 5baed0d..868f94a 100644 --- a/src/Microplate/Exceptions/SectionDoesNotExistException.php +++ b/src/Microplate/Exceptions/SectionDoesNotExistException.php @@ -2,4 +2,4 @@ namespace MLL\Utils\Microplate\Exceptions; -final class SectionDoesNotExistException extends \Exception {} +class SectionDoesNotExistException extends \Exception {} diff --git a/src/Microplate/Exceptions/SectionIsFullException.php b/src/Microplate/Exceptions/SectionIsFullException.php index 9475686..9225a8e 100644 --- a/src/Microplate/Exceptions/SectionIsFullException.php +++ b/src/Microplate/Exceptions/SectionIsFullException.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Microplate\Exceptions; -final class SectionIsFullException extends \UnexpectedValueException +class SectionIsFullException extends \UnexpectedValueException { public function __construct() { diff --git a/src/Microplate/Exceptions/UnexpectedFlowDirection.php b/src/Microplate/Exceptions/UnexpectedFlowDirection.php index 568a0c6..c68afd4 100644 --- a/src/Microplate/Exceptions/UnexpectedFlowDirection.php +++ b/src/Microplate/Exceptions/UnexpectedFlowDirection.php @@ -4,7 +4,7 @@ use MLL\Utils\Microplate\Enums\FlowDirection; -final class UnexpectedFlowDirection extends \UnexpectedValueException +class UnexpectedFlowDirection extends \UnexpectedValueException { public function __construct(FlowDirection $flowDirection) { diff --git a/src/Microplate/Exceptions/WellNotEmptyException.php b/src/Microplate/Exceptions/WellNotEmptyException.php index f5db0d6..15d5818 100644 --- a/src/Microplate/Exceptions/WellNotEmptyException.php +++ b/src/Microplate/Exceptions/WellNotEmptyException.php @@ -2,4 +2,4 @@ namespace MLL\Utils\Microplate\Exceptions; -final class WellNotEmptyException extends \Exception {} +class WellNotEmptyException extends \Exception {} diff --git a/src/Microplate/FullColumnSection.php b/src/Microplate/FullColumnSection.php index 582b417..598bf53 100644 --- a/src/Microplate/FullColumnSection.php +++ b/src/Microplate/FullColumnSection.php @@ -14,7 +14,7 @@ * * @extends AbstractSection */ -final class FullColumnSection extends AbstractSection +class FullColumnSection extends AbstractSection { public function __construct(SectionedMicroplate $sectionedMicroplate) { diff --git a/src/Microplate/Microplate.php b/src/Microplate/Microplate.php index 1c65b25..f8d878a 100644 --- a/src/Microplate/Microplate.php +++ b/src/Microplate/Microplate.php @@ -15,7 +15,7 @@ * * @phpstan-type WellsCollection Collection */ -final class Microplate extends AbstractMicroplate +class Microplate extends AbstractMicroplate { /** @var WellsCollection */ protected Collection $wells; diff --git a/src/Microplate/MicroplateSet/Location.php b/src/Microplate/MicroplateSet/Location.php index 2ae44e4..ff28f51 100644 --- a/src/Microplate/MicroplateSet/Location.php +++ b/src/Microplate/MicroplateSet/Location.php @@ -6,7 +6,7 @@ use MLL\Utils\Microplate\CoordinateSystem; /** @template TCoordinateSystem of CoordinateSystem */ -final class Location +class Location { public string $plateID; diff --git a/src/Microplate/MicroplateSet/MicroplateSetAB.php b/src/Microplate/MicroplateSet/MicroplateSetAB.php index cd95022..fd6c17f 100644 --- a/src/Microplate/MicroplateSet/MicroplateSetAB.php +++ b/src/Microplate/MicroplateSet/MicroplateSetAB.php @@ -9,7 +9,7 @@ * * @phpstan-extends MicroplateSet */ -final class MicroplateSetAB extends MicroplateSet +class MicroplateSetAB extends MicroplateSet { /** Duplicates @see MicroplateSet::plateCount() for static contexts. */ public const PLATE_COUNT = 2; diff --git a/src/Microplate/MicroplateSet/MicroplateSetABCD.php b/src/Microplate/MicroplateSet/MicroplateSetABCD.php index 1b1a06a..b038275 100644 --- a/src/Microplate/MicroplateSet/MicroplateSetABCD.php +++ b/src/Microplate/MicroplateSet/MicroplateSetABCD.php @@ -9,7 +9,7 @@ * * @phpstan-extends MicroplateSet */ -final class MicroplateSetABCD extends MicroplateSet +class MicroplateSetABCD extends MicroplateSet { /** Duplicates @see MicroplateSet::plateCount() for static contexts. */ public const PLATE_COUNT = 4; diff --git a/src/Microplate/MicroplateSet/MicroplateSetABCDE.php b/src/Microplate/MicroplateSet/MicroplateSetABCDE.php index 1a67c36..f285501 100644 --- a/src/Microplate/MicroplateSet/MicroplateSetABCDE.php +++ b/src/Microplate/MicroplateSet/MicroplateSetABCDE.php @@ -9,7 +9,7 @@ * * @phpstan-extends MicroplateSet */ -final class MicroplateSetABCDE extends MicroplateSet +class MicroplateSetABCDE extends MicroplateSet { /** Duplicates @see MicroplateSet::plateCount() for static contexts. */ public const PLATE_COUNT = 5; diff --git a/src/Microplate/Scalars/Column96Well.php b/src/Microplate/Scalars/Column96Well.php index b4cdf8e..12062ac 100644 --- a/src/Microplate/Scalars/Column96Well.php +++ b/src/Microplate/Scalars/Column96Well.php @@ -9,7 +9,7 @@ use GraphQL\Type\Definition\ScalarType; use GraphQL\Utils\Utils; -final class Column96Well extends ScalarType +class Column96Well extends ScalarType { public const MAX_INT = 12; public const MIN_INT = 1; diff --git a/src/Microplate/Scalars/Row96Well.php b/src/Microplate/Scalars/Row96Well.php index 736770e..4f8b779 100644 --- a/src/Microplate/Scalars/Row96Well.php +++ b/src/Microplate/Scalars/Row96Well.php @@ -4,7 +4,7 @@ use MLL\GraphQLScalars\Regex; -final class Row96Well extends Regex +class Row96Well extends Regex { public ?string $description = 'Checks if the given row is of the format 96-well row'; diff --git a/src/Microplate/Section.php b/src/Microplate/Section.php index 1927d49..2885888 100644 --- a/src/Microplate/Section.php +++ b/src/Microplate/Section.php @@ -9,7 +9,7 @@ * * @extends AbstractSection */ -final class Section extends AbstractSection +class Section extends AbstractSection { /** * @param TSectionWell $content diff --git a/src/Microplate/SectionedMicroplate.php b/src/Microplate/SectionedMicroplate.php index 8111b43..4d072b8 100644 --- a/src/Microplate/SectionedMicroplate.php +++ b/src/Microplate/SectionedMicroplate.php @@ -13,7 +13,7 @@ * * @phpstan-extends AbstractMicroplate */ -final class SectionedMicroplate extends AbstractMicroplate +class SectionedMicroplate extends AbstractMicroplate { /** @var Collection */ public Collection $sections; diff --git a/src/Microplate/WellWithCoordinates.php b/src/Microplate/WellWithCoordinates.php index c332c50..ba79d0f 100644 --- a/src/Microplate/WellWithCoordinates.php +++ b/src/Microplate/WellWithCoordinates.php @@ -6,7 +6,7 @@ * @template TWell * @template TCoordinateSystem of CoordinateSystem */ -final class WellWithCoordinates +class WellWithCoordinates { /** @var TWell */ public $content; diff --git a/src/Number.php b/src/Number.php index 29a9380..55a74f0 100644 --- a/src/Number.php +++ b/src/Number.php @@ -2,7 +2,7 @@ namespace MLL\Utils; -final class Number +class Number { /** * Return number as long as its clamped between min and max. diff --git a/src/StringUtil.php b/src/StringUtil.php index 180bb9a..e3a291f 100644 --- a/src/StringUtil.php +++ b/src/StringUtil.php @@ -4,7 +4,7 @@ use Illuminate\Support\Str; -final class StringUtil +class StringUtil { /** https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 */ public const UTF_8_BOM = "\xEF\xBB\xBF"; diff --git a/src/Tecan/BasicCommands/Aspirate.php b/src/Tecan/BasicCommands/Aspirate.php index c251963..9eff6ca 100644 --- a/src/Tecan/BasicCommands/Aspirate.php +++ b/src/Tecan/BasicCommands/Aspirate.php @@ -5,7 +5,7 @@ use MLL\Utils\Tecan\LiquidClass\LiquidClass; use MLL\Utils\Tecan\Location\Location; -final class Aspirate extends BasicPipettingActionCommand +class Aspirate extends BasicPipettingActionCommand { /** * @param float $volume Floating point values are accepted and do not cause an error, diff --git a/src/Tecan/BasicCommands/AspirateAndDispenseParameters.php b/src/Tecan/BasicCommands/AspirateAndDispenseParameters.php index 7e6f21d..57187ed 100644 --- a/src/Tecan/BasicCommands/AspirateAndDispenseParameters.php +++ b/src/Tecan/BasicCommands/AspirateAndDispenseParameters.php @@ -4,7 +4,7 @@ use MLL\Utils\Tecan\Rack\Rack; -final class AspirateAndDispenseParameters +class AspirateAndDispenseParameters { private Rack $rack; diff --git a/src/Tecan/BasicCommands/BreakCommand.php b/src/Tecan/BasicCommands/BreakCommand.php index 4fdbbcd..0436e47 100644 --- a/src/Tecan/BasicCommands/BreakCommand.php +++ b/src/Tecan/BasicCommands/BreakCommand.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Tecan\BasicCommands; -final class BreakCommand extends Command +class BreakCommand extends Command { public function toString(): string { diff --git a/src/Tecan/BasicCommands/Comment.php b/src/Tecan/BasicCommands/Comment.php index f84e04d..9d8f3b9 100644 --- a/src/Tecan/BasicCommands/Comment.php +++ b/src/Tecan/BasicCommands/Comment.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Tecan\BasicCommands; -final class Comment extends Command +class Comment extends Command { private string $comment; diff --git a/src/Tecan/BasicCommands/Dispense.php b/src/Tecan/BasicCommands/Dispense.php index 1e1bc27..3ad8eb4 100644 --- a/src/Tecan/BasicCommands/Dispense.php +++ b/src/Tecan/BasicCommands/Dispense.php @@ -5,7 +5,7 @@ use MLL\Utils\Tecan\LiquidClass\LiquidClass; use MLL\Utils\Tecan\Location\Location; -final class Dispense extends BasicPipettingActionCommand +class Dispense extends BasicPipettingActionCommand { /** * @param float $volume Floating point values are accepted and do not cause an error, diff --git a/src/Tecan/BasicCommands/ReagentDistribution.php b/src/Tecan/BasicCommands/ReagentDistribution.php index 02d9160..8e10e85 100644 --- a/src/Tecan/BasicCommands/ReagentDistribution.php +++ b/src/Tecan/BasicCommands/ReagentDistribution.php @@ -5,7 +5,7 @@ use MLL\Utils\Tecan\LiquidClass\LiquidClass; use MLL\Utils\Tecan\ReagentDistribution\ReagentDistributionDirection; -final class ReagentDistribution extends Command +class ReagentDistribution extends Command { private AspirateAndDispenseParameters $source; diff --git a/src/Tecan/BasicCommands/Wash.php b/src/Tecan/BasicCommands/Wash.php index 33fd5f6..3ba3390 100644 --- a/src/Tecan/BasicCommands/Wash.php +++ b/src/Tecan/BasicCommands/Wash.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Tecan\BasicCommands; -final class Wash extends Command +class Wash extends Command { public function toString(): string { diff --git a/src/Tecan/CustomCommands/AspirateParameters.php b/src/Tecan/CustomCommands/AspirateParameters.php index ea67bf5..070b01f 100644 --- a/src/Tecan/CustomCommands/AspirateParameters.php +++ b/src/Tecan/CustomCommands/AspirateParameters.php @@ -5,7 +5,7 @@ use MLL\Utils\Tecan\BasicCommands\AspirateAndDispenseParameters; use MLL\Utils\Tecan\Rack\Rack; -final class AspirateParameters +class AspirateParameters { private Rack $rack; diff --git a/src/Tecan/CustomCommands/DispenseParameters.php b/src/Tecan/CustomCommands/DispenseParameters.php index d147b50..5eee089 100644 --- a/src/Tecan/CustomCommands/DispenseParameters.php +++ b/src/Tecan/CustomCommands/DispenseParameters.php @@ -5,7 +5,7 @@ use MLL\Utils\Tecan\BasicCommands\AspirateAndDispenseParameters; use MLL\Utils\Tecan\Rack\Rack; -final class DispenseParameters +class DispenseParameters { public Rack $rack; diff --git a/src/Tecan/CustomCommands/MLLReagentDistribution.php b/src/Tecan/CustomCommands/MLLReagentDistribution.php index ac74d43..fbcd06c 100644 --- a/src/Tecan/CustomCommands/MLLReagentDistribution.php +++ b/src/Tecan/CustomCommands/MLLReagentDistribution.php @@ -7,7 +7,7 @@ use MLL\Utils\Tecan\LiquidClass\LiquidClass; use MLL\Utils\Tecan\ReagentDistribution\ReagentDistributionDirection; -final class MLLReagentDistribution extends Command +class MLLReagentDistribution extends Command { public const NUMBER_OF_DITI_REUSES = 6; public const NUMBER_OF_MULTI_DISP = 1; diff --git a/src/Tecan/CustomCommands/TransferWithAutoWash.php b/src/Tecan/CustomCommands/TransferWithAutoWash.php index d40cf07..3b08b3f 100644 --- a/src/Tecan/CustomCommands/TransferWithAutoWash.php +++ b/src/Tecan/CustomCommands/TransferWithAutoWash.php @@ -11,7 +11,7 @@ use MLL\Utils\Tecan\Location\Location; use MLL\Utils\Tecan\TecanProtocol; -final class TransferWithAutoWash extends Command implements UsesTipMask +class TransferWithAutoWash extends Command implements UsesTipMask { private Aspirate $aspirate; diff --git a/src/Tecan/LiquidClass/CustomLiquidClass.php b/src/Tecan/LiquidClass/CustomLiquidClass.php index 09ba698..336f17e 100644 --- a/src/Tecan/LiquidClass/CustomLiquidClass.php +++ b/src/Tecan/LiquidClass/CustomLiquidClass.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Tecan\LiquidClass; -final class CustomLiquidClass implements LiquidClass +class CustomLiquidClass implements LiquidClass { private string $name; diff --git a/src/Tecan/Location/BarcodeLocation.php b/src/Tecan/Location/BarcodeLocation.php index 737485a..9fa7235 100644 --- a/src/Tecan/Location/BarcodeLocation.php +++ b/src/Tecan/Location/BarcodeLocation.php @@ -4,7 +4,7 @@ use MLL\Utils\Tecan\Rack\Rack; -final class BarcodeLocation implements Location +class BarcodeLocation implements Location { private string $barcode; diff --git a/src/Tecan/Location/PositionLocation.php b/src/Tecan/Location/PositionLocation.php index 13b0b4b..d1c5825 100644 --- a/src/Tecan/Location/PositionLocation.php +++ b/src/Tecan/Location/PositionLocation.php @@ -4,7 +4,7 @@ use MLL\Utils\Tecan\Rack\Rack; -final class PositionLocation implements Location +class PositionLocation implements Location { private int $position; diff --git a/src/Tecan/Rack/CustomRack.php b/src/Tecan/Rack/CustomRack.php index dece89b..57ccba1 100644 --- a/src/Tecan/Rack/CustomRack.php +++ b/src/Tecan/Rack/CustomRack.php @@ -2,7 +2,7 @@ namespace MLL\Utils\Tecan\Rack; -final class CustomRack implements Rack +class CustomRack implements Rack { private string $name; diff --git a/src/Tecan/TecanException.php b/src/Tecan/TecanException.php index 0c9d16c..530d6ff 100644 --- a/src/Tecan/TecanException.php +++ b/src/Tecan/TecanException.php @@ -2,4 +2,4 @@ namespace MLL\Utils\Tecan; -final class TecanException extends \Exception {} +class TecanException extends \Exception {} diff --git a/src/Tecan/TecanProtocol.php b/src/Tecan/TecanProtocol.php index 12b27d2..f4e46df 100644 --- a/src/Tecan/TecanProtocol.php +++ b/src/Tecan/TecanProtocol.php @@ -13,7 +13,7 @@ use MLL\Utils\Tecan\BasicCommands\UsesTipMask; use MLL\Utils\Tecan\TipMask\TipMask; -final class TecanProtocol +class TecanProtocol { /** Tecan software runs on Windows. */ public const WINDOWS_NEW_LINE = "\r\n"; diff --git a/src/TecanScanner/NoRackIDException.php b/src/TecanScanner/NoRackIDException.php index a77393c..739f6bb 100644 --- a/src/TecanScanner/NoRackIDException.php +++ b/src/TecanScanner/NoRackIDException.php @@ -2,7 +2,7 @@ namespace MLL\Utils\TecanScanner; -final class NoRackIDException extends TecanScanException +class NoRackIDException extends TecanScanException { public function __construct() { diff --git a/src/TecanScanner/TecanScanEmptyException.php b/src/TecanScanner/TecanScanEmptyException.php index 2e4bb50..370ecd1 100644 --- a/src/TecanScanner/TecanScanEmptyException.php +++ b/src/TecanScanner/TecanScanEmptyException.php @@ -2,7 +2,7 @@ namespace MLL\Utils\TecanScanner; -final class TecanScanEmptyException extends TecanScanException +class TecanScanEmptyException extends TecanScanException { public function __construct() { diff --git a/src/TecanScanner/TecanScanner.php b/src/TecanScanner/TecanScanner.php index 923308e..ef9edcb 100644 --- a/src/TecanScanner/TecanScanner.php +++ b/src/TecanScanner/TecanScanner.php @@ -9,7 +9,7 @@ use MLL\Utils\StringUtil; /** The plate scanner on a tecan worktable. */ -final class TecanScanner +class TecanScanner { public const NO_READ = 'NO READ'; public const RACKID_IDENTIFIER = 'rackid,'; diff --git a/src/TecanScanner/WrongNumberOfWells.php b/src/TecanScanner/WrongNumberOfWells.php index 9537c74..95520dc 100644 --- a/src/TecanScanner/WrongNumberOfWells.php +++ b/src/TecanScanner/WrongNumberOfWells.php @@ -2,7 +2,7 @@ namespace MLL\Utils\TecanScanner; -final class WrongNumberOfWells extends TecanScanException +class WrongNumberOfWells extends TecanScanException { public function __construct(int $expectedCount, int $actualCount) { From a8bd37cdadacdde992f866ba1c592bfecdaecc1a Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 16:05:12 +0200 Subject: [PATCH 12/16] mll-lab/graphql-php-scalars 5 --- .github/workflows/validate.yml | 3 --- composer.json | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fa81f7e..3a0aeae 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -75,9 +75,6 @@ jobs: extensions: mbstring php-version: "${{ matrix.php-version }}" - - if: "! startsWith(matrix.php-version, 8)" - run: composer remove --dev --no-update mll-lab/graphql-php-scalars - - run: composer require "illuminate/support:${{ matrix.illuminate }}" --no-interaction --no-update - if: matrix.dependencies == 'lowest' diff --git a/composer.json b/composer.json index 2dac276..5b5c428 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "illuminate/database": "^8.73 || ^9 || ^10", "infection/infection": "^0.26 || ^0.27", "jangregor/phpstan-prophecy": "^1", - "mll-lab/graphql-php-scalars": "^6", + "mll-lab/graphql-php-scalars": "^5 || ^6", "mll-lab/php-cs-fixer-config": "^5", "nunomaduro/larastan": "^1 || ^2", "orchestra/testbench": "^5 || ^6 || ^7 || ^8", From ce0d12ae4419db5b641ec8df933eb76815dc1c83 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 16:22:43 +0200 Subject: [PATCH 13/16] revert mll-lab/graphql-php-scalars 5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5b5c428..2dac276 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "illuminate/database": "^8.73 || ^9 || ^10", "infection/infection": "^0.26 || ^0.27", "jangregor/phpstan-prophecy": "^1", - "mll-lab/graphql-php-scalars": "^5 || ^6", + "mll-lab/graphql-php-scalars": "^6", "mll-lab/php-cs-fixer-config": "^5", "nunomaduro/larastan": "^1 || ^2", "orchestra/testbench": "^5 || ^6 || ^7 || ^8", From 432fab225d9bcb586fc0cdcdb96944d09631deee Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 16:26:32 +0200 Subject: [PATCH 14/16] skip --- tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php b/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php index 0b8c14e..67eb45a 100644 --- a/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php +++ b/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php @@ -2,6 +2,7 @@ namespace MLL\Utils\Tests\FluidXPlate\Scalars; +use Composer\InstalledVersions; use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use MLL\Utils\FluidXPlate\Scalars\FrameStarBarcode; @@ -9,6 +10,15 @@ final class FrameStarBarcodeTest extends TestCase { + protected function setUp(): void + { + if (! InstalledVersions::isInstalled('mll-lab/graphql-php-scalars')) { + self::markTestSkipped('This test requires mll-lab/graphql-php-scalars to be installed.'); + } + + parent::setUp(); + } + public function testSerializeThrowsIfIsInvalid(): void { $this->expectException(InvariantViolation::class); From efd9911d4a057629d7a265468e3a6cd474fb4ced Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 17:01:16 +0200 Subject: [PATCH 15/16] skip install --- .github/workflows/validate.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3a0aeae..fa81f7e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -75,6 +75,9 @@ jobs: extensions: mbstring php-version: "${{ matrix.php-version }}" + - if: "! startsWith(matrix.php-version, 8)" + run: composer remove --dev --no-update mll-lab/graphql-php-scalars + - run: composer require "illuminate/support:${{ matrix.illuminate }}" --no-interaction --no-update - if: matrix.dependencies == 'lowest' From 0075a181898aa0885ed2b0b728e54c160d3f2c4d Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 23 Apr 2024 17:04:51 +0200 Subject: [PATCH 16/16] extract ScalarTestCase --- .../FluidXPlate/Scalars/FluidXBarcodeTest.php | 4 ++-- .../Scalars/FrameStarBarcodeTest.php | 14 ++------------ tests/ScalarTestCase.php | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 tests/ScalarTestCase.php diff --git a/tests/FluidXPlate/Scalars/FluidXBarcodeTest.php b/tests/FluidXPlate/Scalars/FluidXBarcodeTest.php index f518aa1..936b7ba 100644 --- a/tests/FluidXPlate/Scalars/FluidXBarcodeTest.php +++ b/tests/FluidXPlate/Scalars/FluidXBarcodeTest.php @@ -5,9 +5,9 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use MLL\Utils\FluidXPlate\Scalars\FluidXBarcode; -use PHPUnit\Framework\TestCase; +use MLL\Utils\Tests\ScalarTestCase; -final class FluidXBarcodeTest extends TestCase +final class FluidXBarcodeTest extends ScalarTestCase { public function testSerializeThrowsIfIsInvalid(): void { diff --git a/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php b/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php index 67eb45a..2f05129 100644 --- a/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php +++ b/tests/FluidXPlate/Scalars/FrameStarBarcodeTest.php @@ -2,23 +2,13 @@ namespace MLL\Utils\Tests\FluidXPlate\Scalars; -use Composer\InstalledVersions; use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use MLL\Utils\FluidXPlate\Scalars\FrameStarBarcode; -use PHPUnit\Framework\TestCase; +use MLL\Utils\Tests\ScalarTestCase; -final class FrameStarBarcodeTest extends TestCase +final class FrameStarBarcodeTest extends ScalarTestCase { - protected function setUp(): void - { - if (! InstalledVersions::isInstalled('mll-lab/graphql-php-scalars')) { - self::markTestSkipped('This test requires mll-lab/graphql-php-scalars to be installed.'); - } - - parent::setUp(); - } - public function testSerializeThrowsIfIsInvalid(): void { $this->expectException(InvariantViolation::class); diff --git a/tests/ScalarTestCase.php b/tests/ScalarTestCase.php new file mode 100644 index 0000000..f273fda --- /dev/null +++ b/tests/ScalarTestCase.php @@ -0,0 +1,18 @@ +