Skip to content

Commit

Permalink
Integrate mll-lab/microplate
Browse files Browse the repository at this point in the history
  • Loading branch information
spawnia committed Apr 23, 2024
1 parent b49763e commit 644dcc3
Show file tree
Hide file tree
Showing 44 changed files with 3,062 additions and 12 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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/"
Expand Down
148 changes: 148 additions & 0 deletions src/Microplate/AbstractMicroplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php declare(strict_types=1);

namespace MLL\Utils\Microplate;

use Illuminate\Support\Collection;
use MLL\Utils\Microplate\Enums\FlowDirection;
use MLL\Utils\Microplate\Exceptions\UnexpectedFlowDirection;

/**
* @template TWell
* @template TCoordinateSystem of CoordinateSystem
*
* @phpstan-type WellsCollection Collection<string, TWell|null>
*/
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<TCoordinateSystem> $coordinate
*
* @return TWell|null
*/
public function well(Coordinates $coordinate)
{
return $this->wells()[$coordinate->toString()];
}

/** @param Coordinates<TCoordinateSystem> $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<string, null> */
public function freeWells(): Collection
{
return $this->wells()->filter(
/**
* @param TWell $content
*/
static fn ($content): bool => $content === self::EMPTY_WELL
);
}

/** @return Collection<string, TWell> */
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<TWell, TCoordinateSystem>
*/
public function toWellWithCoordinateMapper(): callable
{
return $this->toWellWithCoordinatesMapper();
}

/** @return callable(TWell $content, string $coordinatesString): WellWithCoordinates<TWell, TCoordinateSystem> */
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();
}
}
27 changes: 27 additions & 0 deletions src/Microplate/AbstractSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types=1);

namespace MLL\Utils\Microplate;

use Illuminate\Support\Collection;

/**
* @template TSectionWell
*/
abstract class AbstractSection
{
/** @var SectionedMicroplate<TSectionWell, CoordinateSystem, static> */
public SectionedMicroplate $sectionedMicroplate;

/** @var Collection<int, TSectionWell|null> */
public Collection $sectionItems;

/** @param SectionedMicroplate<TSectionWell, CoordinateSystem, static> $sectionedMicroplate */
public function __construct(SectionedMicroplate $sectionedMicroplate)
{
$this->sectionedMicroplate = $sectionedMicroplate;
$this->sectionItems = new Collection();
}

/** @param TSectionWell $content */
abstract public function addWell($content): void;
}
40 changes: 40 additions & 0 deletions src/Microplate/Casts/Coordinates96Well.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types=1);

namespace MLL\Utils\Microplate\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use MLL\Utils\Microplate\Coordinates;
use MLL\Utils\Microplate\CoordinateSystem96Well;

/**
* @implements CastsAttributes<Coordinates<CoordinateSystem96Well>, Coordinates<CoordinateSystem96Well>>
*/
final class Coordinates96Well implements CastsAttributes
{
/**
* @param Model $model
* @param string $key
* @param array<array-key, mixed> $attributes
*
* @return Coordinates<CoordinateSystem96Well>
*/
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<array-key, mixed> $attributes
*/
public function set($model, $key, $value, $attributes): string
{
assert($value instanceof Coordinates);

return $value->toString();
}
}
128 changes: 128 additions & 0 deletions src/Microplate/CoordinateSystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php declare(strict_types=1);

namespace MLL\Utils\Microplate;

use Illuminate\Support\Arr;

abstract class CoordinateSystem
{
/** @return list<string> */
abstract public function rows(): array;

/** @return list<int> */
abstract public function columns(): array;

/**
* List of columns, 0-padded to all have the same length.
*
* @return list<string>
*/
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<int, Coordinates<$this>>
*/
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());
}
}
Loading

0 comments on commit 644dcc3

Please sign in to comment.