File: D:/HostingSpaces/slenders/slenders.nl/app/Komma/Kms/Core/Sequence/Sequence.php
<?php
namespace App\Komma\Kms\Core\Sequence;
use App\Komma\Kms\Core\Sequence\Parts\AbstractPart;
use Illuminate\Support\Facades\DB;
/**
* Class Sequence
*
* Builds sequences.
*/
class Sequence {
/** @var AbstractPart[] $parts Defines how the sequence is structures*/
private $parts;
/** @var bool */
private $lastPartMayOverflow = false;
/** @var string */
private $table;
/** @var string */
private $column;
public function __construct()
{
$this->parts = [];
}
/**
* Add a part that describes a part of the sequence's structure to the beginning of the describing structure.
*
* @param $part
* @return Sequence
*/
public function startsWith($part): Sequence
{
$this->validatePart($part);
array_unshift($this->parts, $part);
return $this;
}
/**
* Add a part that describes a part of the sequence's structure to the end of the describing structure
*
* @param $part
* @return Sequence
*/
public function followedBy($part): Sequence
{
$this->validatePart($part);
$this->parts[] = $part;
return $this;
}
/**
* Seeds / primes the sequence with a code / number to start from.
*
* @param string $value
* @return Sequence
*/
public function startingAt(string $value): Sequence
{
$originalValue = $value;
//Fill the parts of the sequence with their values
$copiedParts = $this->parts;
while($currentPart = array_shift($copiedParts))
{
if(is_a($currentPart, AbstractPart::class)) {
//Give the part the whole value, it will shorten it internally when needed, and set its starting value appropriately
$mayOverflow = ($this->lastPartMayOverflow && count($copiedParts) == 0);
$currentPart->startingAt($value, $mayOverflow);
$length = mb_strlen($currentPart->getValue());
} else {
//If it is not an Abstract part, it is a string, and we just need to strip that string from the value.
$length = mb_strlen($currentPart);
}
//Remove the currentPart value of the value. The remaining data will be fed into the next part.
$value = substr($value, $length);
}
if($value !== '') {
throw new \InvalidArgumentException('Sequence: The value was invalid. The sequence must be '.$this->describe().'Was: '.$originalValue);
}
return $this;
}
/**
* Generate a new code based on the sequence
* You can specify which part of the sequence to increment.
* Do this by passing the zero based index of the part in the sequence, or by passing a name of the part.
* If you don't specify a which part to increment, the last part will be incremented.
*
* @param null|string|int $partIdentifier
* @return Sequence
*/
public function next($partIdentifier = null): Sequence
{
//If no part identifier is given. We set the identifier to the last part
$lastPartIdentifierNumber = count($this->getParts()) - 1;
if($partIdentifier == null) $partIdentifier = $lastPartIdentifierNumber;
//Get the part we need to "next".
if(is_string($partIdentifier)) {
$part = $this->getPartByName($partIdentifier);
} elseif(is_int($partIdentifier)) {
if($partIdentifier > $lastPartIdentifierNumber) throw new \OutOfBoundsException('Sequence: You wanted to increment part number "'.$partIdentifier.'". But the last partIdentifier is "'.$lastPartIdentifierNumber.'"');
$part = $this->getParts()[$partIdentifier];
} else {
throw new \InvalidArgumentException('Sequence: The partNumber to increment must either be null, an zero based integer referencing to a part, or a name of a part');
}
if(is_string($part)) throw new \InvalidArgumentException('Sequence: Could not increment the string "'.$partIdentifier.'" obviously.');
$part->next();
$this->throwExceptionWhenOverflownIllegally();
if(!$this->isUnique($this)) throw new \RuntimeException('Sequence: Value "'.$this.'" is not unique in database table "'.$this->table.'", column "'.$this->column.'"');
return $this;
}
/**
* Throws an exception when one of the parts in the sequence did overflow.
* The last part may overflow only when lastPartMayOverflow was called.
*
* @see lastPartMayOverflow
*/
public function throwExceptionWhenOverflownIllegally()
{
$itemsCount = count($this->parts);
foreach($this->parts as $index => $part)
{
if(is_string($part)) continue;
if($part->overflownBecause() === '' || ($this->lastPartMayOverflow && $index = $itemsCount - 1)) continue;
$partName = $part->getName() == '' ?: ' ('.$part->getName().')';
throw new \OutOfBoundsException('Sequence: Part '.$index.$partName.' did overflow because '.$part->overflownBecause());
}
}
/**
* Converts the sequence to a string when it is used in a string, or is casted to a string.
*
* @return string
*/
public function __toString()
{
$currentValue = '';
foreach ($this->parts as $currentPart) {
$currentValue .= (is_a($currentPart, AbstractPart::class)) ? $currentPart->getValue() : $currentPart;
}
return $currentValue;
}
/**
* Validates a part.
*
* @param $part
* @return bool
*/
private function validatePart($part) {
if(!is_string($part) && !is_a($part, AbstractPart::class)) {
throw new \InvalidArgumentException('The given "part" is neither a string or an subclass of '.AbstractPart::class);
}
return true;
}
/**
* Returns a human readable description of how the sequence structure looks like
*
* @return string
*/
public function describe():string {
$description = '';
foreach ($this->parts as $index => $currentPart) {
$description .= $index == 0 ? 'starting with ' : 'Followed by ';
if(is_a($currentPart, AbstractPart::class)) {
$description .= $currentPart->describe().' ';
} else {
$description .= 'the string "'.$currentPart.'". ';
}
}
return $description;
}
/**
* Searches through the part of the sequence to find a part by its name.
* If more then one part share the same name, The first one is returned.
*
* @param string $name
* @return string|AbstractPart
*/
public function getPartByName(string $name)
{
foreach($this->parts as $part)
{
if(is_a($part, AbstractPart::class)) {
if(strtolower($part->getName()) == strtolower($name)) return $part; //Part found a a child of AbstractPart. Return that.
} elseif(is_string($part)) {
if(strtolower($name) == strtolower($part)) return $part; //Part found as string. Simply return the string.
}
}
throw new \InvalidArgumentException('Sequence: i don\'t have have a part called "'.$name.'"');
}
/**
* @return AbstractPart[]
*/
public function getParts(): array
{
return $this->parts;
}
/**
* Allows the last part in the sequence to overflow.
*/
public function lastPartMayOverflow()
{
$this->lastPartMayOverflow = true;
}
/**
* @param string $table
* @param string $column
* @return $this
*/
public function uniqueForTable(string $table, string $column) {
$this->table = $table;
$this->column = $column;
$latestRecords = DB::table($table)->orderBy('id', 'DESC')->limit(1)->get([$column]);
if($latestRecords->count() == 1) {
$latestSequenceValueFromTable = $latestRecords[0]->$column;
$this->startingAt($latestSequenceValueFromTable);
}
return $this;
}
/**
* Check the table and column if the given value is unique.
* If you did not specify a table and column, this method always returns true.
* This function is also used internally by the next method
*
* @see next()
* @param string $value
* @return bool
*/
public function isUnique(string $value) {
if(!$this->table && !$this->column) return true;
$latestRecords = DB::table($this->table)->where($this->column, '=', $value)->get([$this->column]);
return count($latestRecords) == 0;
}
/**
* Clones itself
*/
public function __clone()
{
foreach ($this as $key => $val) {
if (is_object($val) || (is_array($val))) {
$this->{$key} = unserialize(serialize($val)); //Unserializing a serialized value is the trick for a deep copy.
}
}
}
}