diff --git a/.github/ISSUE_TEMPLATE/bugs.yml b/.github/ISSUE_TEMPLATE/bugs.yml new file mode 100644 index 000000000..ad1e90422 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bugs.yml @@ -0,0 +1,64 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug"] +assignees: + - spl1nes +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: dropdown + id: version + attributes: + label: Version + description: What version of our software are you running? + options: + - Alpha (Default) + default: 0 + validations: + required: true + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Opera + - Microsoft Edge + - Other + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/Account/Account.php b/Account/Account.php index 5471d1c5f..a55bbf6fe 100755 --- a/Account/Account.php +++ b/Account/Account.php @@ -15,7 +15,6 @@ declare(strict_types=1); namespace phpOMS\Account; use phpOMS\Localization\Localization; -use phpOMS\Stdlib\Base\Exception\InvalidEnumValue; use phpOMS\Validation\Network\Email; /** @@ -164,12 +163,12 @@ class Account implements \JsonSerializable */ public function hasPermission( int $permission, - int $unit = null, - int $app = null, - string $module = null, - int $category = null, - int $element = null, - int $component = null + ?int $unit = null, + ?int $app = null, + ?string $module = null, + ?int $category = null, + ?int $element = null, + ?int $component = null ) : bool { foreach ($this->groups as $group) { @@ -317,70 +316,6 @@ class Account implements \JsonSerializable $this->email = \mb_strtolower($email); } - /** - * Get status. - * - * @return int Returns the status (AccountStatus) - * - * @since 1.0.0 - */ - public function getStatus() : int - { - return $this->status; - } - - /** - * Get status. - * - * @param int $status Status - * - * @return void - * - * @throws InvalidEnumValue This exception is thrown if a invalid status is used - * - * @since 1.0.0 - */ - public function setStatus(int $status) : void - { - if (!AccountStatus::isValidValue($status)) { - throw new InvalidEnumValue($status); - } - - $this->status = $status; - } - - /** - * Get type. - * - * @return int Returns the type (AccountType) - * - * @since 1.0.0 - */ - public function getType() : int - { - return $this->type; - } - - /** - * Get type. - * - * @param int $type Type - * - * @return void - * - * @throws InvalidEnumValue This exception is thrown if an invalid type is used - * - * @since 1.0.0 - */ - public function setType(int $type) : void - { - if (!AccountType::isValidValue($type)) { - throw new InvalidEnumValue($type); - } - - $this->type = $type; - } - /** * Get last activity. * @@ -445,8 +380,8 @@ class Account implements \JsonSerializable public function toArray() : array { return [ - 'id' => $this->id, - 'name' => [ + 'id' => $this->id, + 'name' => [ $this->name1, $this->name2, $this->name3, diff --git a/Account/AccountManager.php b/Account/AccountManager.php index 454cf9124..5942c9013 100755 --- a/Account/AccountManager.php +++ b/Account/AccountManager.php @@ -71,8 +71,8 @@ final class AccountManager implements \Countable if ($id === 0) { $account = new Account(Auth::authenticate($this->session)); - if (!isset($this->accounts[$account->getId()])) { - $this->accounts[$account->getId()] = $account; + if (!isset($this->accounts[$account->id])) { + $this->accounts[$account->id] = $account; } return $account; @@ -92,8 +92,8 @@ final class AccountManager implements \Countable */ public function add(Account $account) : bool { - if (!isset($this->accounts[$account->getId()])) { - $this->accounts[$account->getId()] = $account; + if (!isset($this->accounts[$account->id])) { + $this->accounts[$account->id] = $account; return true; } diff --git a/Account/Group.php b/Account/Group.php index 0049793a2..9f7405d8b 100755 --- a/Account/Group.php +++ b/Account/Group.php @@ -14,8 +14,6 @@ declare(strict_types=1); namespace phpOMS\Account; -use phpOMS\Stdlib\Base\Exception\InvalidEnumValue; - /** * Account group class. * @@ -88,38 +86,6 @@ class Group implements \JsonSerializable return $this->id; } - /** - * Get group status. - * - * @return int Group status - * - * @since 1.0.0 - */ - public function getStatus() : int - { - return $this->status; - } - - /** - * Set group status. - * - * @param int $status Group status - * - * @return void - * - * @throws InvalidEnumValue This exception is thrown if an invalid status is used - * - * @since 1.0.0 - */ - public function setStatus(int $status) : void - { - if (!GroupStatus::isValidValue($status)) { - throw new InvalidEnumValue($status); - } - - $this->status = $status; - } - /** * Get string representation. * diff --git a/Account/NullAccount.php b/Account/NullAccount.php index 1f8cf59f1..1bf4d5630 100755 --- a/Account/NullAccount.php +++ b/Account/NullAccount.php @@ -24,6 +24,18 @@ namespace phpOMS\Account; */ final class NullAccount extends Account { + /** + * Constructor + * + * @param int $id Model id + * + * @since 1.0.0 + */ + public function __construct(int $id = 0) + { + $this->id = $id; + } + /** * {@inheritdoc} */ diff --git a/Account/PermissionAbstract.php b/Account/PermissionAbstract.php index a70a7c63b..5f677810f 100755 --- a/Account/PermissionAbstract.php +++ b/Account/PermissionAbstract.php @@ -169,13 +169,13 @@ class PermissionAbstract implements \JsonSerializable * @since 1.0.0 */ public function __construct( - int $unit = null, - int $app = null, - string $module = null, - string $from = null, - int $category = null, - int $element = null, - int $component = null, + ?int $unit = null, + ?int $app = null, + ?string $module = null, + ?string $from = null, + ?int $category = null, + ?int $element = null, + ?int $component = null, int $permission = PermissionType::NONE ) { $this->unit = $unit; @@ -308,12 +308,12 @@ class PermissionAbstract implements \JsonSerializable */ public function hasPermission( int $permission, - int $unit = null, - int $app = null, - string $module = null, - int $category = null, - int $element = null, - int $component = null + ?int $unit = null, + ?int $app = null, + ?string $module = null, + ?int $category = null, + ?int $element = null, + ?int $component = null ) : bool { return $permission === PermissionType::NONE || @@ -352,15 +352,15 @@ class PermissionAbstract implements \JsonSerializable public function jsonSerialize() : mixed { return [ - 'id' => $this->id, - 'unit' => $this->unit, - 'app' => $this->app, - 'module' => $this->module, - 'from' => $this->from, - 'category' => $this->category, - 'element' => $this->element, - 'component' => $this->component, - 'permission' => $this->getPermission(), + 'id' => $this->id, + 'unit' => $this->unit, + 'app' => $this->app, + 'module' => $this->module, + 'from' => $this->from, + 'category' => $this->category, + 'element' => $this->element, + 'component' => $this->component, + 'permission' => $this->getPermission(), ]; } } diff --git a/Account/PermissionHandlingTrait.php b/Account/PermissionHandlingTrait.php index a4facc4ac..725d92f9c 100755 --- a/Account/PermissionHandlingTrait.php +++ b/Account/PermissionHandlingTrait.php @@ -135,12 +135,12 @@ trait PermissionHandlingTrait */ public function hasPermission( int $permission, - int $unit = null, - int $app = null, - string $module = null, - int $category = null, - int $element = null, - int $component = null + ?int $unit = null, + ?int $app = null, + ?string $module = null, + ?int $category = null, + ?int $element = null, + ?int $component = null ) : bool { foreach ($this->permissions as $p) { diff --git a/Ai/NeuralNetwork/Neuron.php b/Ai/NeuralNetwork/Neuron.php index 7769e4f4b..bdb5aa566 100755 --- a/Ai/NeuralNetwork/Neuron.php +++ b/Ai/NeuralNetwork/Neuron.php @@ -22,7 +22,7 @@ namespace phpOMS\Ai\NeuralNetwork; * @link https://jingga.app * @since 1.0.0 */ -class Neuron +final class Neuron { /** * Neuron inputs diff --git a/Ai/Ocr/Tesseract/TesseractOcr.php b/Ai/Ocr/Tesseract/TesseractOcr.php index d846222fd..36624a9b1 100755 --- a/Ai/Ocr/Tesseract/TesseractOcr.php +++ b/Ai/Ocr/Tesseract/TesseractOcr.php @@ -85,14 +85,34 @@ final class TesseractOcr * * @since 1.0.0 */ - public function parseImage(string $image, array $languages = ['eng'], int $psm = 3, int $oem = 3) : string + public function parseImage(string $image, array $languages = ['eng', 'deu'], int $psm = 3, int $oem = 3) : string { $temp = \tempnam(\sys_get_temp_dir(), 'oms_ocr_'); if ($temp === false) { return ''; } + $extension = 'png'; try { + // Tesseract needs higher dpi to work properly (identify + adjust if necessary) + $dpi = (int) \trim(\implode('', SystemUtils::runProc( + 'identify', + '-quiet -format "%x" ' . $image + ))); + + if ($dpi < 300) { + $split = \explode('.', $image); + $extension = \end($split); + + SystemUtils::runProc( + 'convert', + '-units PixelsPerInch ' . $image . ' -resample 300 ' . $temp . '.' . $extension + ); + + $image = $temp . '.' . $extension; + } + + // Do actual parsing SystemUtils::runProc( self::$bin, $image . ' ' @@ -100,12 +120,20 @@ final class TesseractOcr . ' -c preserve_interword_spaces=1' . ' --psm ' . $psm . ' --oem ' . $oem - . ' -l ' . \implode('+', $languages) + . (empty($languages) ? '' : ' -l ' . \implode('+', $languages)) ); } catch (\Throwable $_) { + if (\is_file($temp . '.' . $extension)) { + \unlink($temp . '.' . $extension); + } + return ''; } + if (\is_file($temp . '.' . $extension)) { + \unlink($temp . '.' . $extension); + } + $filepath = \is_file($temp . '.txt') ? $temp . '.txt' : $temp; @@ -120,11 +148,7 @@ final class TesseractOcr $parsed = \file_get_contents($filepath); if ($parsed === false) { - // @codeCoverageIgnoreStart - \unlink($temp); - - return ''; - // @codeCoverageIgnoreEnd + $parsed = ''; } \unlink($filepath); diff --git a/Algorithm/Clustering/AffinityPropagation.php b/Algorithm/Clustering/AffinityPropagation.php index b5ca7250d..0b03a7c4c 100644 --- a/Algorithm/Clustering/AffinityPropagation.php +++ b/Algorithm/Clustering/AffinityPropagation.php @@ -22,9 +22,244 @@ namespace phpOMS\Algorithm\Clustering; * @link https://jingga.app * @see ./clustering_overview.png * @since 1.0.0 - * - * @todo Implement */ -final class AffinityPropagation +final class AffinityPropagation implements ClusteringInterface { + /** + * Points of the cluster centers + * + * @var PointInterface[] + * @since 1.0.0 + */ + private array $clusterCenters = []; + + /** + * Cluster points + * + * Points in clusters (helper to avoid looping the cluster array) + * + * @var array + * @since 1.0.0 + */ + private array $clusters = []; + + private array $similarityMatrix = []; + + private array $responsibilityMatrix = []; + + private array $availabilityMatrix = []; + + /** + * Original points used for clusters + * + * @var PointInterface[] + * @since 1.0.0 + */ + private array $points = []; + + /** + * Create similarity matrix from points + * + * @param PointInterface[] $points Points to create the similarity matrix for + * + * @return array> + * + * @since 1.0.0 + */ + private function createSimilarityMatrix(array $points) : array + { + $n = \count($points); + $coordinates = \count($points[0]->coordinates); + $similarityMatrix = \array_fill(0, $n, []); + + $temp = []; + for ($i = 0; $i < $n - 1; ++$i) { + for ($j = $i + 1; $j < $n; ++$j) { + $sum = 0.0; + for ($c = 0; $c < $coordinates; ++$c) { + $sum += ($points[$i]->getCoordinate($c) - $points[$j]->getCoordinate($c)) * ($points[$i]->getCoordinate($c) - $points[$j]->getCoordinate($c)); + } + + $similarityMatrix[$i][$j] = -$sum; + $similarityMatrix[$j][$i] = -$sum; + $temp[] = $similarityMatrix[$i][$j]; + } + } + + \sort($temp); + + $size = $n * ($n - 1) / 2; + $median = $size % 2 === 0 + ? ($temp[(int) ($size / 2)] + $temp[(int) ($size / 2 - 1)]) / 2 + : $temp[(int) ($size / 2)]; + + for ($i = 0; $i < $n; ++$i) { + $similarityMatrix[$i][$i] = $median; + } + + return $similarityMatrix; + } + + /** + * Generate clusters for points + * + * @param PointInterface[] $points Points to cluster + * @param int $iterations Iterations for cluster generation + * + * @return void + * + * @since 1.0.0 + */ + public function generateClusters(array $points, int $iterations = 100) : void + { + $this->points = $points; + $n = \count($points); + + $this->similarityMatrix = $this->createSimilarityMatrix($points); + $this->responsibilityMatrix = clone $this->similarityMatrix; + $this->availabilityMatrix = clone $this->similarityMatrix; + + for ($c = 0; $c < $iterations; ++$c) { + for ($i = 0; $i < $n; ++$i) { + for ($k = 0; $k < $n; ++$k) { + $max = \PHP_INT_MIN; + for ($j = 0; $j < $k; ++$j) { + if (($temp = $this->similarityMatrix[$i][$j] + $this->availabilityMatrix[$i][$j]) > $max) { + $max = $temp; + } + } + + for ($j = $k + 1; $j < $n; ++$j) { + if (($temp = $this->similarityMatrix[$i][$j] + $this->availabilityMatrix[$i][$j]) > $max) { + $max = $temp; + } + } + + $this->responsibilityMatrix[$i][$k] = (1 - 0.9) * ($this->similarityMatrix[$i][$k] - $max) + 0.9 * $this->responsibilityMatrix[$i][$k]; + } + } + + for ($i = 0; $i < $n; ++$i) { + for ($k = 0; $k < $n; ++$k) { + $sum = 0.0; + + if ($i === $k) { + for ($j = 0; $j < $i; ++$j) { + $sum += \max(0.0, $this->responsibilityMatrix[$j][$k]); + } + + for (++$j; $j < $n; ++$j) { + $sum += \max(0.0, $this->responsibilityMatrix[$j][$k]); + } + + $this->availabilityMatrix[$i][$k] = (1 - 0.9) * $sum + 0.9 * $this->availabilityMatrix[$i][$k]; + } else { + $max = \max($i, $k); + $min = \min($i, $k); + + for ($j = 0; $j < $min; ++$j) { + $sum += \max(0.0, $this->responsibilityMatrix[$j][$k]); + } + + for ($j = $min + 1; $j < $max; ++$j) { + $sum += \max(0.0, $this->responsibilityMatrix[$j][$k]); + } + + for ($j = $max + 1; $j < $n; ++$j) { + $sum += \max(0.0, $this->responsibilityMatrix[$j][$k]); + } + + $this->availabilityMatrix[$i][$k] = (1 - 0.9) * \min(0.0, $this->responsibilityMatrix[$k][$k] + $sum) + 0.9 * $this->availabilityMatrix[$i][$k]; + } + } + } + } + + // find center points (exemplar) + for ($i = 0; $i < $n; ++$i) { + $temp = $this->responsibilityMatrix[$i][$i] + $this->availabilityMatrix[$i][$i]; + + if ($temp > 0) { + $this->clusterCenters[$i] = $this->points[$i]; + } + } + } + + /** + * Find the nearest group for a point + * + * @param array $similarityMatrix Similarity matrix + * @param int $point Point id in the similarity matrix to compare + * + * @return int + * + * @since 1.0.0 + */ + private function findNearestGroup(array $similarityMatrix, int $point) : int + { + $maxSim = \PHP_INT_MIN; + $group = 0; + + foreach ($this->clusterCenters as $c => $_) { + if ($similarityMatrix[$point][$c] > $maxSim) { + $maxSim = $similarityMatrix[$point][$c]; + $group = $c; + } + } + + return $group; + } + + /** + * {@inheritdoc} + */ + public function cluster(PointInterface $point) : ?PointInterface + { + $points = clone $this->points; + $points[] = $point; + + $similarityMatrix = $this->createSimilarityMatrix($points); + + $c = $this->findNearestGroup( + $similarityMatrix, + \count($points) - 1, + ); + + return $this->clusterCenters[$c]; + } + + /** + * {@inheritdoc} + */ + public function getClusters() : array + { + if (!empty($this->clusters)) { + return $this->clusters; + } + + $n = \count($this->points); + for ($i = 0; $i < $n; ++$i) { + $group = $this->findNearestGroup($this->points, $i); + + $this->clusters[$group] = $this->points[$i]; + } + + return $this->clusters; + } + + /** + * {@inheritdoc} + */ + public function getCentroids() : array + { + return $this->clusterCenters; + } + + /** + * {@inheritdoc} + */ + public function getNoise() : array + { + return []; + } } diff --git a/Algorithm/Clustering/AgglomerativeClustering.php b/Algorithm/Clustering/AgglomerativeClustering.php index 3f0ee601e..10002ffdb 100644 --- a/Algorithm/Clustering/AgglomerativeClustering.php +++ b/Algorithm/Clustering/AgglomerativeClustering.php @@ -14,17 +14,109 @@ declare(strict_types=1); namespace phpOMS\Algorithm\Clustering; +use phpOMS\Math\Topology\MetricsND; + /** * Clustering points * + * The parent category of this clustering algorithm is hierarchical clustering. + * * @package phpOMS\Algorithm\Clustering + * @license Base: MIT Copyright (c) 2020 Greene Laboratory * @license OMS License 2.0 * @link https://jingga.app + * @see ./DivisiveClustering.php * @see ./clustering_overview.png + * @see https://en.wikipedia.org/wiki/Hierarchical_clustering + * @see https://github.com/greenelab/hclust/blob/master/README.md * @since 1.0.0 * * @todo Implement + * @todo Implement missing linkage functions */ -final class AgglomerativeClustering +final class AgglomerativeClustering implements ClusteringInterface { + /** + * Metric to calculate the distance between two points + * + * @var \Closure + * @since 1.0.0 + */ + private \Closure $metric; + + /** + * Metric to calculate the distance between two points + * + * @var \Closure + * @since 1.0.0 + */ + private \Closure $linkage; + + /** + * Constructor + * + * @param null|\Closure $metric metric to use for the distance between two points + * + * @since 1.0.0 + */ + public function __construct(?\Closure $metric = null, ?\Closure $linkage = null) + { + $this->metric = $metric ?? function (PointInterface $a, PointInterface $b) { + $aCoordinates = $a->coordinates; + $bCoordinates = $b->coordinates; + + return MetricsND::euclidean($aCoordinates, $bCoordinates); + }; + + $this->linkage = $linkage ?? function (array $a, array $b, array $distances) { + return self::averageDistanceLinkage($a, $b, $distances); + }; + } + + /** + * Maximum/Complete-Linkage clustering + */ + public static function maximumDistanceLinkage(array $setA, array $setB, array $distances) : float + { + $max = \PHP_INT_MIN; + foreach ($setA as $a) { + foreach ($setB as $b) { + if ($distances[$a][$b] > $max) { + $max = $distances[$a][$b]; + } + } + } + + return $max; + } + + /** + * Minimum/Single-Linkage clustering + */ + public static function minimumDistanceLinkage(array $setA, array $setB, array $distances) : float + { + $min = \PHP_INT_MAX; + foreach ($setA as $a) { + foreach ($setB as $b) { + if ($distances[$a][$b] < $min) { + $min = $distances[$a][$b]; + } + } + } + + return $min; + } + + /** + * Unweighted average linkage clustering (UPGMA) + */ + public static function averageDistanceLinkage(array $setA, array $setB, array $distances) : float + { + $distance = 0; + foreach ($setA as $a) { + $distance += \array_sum($distances[$a]); + } + + return $distance / \count($setA) / \count($setB); + } } diff --git a/Algorithm/Clustering/Birch.php b/Algorithm/Clustering/Birch.php index 57ba4522e..a941a91aa 100644 --- a/Algorithm/Clustering/Birch.php +++ b/Algorithm/Clustering/Birch.php @@ -25,6 +25,6 @@ namespace phpOMS\Algorithm\Clustering; * * @todo Implement */ -final class Birch +final class Birch implements ClusteringInterface { } diff --git a/Algorithm/Clustering/ClusteringInterface.php b/Algorithm/Clustering/ClusteringInterface.php new file mode 100644 index 000000000..8da25fb94 --- /dev/null +++ b/Algorithm/Clustering/ClusteringInterface.php @@ -0,0 +1,71 @@ + + * @var array * @since 1.0.0 */ private array $clusters = []; @@ -108,7 +116,7 @@ final class DBSCAN * * @since 1.0.0 */ - public function __construct(\Closure $metric = null) + public function __construct(?\Closure $metric = null) { $this->metric = $metric ?? function (PointInterface $a, PointInterface $b) { $aCoordinates = $a->coordinates; @@ -215,15 +223,9 @@ final class DBSCAN } /** - * Find the cluster for a point - * - * @param PointInterface $point Point to find the cluster for - * - * @return int Cluster id - * - * @since 1.0.0 + * {@inheritdoc} */ - public function cluster(PointInterface $point) : int + public function cluster(PointInterface $point) : ?PointInterface { if ($this->convexHulls === []) { foreach ($this->clusters as $c => $cluster) { @@ -232,18 +234,18 @@ final class DBSCAN $points[] = $p->coordinates; } - // @todo: this is only good for 2D. Fix this for ND. + // @todo this is only good for 2D. Fix this for ND. $this->convexHulls[$c] = MonotoneChain::createConvexHull($points); } } foreach ($this->convexHulls as $c => $hull) { if (Polygon::isPointInPolygon($point->coordinates, $hull) <= 0) { - return $c; + return $hull; } } - return -1; + return null; } /** @@ -282,4 +284,48 @@ final class DBSCAN } } } + + /** + * {@inheritdoc} + */ + public function getCentroids() : array + { + if (!empty($this->clusterCenters)) { + return $this->clusterCenters; + } + + $dim = \count(\reset($this->points)->getCoordinates()); + foreach ($this->clusters as $cluster) { + $middle = \array_fill(0, $dim, 0); + foreach ($cluster as $point) { + for ($i = 0; $i < $dim; ++$i) { + $middle[$i] += $point->getCoordinate($i); + } + } + + for ($i = 0; $i < $dim; ++$i) { + $middle[$i] /= \count($cluster); + } + + $this->clusterCenters = new Point($middle); + } + + return $this->clusterCenters; + } + + /** + * {@inheritdoc} + */ + public function getNoise() : array + { + return $this->noisePoints; + } + + /** + * {@inheritdoc} + */ + public function getClusters() : array + { + return $this->clusters; + } } diff --git a/Algorithm/Clustering/Ward.php b/Algorithm/Clustering/DivisiveClustering.php similarity index 65% rename from Algorithm/Clustering/Ward.php rename to Algorithm/Clustering/DivisiveClustering.php index 6791356a2..aeb7aa0db 100644 --- a/Algorithm/Clustering/Ward.php +++ b/Algorithm/Clustering/DivisiveClustering.php @@ -17,14 +17,18 @@ namespace phpOMS\Algorithm\Clustering; /** * Clustering points * + * The parent category of this clustering algorithm is hierarchical clustering. + * * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app + * @see ./AgglomerativeClustering.php * @see ./clustering_overview.png + * @see https://en.wikipedia.org/wiki/Hierarchical_clustering * @since 1.0.0 * * @todo Implement */ -final class Ward +final class DivisiveClustering implements ClusteringInterface { } diff --git a/Algorithm/Clustering/Kmeans.php b/Algorithm/Clustering/Kmeans.php index 14e8f05dd..f78032e81 100755 --- a/Algorithm/Clustering/Kmeans.php +++ b/Algorithm/Clustering/Kmeans.php @@ -25,7 +25,7 @@ use phpOMS\Math\Topology\MetricsND; * @see ./clustering_overview.png * @since 1.0.0 */ -final class Kmeans +final class Kmeans implements ClusteringInterface { /** * Epsilon for float comparison. @@ -49,7 +49,23 @@ final class Kmeans * @var PointInterface[] * @since 1.0.0 */ - private $clusterCenters = []; + private array $clusterCenters = []; + + /** + * Points of the clusters + * + * @var PointInterface[] + * @since 1.0.0 + */ + private array $clusters = []; + + /** + * Points + * + * @var PointInterface[] + * @since 1.0.0 + */ + private array $points = []; /** * Constructor @@ -58,7 +74,7 @@ final class Kmeans * * @since 1.0.0 */ - public function __construct(\Closure $metric = null) + public function __construct(?\Closure $metric = null) { $this->metric = $metric ?? function (PointInterface $a, PointInterface $b) { $aCoordinates = $a->coordinates; @@ -66,18 +82,10 @@ final class Kmeans return MetricsND::euclidean($aCoordinates, $bCoordinates); }; - - //$this->generateClusters($points, $clusters); } /** - * Find the cluster for a point - * - * @param PointInterface $point Point to find the cluster for - * - * @return null|PointInterface Cluster center point - * - * @since 1.0.0 + * {@inheritdoc} */ public function cluster(PointInterface $point) : ?PointInterface { @@ -95,17 +103,21 @@ final class Kmeans } /** - * Get cluster centroids - * - * @return array - * - * @since 1.0.0 + * {@inheritdoc} */ public function getCentroids() : array { return $this->clusterCenters; } + /** + * {@inheritdoc} + */ + public function getNoise() : array + { + return []; + } + /** * Generate the clusters of the points * @@ -118,6 +130,7 @@ final class Kmeans */ public function generateClusters(array $points, int $clusters) : void { + $this->points = $points; $n = \count($points); $clusterCenters = $this->kpp($points, $clusters); $coordinates = \count($points[0]->coordinates); @@ -215,7 +228,7 @@ final class Kmeans foreach ($points as $key => $point) { $d[$key] = $this->nearestClusterCenter($point, $clusters)[1]; - $sum += $d[$key]; + $sum += $d[$key]; } $sum *= \mt_rand(0, \mt_getrandmax()) / \mt_getrandmax(); @@ -245,4 +258,21 @@ final class Kmeans return $clusters; } + + /** + * {@inheritdoc} + */ + public function getClusters() : array + { + if (!empty($this->clusters)) { + return $this->clusters; + } + + foreach ($this->points as $point) { + $c = $this->cluster($point); + $this->clusters[$c] = $point; + } + + return $this->clusters; + } } diff --git a/Algorithm/Clustering/MeanShift.php b/Algorithm/Clustering/MeanShift.php index 3db459b0c..c549dd267 100644 --- a/Algorithm/Clustering/MeanShift.php +++ b/Algorithm/Clustering/MeanShift.php @@ -25,8 +25,10 @@ use phpOMS\Math\Topology\MetricsND; * @link https://jingga.app * @see ./clustering_overview.png * @since 1.0.0 + * + * @todo Implement noise points */ -final class MeanShift +final class MeanShift implements ClusteringInterface { /** * Min distance for clustering @@ -80,7 +82,7 @@ final class MeanShift * @var PointInterface[] * @since 1.0.0 */ - private $clusterCenters = []; + private array $clusterCenters = []; /** * Max distance to cluster to be still considered part of cluster @@ -100,7 +102,7 @@ final class MeanShift * * @since 1.0.0 */ - public function __construct(\Closure $metric = null, \Closure $kernel = null) + public function __construct(?\Closure $metric = null, ?\Closure $kernel = null) { $this->metric = $metric ?? function (PointInterface $a, PointInterface $b) { $aCoordinates = $a->coordinates; @@ -126,8 +128,9 @@ final class MeanShift */ public function generateClusters(array $points, array $bandwidth) : void { - $shiftPoints = $points; - $maxMinDist = 1; + $this->points = $points; + $shiftPoints = $points; + $maxMinDist = 1; $stillShifting = \array_fill(0, \count($points), true); @@ -292,13 +295,15 @@ final class MeanShift } /** - * Find the cluster for a point - * - * @param PointInterface $point Point to find the cluster for - * - * @return null|PointInterface Cluster center point - * - * @since 1.0.0 + * {@inheritdoc} + */ + public function getCentroids() : array + { + return $this->clusterCenters; + } + + /** + * {@inheritdoc} */ public function cluster(PointInterface $point) : ?PointInterface { @@ -306,4 +311,20 @@ final class MeanShift return $this->clusterCenters[$clusterId] ?? null; } + + /** + * {@inheritdoc} + */ + public function getNoise() : array + { + return $this->noisePoints; + } + + /** + * {@inheritdoc} + */ + public function getClusters() : array + { + return $this->clusters; + } } diff --git a/Algorithm/Clustering/Point.php b/Algorithm/Clustering/Point.php index 0157da8a6..d9c62f2fc 100755 --- a/Algorithm/Clustering/Point.php +++ b/Algorithm/Clustering/Point.php @@ -28,7 +28,7 @@ class Point implements PointInterface * Coordinates of the point * * @var array - * @sicne 1.0.0 + * @since 1.0.0 */ public array $coordinates = []; diff --git a/Algorithm/Clustering/SpectralClustering.php b/Algorithm/Clustering/SpectralClustering.php index 8ad8b6b2d..01ccd36d0 100644 --- a/Algorithm/Clustering/SpectralClustering.php +++ b/Algorithm/Clustering/SpectralClustering.php @@ -25,6 +25,6 @@ namespace phpOMS\Algorithm\Clustering; * * @todo Implement */ -final class SpectralClustering +final class SpectralClustering implements ClusteringInterface { } diff --git a/Algorithm/Frequency/Apriori.php b/Algorithm/Frequency/Apriori.php index 3a4749ab4..33ba7751f 100644 --- a/Algorithm/Frequency/Apriori.php +++ b/Algorithm/Frequency/Apriori.php @@ -4,7 +4,7 @@ * * PHP Version 8.1 * - * @package phpOMS\Algorithm\CoinMatching + * @package phpOMS\Algorithm\Frequency * @copyright Dennis Eichhorn * @license OMS License 2.0 * @version 1.0.0 @@ -12,14 +12,14 @@ */ declare(strict_types=1); -namespace phpOMS\Algorithm\CoinMatching; +namespace phpOMS\Algorithm\Frequency; /** * Apriori algorithm. * - * The algorithm cheks how often a set exists in a given set of sets. + * The algorithm checks how often a set exists in a given set of sets. * - * @package phpOMS\Algorithm\CoinMatching + * @package phpOMS\Algorithm\Frequency * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 @@ -39,7 +39,7 @@ final class Apriori /** * Generate all possible subsets * - * @param array $arr Array of eleements + * @param array $arr Array of elements * * @return array * @@ -70,13 +70,14 @@ final class Apriori * * The algorithm cheks how often a set exists in a given set of sets. * - * @param array $sets Sets of a set (e.g. [[1,2,3,4], [1,2], [1]]) + * @param array $sets Sets of a set (e.g. [[1,2,3,4], [1,2], [1]]) + * @param string[] $subset Subset to check for (empty array -> all subsets are checked) * * @return array * * @since 1.0.0 */ - public static function apriori(array $sets) : array + public static function apriori(array $sets, array $subset = []) : array { // Unique single items $totalSet = []; @@ -90,6 +91,7 @@ final class Apriori $totalSet = \array_unique($totalSet); \sort($totalSet); + \sort($subset); // Combinations of items $combinations = self::generateSubsets($totalSet); @@ -98,10 +100,18 @@ final class Apriori $table = []; foreach ($combinations as &$c) { \sort($c); + if (!empty($subset) && $c !== $subset) { + continue; + } + $table[\implode(':', $c)] = 0; } foreach ($combinations as $combination) { + if (!empty($subset) && $combination !== $subset) { + continue; + } + foreach ($sets as $set) { foreach ($combination as $item) { if (!\in_array($item, $set)) { diff --git a/Algorithm/Graph/DependencyResolver.php b/Algorithm/Graph/DependencyResolver.php index e1662e446..6ca83793e 100755 --- a/Algorithm/Graph/DependencyResolver.php +++ b/Algorithm/Graph/DependencyResolver.php @@ -67,7 +67,7 @@ final class DependencyResolver $unresolved[] = $dependency; self::dependencyResolve($dependency, $items, $resolved, $unresolved); } else { - continue; // circular dependency + return; // circular dependency } } diff --git a/Algorithm/Graph/MarkovChain.php b/Algorithm/Graph/MarkovChain.php index c74eb26ac..5cca43932 100644 --- a/Algorithm/Graph/MarkovChain.php +++ b/Algorithm/Graph/MarkovChain.php @@ -117,7 +117,7 @@ final class MarkovChain * * @since 1.0.0 */ - public function generate(int $length, array $start = null) : array + public function generate(int $length, ?array $start = null) : array { $orderKeys = \array_keys($this->data); $orderValues = \array_keys(\reset($this->data)); @@ -137,16 +137,14 @@ final class MarkovChain $cProb += $p; if ($prob <= $cProb) { - $new = $val; + $new = $val; break; } } // Couldn't find possible key - if ($new === null) { - $new = $orderValues[\array_rand($orderValues)]; - } + $new ??= $orderValues[\array_rand($orderValues)]; $output[] = $new; $key[] = $new; @@ -177,7 +175,7 @@ final class MarkovChain $prob = 1.0; for ($i = $this->order; $i < $length; ++$i) { - $prob *= $this->data[\implode($key)][$path[$i]] ?? 0.0; + $prob *= $this->data[\implode(' ', $key)][$path[$i]] ?? 0.0; $key[] = $path[$i]; \array_shift($key); diff --git a/Algorithm/JobScheduling/Job.php b/Algorithm/JobScheduling/Job.php index 5ccb5af25..caefd9eff 100755 --- a/Algorithm/JobScheduling/Job.php +++ b/Algorithm/JobScheduling/Job.php @@ -57,7 +57,7 @@ class Job implements JobInterface public string $name = ''; /** - * Cosntructor. + * Constructor. * * @param float $value Value of the job * @param \DateTime $start Start time of the job diff --git a/Algorithm/JobScheduling/Weighted.php b/Algorithm/JobScheduling/Weighted.php index f348f6346..d3770ed64 100755 --- a/Algorithm/JobScheduling/Weighted.php +++ b/Algorithm/JobScheduling/Weighted.php @@ -62,7 +62,7 @@ final class Weighted } /** - * Search for a none-conflicting job that comes befor a defined job + * Search for a none-conflicting job that comes before a defined job * * @param JobInterface[] $jobs List of jobs * @param int $pivot Job to find the previous job to @@ -130,7 +130,7 @@ final class Weighted if ($l != -1) { $value += $valueTable[$l]; - $jList = \array_merge($resultTable[$l], $jList); + $jList = \array_merge($resultTable[$l], $jList); } if ($value > $valueTable[$i - 1]) { diff --git a/Algorithm/Knapsack/Backpack.php b/Algorithm/Knapsack/Backpack.php index d0d72e478..1ea278ab9 100755 --- a/Algorithm/Knapsack/Backpack.php +++ b/Algorithm/Knapsack/Backpack.php @@ -106,7 +106,7 @@ class Backpack implements BackpackInterface public function addItem(ItemInterface $item, int | float $quantity = 1) : void { $this->items[] = ['item' => $item, 'quantity' => $quantity]; - $this->value += $item->getValue() * $quantity; - $this->cost += $item->getCost() * $quantity; + $this->value += $item->getValue() * $quantity; + $this->cost += $item->getCost() * $quantity; } } diff --git a/Algorithm/Knapsack/Item.php b/Algorithm/Knapsack/Item.php index 7284916be..cc49c12d1 100755 --- a/Algorithm/Knapsack/Item.php +++ b/Algorithm/Knapsack/Item.php @@ -49,7 +49,7 @@ class Item implements ItemInterface public string $name = ''; /** - * Cosntructor. + * Constructor. * * @param float $value Value of the item * @param float $cost Cost of the item diff --git a/Algorithm/Optimization/GeneticOptimization.php b/Algorithm/Optimization/GeneticOptimization.php index 6c7fdccc1..e8495ddb5 100644 --- a/Algorithm/Optimization/GeneticOptimization.php +++ b/Algorithm/Optimization/GeneticOptimization.php @@ -80,9 +80,9 @@ class GeneticOptimization * * @example See unit test for example use case * - * @param array $population List of all elements with ther parameters (i.e. list of "objects" as arrays). + * @param array $population List of all elements with their parameters (i.e. list of "objects" as arrays). * The constraints are defined as array values. - * @param \Closure $fitness Fitness function calculates score/feasability of solution + * @param \Closure $fitness Fitness function calculates score/feasibility of solution * @param \Closure $mutate Mutation function to change the parameters of an "object" * @param \Closure $crossover Crossover function to exchange parameter values between "objects". * Sometimes single parameters can be exchanged but sometimes interdependencies exist between parameters which is why this function is required. diff --git a/Algorithm/Optimization/SimulatedAnnealing.php b/Algorithm/Optimization/SimulatedAnnealing.php index b0813f20a..184d4bb74 100644 --- a/Algorithm/Optimization/SimulatedAnnealing.php +++ b/Algorithm/Optimization/SimulatedAnnealing.php @@ -40,7 +40,7 @@ class SimulatedAnnealing return $x; } - // can be many things, e.g. swapping parameters, increasing/decrising, random generation + // can be many things, e.g. swapping parameters, increasing/decreasing, random generation public static function neighbor(array $generation, $parameterCount) { $newGeneration = $generation; @@ -57,17 +57,17 @@ class SimulatedAnnealing */ // Simulated Annealing algorithm - // @todo allow to create a solution space (currently all soluctions need to be in space) - // @todo: currently only replacing generations, not altering them + // @todo allow to create a solution space (currently all solutions need to be in space) + // @todo currently only replacing generations, not altering them /** * Perform optimization * * @example See unit test for example use case * - * @param array $space List of all elements with ther parameters (i.e. list of "objects" as arrays). + * @param array $space List of all elements with their parameters (i.e. list of "objects" as arrays). * The constraints are defined as array values. * @param int $initialTemperature Starting temperature - * @param \Closure $costFunction Fitness function calculates score/feasability of solution + * @param \Closure $costFunction Fitness function calculates score/feasibility of solution * @param \Closure $neighbor Neighbor function to find a new solution/neighbor * @param float $coolingRate Rate at which cooling takes place * @param int $iterations Number of iterations diff --git a/Algorithm/PathFinding/Path.php b/Algorithm/PathFinding/Path.php index fbd6b3f3a..80f0bc8d1 100755 --- a/Algorithm/PathFinding/Path.php +++ b/Algorithm/PathFinding/Path.php @@ -54,7 +54,7 @@ class Path private array $expandedNodes = []; /** - * Cosntructor. + * Constructor. * * @param Grid $grid Grid this path belongs to * @@ -152,7 +152,7 @@ class Path } /** - * Find nodes in bettween two nodes. + * Find nodes in between two nodes. * * The path may only contain the jump points or pivot points. * In order to get every node it needs to be expanded. @@ -190,7 +190,7 @@ class Path if ($e2 > -$dy) { $err -= $dy; - $x0 += $sx; + $x0 += $sx; } if ($e2 < $dx) { diff --git a/Algorithm/Rating/Elo.php b/Algorithm/Rating/Elo.php index 098fca94b..b16d9d3e6 100644 --- a/Algorithm/Rating/Elo.php +++ b/Algorithm/Rating/Elo.php @@ -74,4 +74,37 @@ final class Elo 'elo' => (int) \max($eloNew, $this->MIN_ELO), ]; } + + /** + * Calculate an approximated win probability based on elo points. + * + * @param int $elo1 Elo of the player we want to calculate the win probability for + * @param int $elo2 Opponent elo + * @param bool $canDraw Is a draw possible? + * + * @return float + * + * @since 1.0.0 + */ + public function winProbability(int $elo1, int $elo2, bool $canDraw = false) : float + { + return $canDraw + ? -1.0 // @todo implement + : 1 / (1 + \pow(10, ($elo2 - $elo1) / 400)); + } + + /** + * Calculate an approximated draw probability based on elo points. + * + * @param int $elo1 Elo of the player we want to calculate the win probability for + * @param int $elo2 Opponent elo + * + * @return float + * + * @since 1.0.0 + */ + public function drawProbability(int $elo1, int $elo2) : float + { + return -1.0; // @todo implement + } } diff --git a/Algorithm/Rating/Glicko2.php b/Algorithm/Rating/Glicko2.php index 8b19528ff..171ee9687 100644 --- a/Algorithm/Rating/Glicko2.php +++ b/Algorithm/Rating/Glicko2.php @@ -119,7 +119,7 @@ final class Glicko2 // Step 0: $rdOld /= self::Q; - $elo = ($elo - $this->DEFAULT_ELO) / self::Q; + $elo = ($elo - $this->DEFAULT_ELO) / self::Q; foreach ($oElo as $idx => $value) { $oElo[$idx] = ($value - $this->DEFAULT_ELO) / self::Q; diff --git a/Algorithm/Rating/TrueSkill.php b/Algorithm/Rating/TrueSkill.php index 661b17aa1..e695262dc 100644 --- a/Algorithm/Rating/TrueSkill.php +++ b/Algorithm/Rating/TrueSkill.php @@ -25,22 +25,67 @@ use phpOMS\Math\Stochastic\Distribution\NormalDistribution; * @since 1.0.0 * @see https://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf * - * @todo implement https://github.com/sublee/trueskill/blob/master/trueskill/__init__.py + * @todo Implement https://github.com/sublee/trueskill/blob/master/trueskill/__init__.py + * https://github.com/Karaka-Management/phpOMS/issues/337 */ class TrueSkill { - public int $DEFAULT_MU = 25; + public const DEFAULT_MU = 25; - public float $DEFAULT_SIGMA = 25 / 3; + public const DEFAULT_SIGMA = 25 / 3; - public float $DEFAULT_BETA = 25 / 3 / 2; + public const DEFAULT_BETA = 25 / 3 / 2; - public float $DEFAULT_TAU = 25 / 3 / 100; + public const DEFAULT_TAU = 25 / 3 / 100; - public float $DEFAULT_DRAW_PROBABILITY = 0.1; + public const DEFAULT_DRAW_PROBABILITY = 0.1; - public function __construct() + private float $mu = 0.0; + + private float $sigma = 0.0; + + private float $beta = 0.0; + + private float $tau = 0.0; + + private float $drawProbability = 0.0; + + public function __construct( + ?float $mu = null, + ?float $sigma = null, + ?float $beta = null, + ?float $tau = null, + ?float $drawProbability = null) { + $this->mu = $mu ?? self::DEFAULT_MU; + $this->sigma = $sigma ?? self::DEFAULT_SIGMA; + $this->beta = $beta ?? self::DEFAULT_BETA; + $this->tau = $tau ?? self::DEFAULT_TAU; + $this->drawProbability = $drawProbability ?? self::DEFAULT_DRAW_PROBABILITY; + } + + public function winProbability(array $team1, array $team2, float $drawMargin = 0.0) + { + $sigmaSum = 0.0; + $mu1 = 0.0; + foreach ($team1 as $player) { + $mu1 += $player->mu; + $sigmaSum += $player->sigma * $player->sigma; + } + + $mu2 = 0.0; + foreach ($team2 as $player) { + $mu2 += $player->mu; + $sigmaSum += $player->sigma * $player->sigma; + } + + $deltaMu = $mu1 - $mu2; + + return NormalDistribution::getCdf( + ($deltaMu - $drawMargin) / \sqrt((\count($team1) + \count($team2)) * ($this->beta * $this->beta) + $sigmaSum), + 0, + 1 + ); } // Draw margin = epsilon diff --git a/Algorithm/Rating/TrueSkillFactoryGraph.php b/Algorithm/Rating/TrueSkillFactoryGraph.php new file mode 100644 index 000000000..e69de29bb diff --git a/Api/Geocoding/Nominatim.php b/Api/Geocoding/Nominatim.php index ab05fe186..37bc72481 100644 --- a/Api/Geocoding/Nominatim.php +++ b/Api/Geocoding/Nominatim.php @@ -34,13 +34,16 @@ final class Nominatim /** * {@inheritdoc} */ - public static function geocoding(string $country, string $city, string $address = '') : array + public static function geocoding(string $country, string $city, string $address = '', string $postal = '') : array { $URL = 'https://nominatim.openstreetmap.org/search.php?format=jsonv2'; $request = new HttpRequest( new HttpUri( - $URL . '&country=' . \urlencode($country) . '&city=' . \urlencode($city) . ($address === '' ? '' : '&street=' . \urlencode($address)) + $URL . '&country=' . \urlencode($country) + . '&city=' . \urlencode($city) + . ($address === '' ? '' : '&street=' . \urlencode($address)) + . ($postal === '' ? '' : '&postalcode=' . \urlencode($postal)) ) ); $request->setMethod(RequestMethod::GET); diff --git a/Api/Shipping/AuthStatus.php b/Api/Shipping/AuthStatus.php new file mode 100644 index 000000000..83f422e19 --- /dev/null +++ b/Api/Shipping/AuthStatus.php @@ -0,0 +1,36 @@ +expire = new \DateTime('now'); + $this->refreshExpire = new \DateTime('now'); + } + + /** + * {@inheritdoc} + */ + public function authLogin( + string $login, string $password, + ?string $client = null, + ?string $payload = null + ) : int + { + $this->client = $client ?? $this->client; + $this->login = $login; + $this->password = $password; + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/' . self::API_VERSION . '/auth/accesstoken'; + + $request = new HttpRequest(new HttpUri($uri)); + $request->setMethod(RequestMethod::GET); + + $request->header->set('Content-Type', MimeType::M_JSON); + $request->header->set('Accept', '*/*'); + $request->header->set('Authorization', 'Basic ' . \base64_encode($login . ':' . $password)); + + $this->expire = new \DateTime('now'); + + $response = Rest::request($request); + + switch ($response->header->status) { + case 400: + case 401: + $status = AuthStatus::FAILED; + break; + case 403: + $status = AuthStatus::BLOCKED; + break; + case 429: + $status = AuthStatus::LIMIT_EXCEEDED; + break; + case 200: + $this->token = $response->getDataString('access_token') ?? ''; + $this->expire->setTimestamp($this->expire->getTimestamp() + ((int) $response->getData('expires_in'))); + + $status = AuthStatus::OK; + break; + default: + $status = AuthStatus::FAILED; + } + + return $status; + } + + /** + * {@inheritdoc} + */ + public function authRedirectLogin( + string $client, + ?string $redirect = null, + array $payload = [] + ) : HttpRequest + { + return new HttpRequest(); + } + + /** + * {@inheritdoc} + */ + public function tokenFromRedirect( + string $login, string $password, + HttpRequest $redirect + ) : int + { + return AuthStatus::FAILED; + } + + /** + * {@inheritdoc} + */ + public function refreshToken() : int + { + return AuthStatus::FAILED; + } + + /** + * {@inheritdoc} + */ + public function authApiKey(string $key) : int + { + return AuthStatus::FAILED; + } + + /** + * {@inheritdoc} + */ + public function ship( + array $sender, + array $shipFrom, + array $receiver, + array $package, + array $data + ) : array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function cancel(string $shipment, array $packages = []) : bool + { + return false; + } + + /** + * {@inheritdoc} + */ + public function track(string $shipment) : array + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX2_URL; + $uri = $base . '/track/shipments'; + + $httpUri = new HttpUri($uri); + $httpUri->addQuery('trackingnumber', $shipment); + $httpUri->addQuery('limit', '10'); + + // @todo implement: express, parcel-de, ecommerce, dgf, parcel-uk, post-de, sameday, freight, parcel-nl, parcel-pl, dsc, ecommerce-europe, svb + //$httpUri->addQuery('service', ''); + + // @odo: implement + //$httpUri->addQuery('requesterCountryCode', ''); + //$httpUri->addQuery('originCountryCode', ''); + //$httpUri->addQuery('recipientPostalCode', ''); + + $request = new HttpRequest($httpUri); + + $request->setMethod(RequestMethod::GET); + $request->header->set('accept', MimeType::M_JSON); + $request->header->set('dhl-api-key', $this->apiKey); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + $shipments = $response->getDataArray('shipments') ?? []; + $tracking = []; + + // @todo add general shipment status (not just for individual packages) + + foreach ($shipments as $shipment) { + $packages = []; + $package = $shipment; + + $activities = []; + foreach ($package['events'] as $activity) { + $activities[] = [ + 'date' => new \DateTime($activity['timestamp']), + 'description' => $activity['description'], + 'location' => [ + 'address' => [ + $activity['location']['address']['streetAddress'], + $activity['location']['address']['addressLocality'], + ], + 'city' => '', + 'country' => '', + 'country_code' => $activity['location']['address']['countryCode'], + 'zip' => $activity['location']['address']['postalCode'], + 'state' => '', + ], + 'status' => [ + 'code' => $activity['statusCode'], + 'statusCode' => $activity['statusCode'], + 'description' => $activity['status'], + ], + ]; + } + + $packages[] = [ + 'status' => [ + 'code' => $package['status']['statusCode'], + 'statusCode' => $package['status']['statusCode'], + 'description' => $package['status']['status'], + ], + 'deliveryDate' => new \DateTime($package['estimatedTimeOfDelivery']), + 'count' => $package['details']['totalNumberOfPieces'], + 'weight' => $package['details']['weight']['weight'], + 'weight_unit' => 'g', + 'activities' => $activities, + 'received' => [ + 'by' => $package['details']['proofOfDelivery']['familyName'], + 'signature' => $package['details']['proofOfDelivery']['signatureUrl'], + 'location' => '', + 'date' => $package['details']['proofOfDelivery']['timestamp'], + ], + ]; + + $tracking[] = $packages; + } + + return $tracking; + } + + /** + * Get label information for shipment + * + * @param string $shipment Shipment id or token + * + * @return array + * + * @since 1.0.0 + */ + public function label(string $shipment) : array + { + return []; + } + + /** + * Finalize shipments (no further changes possible) + * + * @param string[] $shipment Shipments to finalize + * + * @return bool + * + * @since 1.0.0 + */ + public function finalize(array $shipment = []) : bool + { + return true; + } +} diff --git a/Api/Shipping/DHL/DHLParcelDEShipping.php b/Api/Shipping/DHL/DHLParcelDEShipping.php new file mode 100644 index 000000000..23a636fe3 --- /dev/null +++ b/Api/Shipping/DHL/DHLParcelDEShipping.php @@ -0,0 +1,659 @@ +expire = new \DateTime('now'); + $this->refreshExpire = new \DateTime('now'); + } + + /** + * {@inheritdoc} + */ + public function authLogin( + string $login, string $password, + ?string $client = null, + ?string $payload = null + ) : int + { + $this->apiKey = $client ?? $this->client; + $this->login = $login; + $this->password = $password; + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/parcel/de/shipping/' . self::API_VERSION; + + $request = new HttpRequest(new HttpUri($uri)); + $request->setMethod(RequestMethod::GET); + $request->header->set('Authorization', 'Basic ' . \base64_encode($this->login . ':' . $this->password)); + $request->header->set('dhl-api-key', $this->apiKey); + + $this->expire = new \DateTime('now'); + + $response = Rest::request($request); + + switch ($response->header->status) { + case 400: + case 500: + $status = AuthStatus::FAILED; + break; + case 403: + $status = AuthStatus::BLOCKED; + break; + case 429: + $status = AuthStatus::LIMIT_EXCEEDED; + break; + case 200: + $status = AuthStatus::OK; + break; + default: + $status = AuthStatus::FAILED; + } + + return $status; + } + + /** + * {@inheritdoc} + */ + public function authRedirectLogin( + string $client, + ?string $redirect = null, + array $payload = [] + ) : HttpRequest + { + return new HttpRequest(); + } + + /** + * {@inheritdoc} + */ + public function tokenFromRedirect( + string $login, string $password, + HttpRequest $redirect + ) : int + { + return AuthStatus::FAILED; + } + + /** + * {@inheritdoc} + */ + public function refreshToken() : int + { + return AuthStatus::FAILED; + } + + /** + * {@inheritdoc} + */ + public function authApiKey(string $key) : int + { + $this->apiKey = $key; + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/parcel/de/shipping/' . self::API_VERSION; + + $request = new HttpRequest(new HttpUri($uri)); + $request->setMethod(RequestMethod::GET); + $request->header->set('Accept', MimeType::M_JSON); + $request->header->set('dhl-api-key', $key); + + $response = Rest::request($request); + + switch ($response->header->status) { + case 400: + case 500: + $status = AuthStatus::FAILED; + break; + case 403: + $status = AuthStatus::BLOCKED; + break; + case 429: + $status = AuthStatus::LIMIT_EXCEEDED; + break; + case 200: + $status = AuthStatus::OK; + break; + default: + $status = AuthStatus::FAILED; + } + + return $status; + } + + /** + * {@inheritdoc} + */ + public function ship( + array $sender, + array $shipFrom, + array $receiver, + array $package, + array $data + ) : array + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/parcel/de/shipping/' . self::API_VERSION .'/orders'; + + $httpUri = new HttpUri($uri); + $httpUri->addQuery('validate', 'true'); + + // @todo implement docFormat + $httpUri->addQuery('docFormat', 'PDF'); + + // @todo implement printFormat + // Available values : A4, 910-300-600, 910-300-610, 910-300-700, 910-300-700-oz, 910-300-710, 910-300-300, 910-300-300-oz, 910-300-400, 910-300-410, 100x70mm + // If not set, default specified in customer portal will be used + // @todo implement as class setting + //$request->setData('printFormat', ''); + + $request = new HttpRequest($httpUri); + $request->setMethod(RequestMethod::POST); + $request->header->set('Content-Type', MimeType::M_JSON); + $request->header->set('Accept-Language', 'en-US'); + $request->header->set('Authorization', 'Basic ' . \base64_encode($this->login . ':' . $this->password)); + + $request->setData('STANDARD_GRUPPENPROFIL', 'PDF'); + + $shipments = [ + [ + 'product' => 'V01PAK', // V53WPAK, V53WPAK + 'billingNumber' => $data['costcenter'], // @todo maybe dhl number, check + 'refNo' => $package['id'], + 'shipper' => [ + 'name1' => $sender['name'], + 'addressStreet' => $sender['address'], + 'additionalAddressInformation1' => $sender['address_addition'], + 'postalCode' => $sender['zip'], + 'city' => $sender['city'], + 'country' => ISO3166CharEnum::getBy2Code($sender['country_code']), + 'email' => $sender['email'], + 'phone' => $sender['phone'], + ], + 'consignee' => [ + 'name1' => $receiver['name'], + 'addressStreet' => $receiver['address'], + 'additionalAddressInformation1' => $receiver['address_addition'], + 'postalCode' => $receiver['zip'], + 'city' => $receiver['city'], + 'country' => ISO3166CharEnum::getBy2Code($receiver['country_code']), + 'email' => $receiver['email'], + 'phone' => $receiver['phone'], + ], + 'details' => [ + 'dim' => [ + 'uom' => 'mm', + 'height' => $package['height'], + 'length' => $package['length'], + 'width' => $package['width'], + ], + 'weight' => [ + 'uom' => 'g', + 'value' => $package['weight'], + ], + ], + ], + ]; + + $request->setData('shipments', $shipments); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + $result = $response->getDataArray('items') ?? []; + + $labelUri = new HttpUri($result[0]['label']['url']); + $label = $this->label($labelUri->getQuery('token')); + + return [ + 'id' => $result[0]['shipmentNo'], + 'label' => [ + 'code' => $result[0]['label']['format'], + 'url' => $result[0]['label']['url'], + 'data' => $label['data'], + ], + 'packages' => [ + 'id' => $result[0]['shipmentNo'], + 'label' => [ + 'code' => $result[0]['label']['format'], + 'url' => $result[0]['label']['url'], + 'data' => $label['data'], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function cancel(string $shipment, array $packages = []) : bool + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/parcel/de/shipping/' . self::API_VERSION .'/orders'; + + $request = new HttpRequest(new HttpUri($uri)); + $request->setMethod(RequestMethod::DELETE); + $request->header->set('Accept-Language', 'en-US'); + $request->header->set('Authorization', 'Basic ' . \base64_encode($this->login . ':' . $this->password)); + + $request->setData('profile', 'STANDARD_GRUPPENPROFIL'); + $request->setData('shipment', $shipment); + + $response = Rest::request($request); + + return $response->header->status === 200; + } + + /** + * Get shipment information (no tracking) + * + * This includes depending on service labels, shipping documents and general shipment information. + * For some services this function simply re-creates the data from ship(). + * + * @param string $shipment Shipment id or token + * + * @return array + * + * @since 1.0.0 + */ + public function info(string $shipment) : array + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/parcel/de/shipping/' . self::API_VERSION .'/orders'; + + $httpUri = new HttpUri($uri); + $httpUri->addQuery('shipment', $shipment); + + // @todo implement docFormat etc + $httpUri->addQuery('docFormat', 'PDF'); + + $request = new HttpRequest($httpUri); + $request->setMethod(RequestMethod::GET); + $request->header->set('Accept-Language', 'en-US'); + $request->header->set('Authorization', 'Basic ' . \base64_encode($this->login . ':' . $this->password)); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + $result = $response->getDataArray('items') ?? []; + + $labelUri = new HttpUri($result[0]['label']['url']); + $label = $this->label($labelUri->getQuery('token')); + + return [ + 'id' => $result[0]['shipmentNo'], + 'label' => [ + 'code' => $result[0]['label']['format'], + 'url' => $result[0]['label']['url'], + 'data' => $label['data'], + ], + 'packages' => [ + 'id' => $result[0]['shipmentNo'], + 'label' => [ + 'code' => $result[0]['label']['format'], + 'url' => $result[0]['label']['url'], + 'data' => $label['data'], + ], + ], + ]; + } + + /** + * Get label information for shipment + * + * @param string $shipment Shipment id or token + * + * @return array + * + * @since 1.0.0 + */ + public function label(string $shipment) : array + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/parcel/de/shipping/' . self::API_VERSION .'/labels'; + + $httpUri = new HttpUri($uri); + $httpUri->addQuery('token', $shipment); + + $request = new HttpRequest($httpUri); + $request->setMethod(RequestMethod::GET); + $request->header->set('Content-Type', MimeType::M_PDF); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + return [ + 'data' => $response->getData(), + ]; + } + + /** + * {@inheritdoc} + */ + public function track(string $shipment) : array + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX2_URL; + $uri = $base . '/track/shipments'; + + $httpUri = new HttpUri($uri); + $httpUri->addQuery('trackingnumber', $shipment); + $httpUri->addQuery('limit', '10'); + + // @todo implement: express, parcel-de, ecommerce, dgf, parcel-uk, post-de, sameday, freight, parcel-nl, parcel-pl, dsc, ecommerce-europe, svb + //$httpUri->addQuery('service', ''); + + // @odo: implement + //$httpUri->addQuery('requesterCountryCode', ''); + //$httpUri->addQuery('originCountryCode', ''); + //$httpUri->addQuery('recipientPostalCode', ''); + + $request = new HttpRequest($httpUri); + + $request->setMethod(RequestMethod::GET); + $request->header->set('accept', MimeType::M_JSON); + $request->header->set('dhl-api-key', $this->apiKey); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + $shipments = $response->getDataArray('shipments') ?? []; + $tracking = []; + + // @todo add general shipment status (not just for individual packages) + + foreach ($shipments as $shipment) { + $packages = []; + $package = $shipment; + + $activities = []; + foreach ($package['events'] as $activity) { + $activities[] = [ + 'date' => new \DateTime($activity['timestamp']), + 'description' => $activity['description'], + 'location' => [ + 'address' => [ + $activity['location']['address']['streetAddress'], + $activity['location']['address']['addressLocality'], + ], + 'city' => '', + 'country' => '', + 'country_code' => $activity['location']['address']['countryCode'], + 'zip' => $activity['location']['address']['postalCode'], + 'state' => '', + ], + 'status' => [ + 'code' => $activity['statusCode'], + 'statusCode' => $activity['statusCode'], + 'description' => $activity['status'], + ], + ]; + } + + $packages[] = [ + 'status' => [ + 'code' => $package['status']['statusCode'], + 'statusCode' => $package['status']['statusCode'], + 'description' => $package['status']['status'], + ], + 'deliveryDate' => new \DateTime($package['estimatedTimeOfDelivery']), + 'count' => $package['details']['totalNumberOfPieces'], + 'weight' => $package['details']['weight']['weight'], + 'weight_unit' => 'g', + 'activities' => $activities, + 'received' => [ + 'by' => $package['details']['proofOfDelivery']['familyName'], + 'signature' => $package['details']['proofOfDelivery']['signatureUrl'], + 'location' => '', + 'date' => $package['details']['proofOfDelivery']['timestamp'], + ], + ]; + + $tracking[] = $packages; + } + + return $tracking; + } + + /** + * Get daily manifest + * + * @param \DateTime $date Date of the manifest + * + * @return array + * + * @since 1.0.0 + */ + public function getManifest(?\DateTime $date = null) : array + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/parcel/de/shipping/' . self::API_VERSION .'/manifest'; + + $httpUri = new HttpUri($uri); + if ($date !== null) { + $httpUri->addQuery('date', $date->format('Y-m-d')); + } + + $request = new HttpRequest($httpUri); + $request->setMethod(RequestMethod::GET); + $request->header->set('Accept-Language', 'en-US'); + $request->header->set('Authorization', 'Basic ' . \base64_encode($this->login . ':' . $this->password)); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + return [ + 'date' => $response->getDataDateTime('manifestDate'), + 'b64' => $response->getDataArray('manifest')['b64'], + 'zpl2' => $response->getDataArray('manifest')['zpl2'], + 'url' => $response->getDataArray('manifest')['url'], + 'format' => $response->getDataArray('manifest')['printFormat'], + ]; + } + + /** + * Finalize shipments. + * + * No further adjustments are possible. + * + * @param array $shipment Shipments to finalize. If empty = all + * + * @return bool + * + * @since 1.0.0 + */ + public function finalize(array $shipment = []) : bool + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/parcel/de/shipping/' . self::API_VERSION .'/manifest'; + + $httpUri = new HttpUri($uri); + $httpUri->addQuery('all', empty($shipment) ? 'true' : 'false'); + + $request = new HttpRequest($httpUri); + $request->setMethod(RequestMethod::POST); + $request->header->set('Content-Type', MimeType::M_JSON); + $request->header->set('Authorization', 'Basic ' . \base64_encode($this->login . ':' . $this->password)); + + if (!empty($shipment)) { + $request->setData('shipmentNumbers', $shipment); + } + + $response = Rest::request($request); + + return $response->header->status === 200; + } +} diff --git a/Api/Shipping/DHL/DHLeCommerceShipping.php b/Api/Shipping/DHL/DHLeCommerceShipping.php new file mode 100644 index 000000000..bc3ef10e5 --- /dev/null +++ b/Api/Shipping/DHL/DHLeCommerceShipping.php @@ -0,0 +1,114 @@ +uri")) + * + * @see authLogin() for services that require login+password + * @see authApiKey() for services that require api key + * + * @since 1.0.0 + */ + public function authRedirectLogin( + string $client, + ?string $redirect = null, + array $payload = [] + ) : HttpRequest; + + /** + * Parses the redirect code after using authRedirectLogin() and creates a token from that code. + * + * @param string $login Login name/email + * @param string $password Password + * @param HttpRequest $redirect redirect request after the user successfully logged in + * + * @return int Returns auth status + * + * @see authRedirectLogin() + * + * @since 1.0.0 + */ + public function tokenFromRedirect( + string $login, string $password, + HttpRequest $redirect + ) : int; + + /** + * Connect to API + * + * @param string $key Api key/permanent token + * + * @return int Returns auth status + * + * @since 1.0.0 + */ + public function authApiKey(string $key) : int; + + /** + * Refreshes token using a refresh token + * + * @return int Returns auth status + * + * @since 1.0.0 + */ + public function refreshToken() : int; + + /** + * Create shipment. + * + * @param array $sender Sender + * @param array $shipFrom Ship from location (sometimes sender != pickup location) + * @param array $receiver Receiver + * @param array $package Package + * @param array $data Shipping data + * + * @return array + * + * @since 1.0.0 + */ + public function ship( + array $sender, + array $shipFrom, + array $receiver, + array $package, + array $data + ) : array; + + /** + * Cancel shipment. + * + * @param string $shipment Shipment id + * @param string[] $packages Packed ids (if a shipment consists of multiple packages) + * + * @return bool + * + * @since 1.0.0 + */ + public function cancel(string $shipment, array $packages = []) : bool; + + /** + * Track shipment. + * + * @param string $shipment Shipment id + * + * @return array + * + * @since 1.0.0 + */ + public function track(string $shipment) : array; +} diff --git a/Api/Shipping/ShippingType.php b/Api/Shipping/ShippingType.php new file mode 100644 index 000000000..3c4596998 --- /dev/null +++ b/Api/Shipping/ShippingType.php @@ -0,0 +1,42 @@ +expire = new \DateTime('now'); + $this->refreshExpire = new \DateTime('now'); + } + + /** + * {@inheritdoc} + */ + public function authLogin( + string $login, string $password, + ?string $client = null, + ?string $payload = null + ) : int + { + $this->client = $client ?? $this->client; + $this->login = $login; + $this->password = $password; + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/security/' . self::API_VERSION . '/oauth/token'; + + $request = new HttpRequest(new HttpUri($uri)); + $request->setMethod(RequestMethod::POST); + $request->setData('grant_type', 'client_credentials'); + $request->header->set('Content-Type', MimeType::M_POST); + + if ($client !== null) { + $request->header->set('x-merchant-id', $client); + } + + $request->header->set('Authorization', 'Basic ' . \base64_encode($login . ':' . $password)); + + $this->expire = new \DateTime('now'); + + $response = Rest::request($request); + + switch ($response->header->status) { + case 400: + case 401: + $status = AuthStatus::FAILED; + break; + case 403: + $status = AuthStatus::BLOCKED; + break; + case 429: + $status = AuthStatus::LIMIT_EXCEEDED; + break; + case 200: + $this->token = $response->getDataString('access_token') ?? ''; + $this->expire->setTimestamp($this->expire->getTimestamp() + ((int) $response->getData('expires_in'))); + + $status = AuthStatus::OK; + break; + default: + $status = AuthStatus::FAILED; + } + + return $status; + } + + /** + * {@inheritdoc} + */ + public function authRedirectLogin( + string $client, + ?string $redirect = null, + array $payload = [] + ) : HttpRequest + { + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/security/' . self::API_VERSION . '/oauth/authorize'; + + $request = new HttpRequest(new HttpUri($uri)); + + $request->setMethod(RequestMethod::GET); + $request->setData('client_id', $client); + $request->setData('redirect_uri', $redirect); + $request->setData('response_type', 'code'); + + if (isset($payload['id'])) { + $request->setData('scope', $payload['id']); + } + + return $request; + } + + /** + * {@inheritdoc} + */ + public function tokenFromRedirect( + string $login, string $password, + HttpRequest $redirect + ) : int + { + $code = $redirect->getData('code') ?? ''; + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/security/' . self::API_VERSION . '/oauth/token'; + + $request = new HttpRequest(new HttpUri($uri)); + $request->setMethod(RequestMethod::POST); + + // @remark: One api documentation part says redirect_uri is required another says it's not required + // Personally I don't see why a redirect is required or even helpful. Will try without it! + $request->setData('grant_type', 'authorization_code'); + $request->setData('code', $code); + $request->header->set('Content-Type', MimeType::M_POST); + $request->header->set('Authorization', 'Basic ' . \base64_encode($login . ':' . $password)); + + $this->expire = new \DateTime('now'); + $this->refreshExpire = new \DateTime('now'); + + $response = Rest::request($request); + + switch ($response->header->status) { + case 400: + case 401: + $status = AuthStatus::FAILED; + break; + case 403: + $status = AuthStatus::BLOCKED; + break; + case 429: + $status = AuthStatus::LIMIT_EXCEEDED; + break; + case 200: + $this->token = $response->getDataString('access_token') ?? ''; + $this->refreshToken = $response->getDataString('refresh_token') ?? ''; + + $this->expire->setTimestamp($this->expire->getTimestamp() + ((int) $response->getData('expires_in'))); + $this->refreshExpire->setTimestamp($this->refreshExpire->getTimestamp() + ((int) $response->getData('refresh_token_expires_in'))); + + $status = AuthStatus::OK; + break; + default: + $status = AuthStatus::FAILED; + } + + return $status; + } + + /** + * {@inheritdoc} + */ + public function refreshToken() : int + { + $now = new \DateTime('now'); + if ($this->refreshExpire->getTimestamp() < $now->getTimestamp() - self::TIME_DELTA) { + return AuthStatus::FAILED; + } + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/security/' . self::API_VERSION . '/oauth/refresh'; + + $request = new HttpRequest(new HttpUri($uri)); + + $request->setMethod(RequestMethod::POST); + $request->header->set('Content-Type', MimeType::M_POST); + $request->header->set('Authorization', 'Basic ' . \base64_encode($this->login . ':' . $this->password)); + + $request->setData('grant_type', 'refresh_token'); + $request->setData('refresh_token', $this->refreshToken); + + $this->expire = clone $now; + $this->refreshExpire = clone $now; + + $response = Rest::request($request); + + switch ($response->header->status) { + case 400: + case 401: + $status = AuthStatus::FAILED; + break; + case 403: + $status = AuthStatus::BLOCKED; + break; + case 429: + $status = AuthStatus::LIMIT_EXCEEDED; + break; + case 200: + $this->token = $response->getDataString('access_token') ?? ''; + $this->refreshToken = $response->getDataString('refresh_token') ?? ''; + + $this->expire->setTimestamp($this->expire->getTimestamp() + ((int) $response->getData('expires_in'))); + $this->refreshExpire->setTimestamp($this->refreshExpire->getTimestamp() + ((int) $response->getData('refresh_token_expires_in'))); + + $status = AuthStatus::OK; + break; + default: + $status = AuthStatus::FAILED; + } + + return $status; + } + + /** + * {@inheritdoc} + */ + public function authApiKey(string $key) : int + { + return AuthStatus::FAILED; + } + + /** + * {@inheritdoc} + */ + public function timeInTransit(array $shipFrom, array $receiver, array $package, \DateTime $shipDate) : array + { + if (!$this->validateOrReconnectAuth()) { + return []; + } + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/api/shipments/' . self::API_VERSION . '/transittimes'; + + $request = new HttpRequest(new HttpUri($uri)); + + $request->setMethod(RequestMethod::POST); + $request->header->set('Content-Type', MimeType::M_JSON); + $request->header->set('Authorization', 'Bearer ' . $this->token); + $request->header->set('transId', ((string) \microtime(true)) . '-' . \bin2hex(\random_bytes(6))); + $request->header->set('transactionSrc', 'jingga'); + + $request->setData('originCountryCode', $shipFrom['country_code']); + $request->setData('originStateProvince', \substr($shipFrom['state'], 0, 50)); + $request->setData('originCityName', \substr($shipFrom['city'], 0, 50)); + $request->setData('originPostalCode', \substr($shipFrom['zip'], 0, 10)); + + $request->setData('destinationCountryCode', $receiver['country_code']); + $request->setData('destinationStateProvince', \substr($receiver['state'], 0, 50)); + $request->setData('destinationCityName', \substr($receiver['city'], 0, 50)); + $request->setData('destinationPostalCode', \substr($receiver['zip'], 0, 10)); + $request->setData('avvFlag', true); + + $request->setData('billType', $package['type']); + $request->setData('weight', $package['weight']); + $request->setData('weightUnitOfMeasure', $package['weight_unit']); // LBS or KGS + $request->setData('shipmentContentsValue', $package['value']); + $request->setData('shipmentContentsCurrencyCode', $package['currency']); // 3 char ISO code + $request->setData('numberOfPackages', $package['count']); + + $request->setData('shipDate', $shipDate->format('Y-m-d')); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + $services = $response->getDataArray('services') ?? []; + $transits = []; + + foreach ($services as $service) { + $transits[] = [ + 'serviceLevel' => $service['serviceLevel'], + 'deliveryDate' => new \DateTime($service['deliveryDaye']), + 'deliveryDateFrom' => null, + 'deliveryDateTo' => null, + ]; + } + + return $transits; + } + + /** + * {@inheritdoc} + */ + public function ship(array $sender, array $shipFrom, array $receiver, array $package, array $data) : array + { + if (!$this->validateOrReconnectAuth()) { + return []; + } + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/api/shipments/' . self::API_VERSION . '/ship'; + + $request = new HttpRequest(new HttpUri($uri)); + + $request->setMethod(RequestMethod::POST); + $request->header->set('Authorization', 'Bearer ' . $this->token); + $request->header->set('transId', ((string) \microtime(true)) . '-' . \bin2hex(\random_bytes(6))); + $request->header->set('transactionSrc', 'jingga'); + + // @todo dangerous goods + // @todo implement printing standard (pdf-zpl/format and size) + + $body = [ + 'Request' => [ + 'RequestOption' => 'validate', + 'SubVersion' => '2205', + ], + 'Shipment' => [ + 'Description' => $package['description'], + 'DocumentsOnlyIndicator' => '0', + 'Shipper' => [ + 'Name' => \substr($sender['name'], 0, 35), + 'AttentionName' => \substr($sender['fao'], 0, 35), + 'CompanyDisplayableName' => \substr($sender['name'], 0, 35), + 'TaxIdentificationNumber' => \substr($sender['taxid'], 0, 15), + 'Phone' => [ + 'Number' => \substr($sender['phone'], 0, 15), + ], + 'ShipperNumber' => $sender['number'], + 'EMailAddress' => \substr($sender['email'], 0, 50), + 'Address' => [ + 'AddressLine' => \substr($sender['address'], 0, 35), + 'City' => \substr($sender['city'], 0, 30), + 'StateProvinceCode' => \substr($sender['state'], 0, 5), + 'PostalCode' => \substr($sender['zip'], 0, 9), + 'CountryCode' => $sender['country_code'], + ], + ], + 'ShipTo' => [ + 'Name' => \substr($receiver['name'], 0, 35), + 'AttentionName' => \substr($receiver['fao'], 0, 35), + 'CompanyDisplayableName' => \substr($receiver['name'], 0, 35), + 'TaxIdentificationNumber' => \substr($receiver['taxid'], 0, 15), + 'Phone' => [ + 'Number' => \substr($receiver['phone'], 0, 15), + ], + 'ShipperNumber' => $receiver['number'], + 'EMailAddress' => \substr($receiver['email'], 0, 50), + 'Address' => [ + 'AddressLine' => \substr($receiver['address'], 0, 35), + 'City' => \substr($receiver['city'], 0, 30), + 'StateProvinceCode' => \substr($receiver['state'], 0, 5), + 'PostalCode' => \substr($receiver['zip'], 0, 9), + 'CountryCode' => $receiver['country_code'], + ], + ], + /* @todo only allowed for US -> US and PR -> PR shipments? + 'ReferenceNumber' => [ + 'BarCodeIndicator' => '1', + 'Code' => '', + 'Value' => '', + ], + */ + 'Service' => [ + 'Code' => $data['service_code'], + 'Description' => \substr($data['service_description'], 0, 35), + ], + 'InvoiceLineTotal' => [ + 'CurrencyCode' => $package['currency'], + 'MonetaryValue' => $package['value'], + ], + 'NumOfPiecesInShipment' => $package['count'], + 'CostCenter' => \substr($package['costcenter'], 0, 30), + 'PackageID' => \substr($package['id'], 0, 30), + 'PackageIDBarcodeIndicator' => '1', + 'Package' => [], + ], + 'LabelSpecification' => [ + 'LabelImageFormat' => [ + 'Code' => $data['label_code'], + 'Description' => \substr($data['label_description'], 0, 35), + ], + 'LabelStockSize' => [ + 'Height' => $data['label_height'], + 'Width' => $data['label_width'], + ], + ], + 'ReceiptSpecification' => [ + 'ImageFormat' => [ + 'Code' => $data['receipt_code'], + 'Description' => \substr($data['receipt_description'], 0, 35), + ], + ], + ]; + + $packages = []; + foreach ($package['packages'] as $p) { + $packages[] = [ + 'Description' => \substr($p['description'], 0, 35), + 'Packaging' => [ + 'Code' => $p['package_code'], + 'Description' => $p['package_description'], + ], + 'Dimensions' => [ + 'UnitOfMeasurement' => [ + 'Code' => $p['package_dim_unit'], // IN or CM or 00 or 01 + 'Description' => \substr($p['package_dim_unit_description'], 0, 35), + ], + 'Length' => $p['length'], + 'Width' => $p['width'], + 'Height' => $p['height'], + ], + 'DimWeight' => [ + 'UnitOfMeasurement' => [ + 'Code' => $p['package_dimweight_unit'], + 'Description' => \substr($p['package_dimweight_unit_description'], 0, 35), + ], + 'Weight' => $p['weight'], + ], + 'PackageWeight' => [ + 'UnitOfMeasurement' => [ + 'Code' => $p['package_weight_unit'], + 'Description' => \substr($p['package_weight_unit_description'], 0, 35), + ], + 'Weight' => $p['weight'], + ], + + ]; + } + + $body['Shipment']['Package'] = $packages; + + // Only required if shipper != shipFrom (e.g. pickup location != shipper) + if (!empty($shipFrom)) { + $body['Shipment']['ShipFrom'] = [ + 'Name' => \substr($shipFrom['name'], 0, 35), + 'AttentionName' => \substr($shipFrom['fao'], 0, 35), + 'CompanyDisplayableName' => \substr($shipFrom['name'], 0, 35), + 'TaxIdentificationNumber' => \substr($shipFrom['taxid'], 0, 15), + 'Phone' => [ + 'Number' => \substr($shipFrom['phone'], 0, 15), + ], + 'ShipperNumber' => $shipFrom['number'], + 'EMailAddress' => \substr($shipFrom['email'], 0, 50), + 'Address' => [ + 'AddressLine' => \substr($shipFrom['address'], 0, 35), + 'City' => \substr($shipFrom['city'], 0, 30), + 'StateProvinceCode' => \substr($shipFrom['state'], 0, 5), + 'PostalCode' => \substr($shipFrom['zip'], 0, 9), + 'CountryCode' => $shipFrom['country_code'], + ], + ]; + } + + $request->setData('ShipmentRequest', $body); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + $result = $response->getDataArray('ShipmentResponse') ?? []; + + $shipment = [ + 'id' => $result['ShipmentResults']['ShipmentIdentificationNumber'] ?? '', + 'costs' => [ + 'service' => $result['ShipmentResults']['ShipmentCharges']['BaseServiceCharge']['MonetaryValue'] ?? null, + 'transportation' => $result['ShipmentResults']['ShipmentCharges']['TransportationCharges']['MonetaryValue'] ?? null, + 'options' => $result['ShipmentResults']['ShipmentCharges']['ServiceOptionsCharges']['MonetaryValue'] ?? null, + 'subtotal' => $result['ShipmentResults']['ShipmentCharges']['TotalCharges']['MonetaryValue'] ?? null, + 'taxes' => $result['ShipmentResults']['ShipmentCharges']['TaxCharges']['MonetaryValue'] ?? null, + 'taxes_type' => $result['ShipmentResults']['ShipmentCharges']['TaxCharges']['Type'] ?? null, + 'total' => $result['ShipmentResults']['ShipmentCharges']['TotalChargesWithTaxes']['MonetaryValue'] ?? null, + 'currency' => $result['ShipmentResults']['ShipmentCharges']['TotalCharges']['CurrencyCode'] ?? null, + ], + 'packages' => [], + 'label' => [ + 'code' => '', + 'url' => $result['ShipmentResults']['LabelURL'] ?? '', + 'barcode' => $result['ShipmentResults']['BarCodeImage'] ?? '', + 'local' => $result['ShipmentResults']['LocalLanguageLabelURL'] ?? '', + 'data' => '', + ], + 'receipt' => [ + 'code' => '', + 'url' => $result['ShipmentResults']['ReceiptURL'] ?? '', + 'local' => $result['ShipmentResults']['LocalLanguageReceiptURL'] ?? '', + 'data' => '', + ], + // @todo dangerous goods paper image + ]; + + $packages = []; + foreach ($result['ShipmentResults']['Packages'] as $package) { + $packages[] = [ + 'id' => $package['TrackingNumber'], + 'label' => [ + 'code' => $package['ShippingLabel']['ImageFormat']['Code'], + 'url' => '', + 'barcode' => $package['PDF417'], + 'image' => $package['ShippingLabel']['GraphicImage'], + 'browser' => $package['HTMLImage'], + 'data' => '', + ], + 'receipt' => [ + 'code' => $package['ShippingReceipt']['ImageFormat']['Code'], + 'image' => $package['ShippingReceipt']['ImageFormat']['GraphicImage'], + ], + ]; + } + + $shipment['packages'] = $packages; + + return $shipment; + } + + /** + * {@inheritdoc} + */ + public function cancel(string $shipment, array $packages = []) : bool + { + if (!$this->validateOrReconnectAuth()) { + return false; + } + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/api/shipments/' . self::API_VERSION . '/void/cancel/' . $shipment; + + $request = new HttpRequest(new HttpUri($uri)); + + $request->setMethod(RequestMethod::DELETE); + $request->header->set('Authorization', 'Bearer ' . $this->token); + $request->header->set('transId', ((string) \microtime(true)) . '-' . \bin2hex(\random_bytes(6))); + $request->header->set('transactionSrc', 'jingga'); + + $request->setData('trackingnumber', empty($shipment) ? $shipment : \implode(',', $packages)); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return false; + } + + return ($response->getDataArray('VoidShipmentResponse')['Response']['ResponseStatus']['Code'] ?? '0') === '1'; + } + + /** + * {@inheritdoc} + */ + public function track(string $shipment) : array + { + if (!$this->validateOrReconnectAuth()) { + return []; + } + + $base = self::$ENV === 'live' ? self::LIVE_URL : self::SANDBOX_URL; + $uri = $base . '/api/track/v1/details/' . $shipment; + + $request = new HttpRequest(new HttpUri($uri)); + + $request->setMethod(RequestMethod::GET); + $request->header->set('Authorization', 'Bearer ' . $this->token); + $request->header->set('transId', ((string) \microtime(true)) . '-' . \bin2hex(\random_bytes(6))); + $request->header->set('transactionSrc', 'jingga'); + + $request->setData('locale', 'en_US'); + $request->setData('returnSignature', 'false'); + + $response = Rest::request($request); + if ($response->header->status !== 200) { + return []; + } + + $shipments = $response->getDataArray('trackResponse') ?? []; + $shipments = $shipments['shipment'] ?? []; + + $tracking = []; + + // @todo add general shipment status (not just for individual packages) + + foreach ($shipments as $shipment) { + $packages = []; + foreach ($shipment['package'] as $package) { + $activities = []; + foreach ($package['activity'] as $activity) { + $activities[] = [ + 'date' => new \DateTime($activity['date'] . ' ' . $activity['time']), + 'description' => '', + 'location' => [ + 'address' => [ + $activity['location']['address']['addressLine1'], + $activity['location']['address']['addressLine2'], + $activity['location']['address']['addressLine3'], + ], + 'city' => $activity['location']['address']['city'], + 'country' => $activity['location']['address']['country'], + 'country_code' => $activity['location']['address']['country_code'], + 'zip' => $activity['location']['address']['postalCode'], + 'state' => $activity['location']['address']['stateProvice'], + ], + 'status' => [ + 'code' => $activity['status']['code'], + 'statusCode' => $activity['status']['statusCode'], + 'description' => $activity['status']['description'], + ], + ]; + } + + $packages[] = [ + 'status' => [ + 'code' => $package['status']['code'], + 'statusCode' => $package['status']['statusCode'], + 'description' => $package['status']['description'], + ], + 'deliveryDate' => new \DateTime($package['deliveryDate'] . ' ' . $package['deliveryTime']['endTime']), + 'count' => $package['packageCount'], + 'weight' => $package['weight']['weight'], + 'weight_unit' => $package['weight']['unitOfMeasurement'], + 'activities' => $activities, + 'received' => [ + 'by' => $package['deliveryInformation']['receivedBy'], + 'signature' => $package['deliveryInformation']['signature'], + 'location' => $package['deliveryInformation']['location'], + 'date' => '', + ], + ]; + } + + $tracking[] = $packages; + } + + return $tracking; + } + + /** + * Validates the current authentication and tries to reconnect if the connection timed out + * + * @return bool + * + * @since 1.0.0 + */ + private function validateOrReconnectAuth() : bool + { + $status = AuthStatus::OK; + $now = new \DateTime('now'); + + if ($this->expire->getTimestamp() < $now->getTimestamp() - self::TIME_DELTA) { + $status = AuthStatus::FAILED; + + if ($this->refreshToken !== '') { + $status = $this->refreshToken(); + } elseif ($this->login !== '' && $this->password !== '') { + $status = $this->authLogin($this->login, $this->password, $this->client); + } + } + + return $status === AuthStatus::OK + && $this->expire->getTimestamp() > $now->getTimestamp() - self::TIME_DELTA; + } +} diff --git a/Api/Shipping/Usps/UspsShipping.php b/Api/Shipping/Usps/UspsShipping.php new file mode 100644 index 000000000..455c44e17 --- /dev/null +++ b/Api/Shipping/Usps/UspsShipping.php @@ -0,0 +1,112 @@ + $path) { - if (\str_starts_with($class, $map)) { - include_once $path . $class . '.php'; - - return; - } - } - } - */ } foreach (self::$paths as $path) { diff --git a/Business/Finance/FinanceFormulas.php b/Business/Finance/FinanceFormulas.php index b3638498e..e1559078c 100755 --- a/Business/Finance/FinanceFormulas.php +++ b/Business/Finance/FinanceFormulas.php @@ -278,7 +278,7 @@ final class FinanceFormulas * * @since 1.0.0 */ - public static function getAnnutiyPaymentFactorPV(float $r, int $n) : float + public static function getAnnuityPaymentFactorPV(float $r, int $n) : float { return $r / (1 - \pow(1 + $r, -$n)); } @@ -561,7 +561,7 @@ final class FinanceFormulas * * @since 1.0.0 */ - public static function getPrincipalOfCompundInterest(float $C, float $r, int $n) : float + public static function getPrincipalOfCompoundInterest(float $C, float $r, int $n) : float { return $C / (\pow(1 + $r, $n) - 1); } @@ -577,7 +577,7 @@ final class FinanceFormulas * * @since 1.0.0 */ - public static function getPeriodsOfCompundInterest(float $P, float $C, float $r) : float + public static function getPeriodsOfCompoundInterest(float $P, float $C, float $r) : float { return \log($C / $P + 1) / \log(1 + $r); } diff --git a/Business/Marketing/Metrics.php b/Business/Marketing/Metrics.php index f7e2269ab..d42fb077f 100755 --- a/Business/Marketing/Metrics.php +++ b/Business/Marketing/Metrics.php @@ -137,7 +137,7 @@ final class Metrics } /** - * Calculate the profitability of customers based on their purchase behaviour + * Calculate the profitability of customers based on their purchase behavior * * The basis for the calculation is the migration model using a markov chain * diff --git a/Business/Marketing/NetPromoterScore.php b/Business/Marketing/NetPromoterScore.php index 2157b7799..660111cc7 100755 --- a/Business/Marketing/NetPromoterScore.php +++ b/Business/Marketing/NetPromoterScore.php @@ -127,7 +127,7 @@ final class NetPromoterScore /** * Count promoters * - * Promotoers are all ratings larger 8 + * Promoters are all ratings larger 8 * * @return int Returns the amount of promoters (>= 0) * diff --git a/Business/Marketing/PageRank.php b/Business/Marketing/PageRank.php index 7e0757650..641649b8a 100755 --- a/Business/Marketing/PageRank.php +++ b/Business/Marketing/PageRank.php @@ -106,7 +106,7 @@ final class PageRank * * @since 1.0.0 */ - public function calculateRanks(int $iterations = 20, array $startRank = null) : array + public function calculateRanks(int $iterations = 20, ?array $startRank = null) : array { if ($startRank !== null) { $this->pageRanks = $startRank; diff --git a/Business/Recommendation/ArticleCorrelationAffinity.php b/Business/Recommendation/ArticleCorrelationAffinity.php index b3163add2..10340beef 100644 --- a/Business/Recommendation/ArticleCorrelationAffinity.php +++ b/Business/Recommendation/ArticleCorrelationAffinity.php @@ -37,7 +37,7 @@ final class ArticleCorrelationAffinity private array $affinity = []; /** - * Item order behaviour (when are which items ordered) + * Item order behavior (when are which items ordered) * * In tearms of the pearson correlation these are our random variables * diff --git a/Business/Recommendation/BayesianPersonalizedRanking.php b/Business/Recommendation/BayesianPersonalizedRanking.php index 93d5e6d33..95e7a9894 100644 --- a/Business/Recommendation/BayesianPersonalizedRanking.php +++ b/Business/Recommendation/BayesianPersonalizedRanking.php @@ -39,7 +39,7 @@ final class BayesianPersonalizedRanking // num_factors determines the dimensionality of the latent factor space. // learning_rate controls the step size for updating the latent factors during optimization. - // regularization prevents overfitting by adding a penalty for large parameter values. + // regularization prevents over-fitting by adding a penalty for large parameter values. public function __construct(int $numFactors, float $learningRate, float $regularization) { $this->numFactors = $numFactors; diff --git a/Business/Recommendation/MemoryCF.php b/Business/Recommendation/MemoryCF.php index d66be710e..972511543 100644 --- a/Business/Recommendation/MemoryCF.php +++ b/Business/Recommendation/MemoryCF.php @@ -87,7 +87,7 @@ final class MemoryCF * * @since 1.0.0 */ - public function euclideanDistance(array $ranking, array $rankings) : array + private function euclideanDistance(array $ranking, array $rankings) : array { $distances = []; foreach ($rankings as $idx => $r) { @@ -107,7 +107,7 @@ final class MemoryCF * * @since 1.0.0 */ - public function cosineDistance(array $ranking, array $rankings) : array + private function cosineDistance(array $ranking, array $rankings) : array { $distances = []; foreach ($rankings as $idx => $r) { @@ -189,11 +189,9 @@ final class MemoryCF $anglePointer = \array_keys($cosine); // Inspect items of the top n comparable users - for ($i = 1; $i <= $size; ++$i) { - $index = (int) ($i / 2) - 1; - - $uId = $i % 2 === 1 ? $distancePointer[$index] : $anglePointer[$index]; - $distances = $i % 2 === 1 ? $euclidean : $cosine; + for ($i = 0; $i < $size; ++$i) { + $uId = $i % 2 === 0 ? $distancePointer[$i] : $anglePointer[$i]; + $distances = $i % 2 === 0 ? $euclidean : $cosine; foreach ($this->rankings[$uId] as $iId => $_) { // Item is not already in dataset and not in historic dataset (we are only interested in new) diff --git a/Business/Recommendation/ModelCF.php b/Business/Recommendation/ModelCF.php index 9f9cda6f2..8bdbf36e9 100644 --- a/Business/Recommendation/ModelCF.php +++ b/Business/Recommendation/ModelCF.php @@ -49,15 +49,30 @@ final class ModelCF * the multiplication gives a score of how much the user may like that movie. * A segnificant amount of attributes are required to calculate a good match * - * @param Matrix $users A mxa matrix where each "m" defines how much the user likes a certain attribute type and "a" defines different users - * @param Matrix $items A bxm matrix where each "b" defines a item and "m" defines how much it belongs to a certain attribute type + * @param array> $users A mxa matrix where each "m" defines how much the user likes a certain attribute type and "a" defines different users + * @param array> $items A bxm matrix where each "b" defines a item and "m" defines how much it belongs to a certain attribute type * * @return array * * @since 1.0.0 */ - public static function score(Matrix $users, Matrix $items) : array + public static function score(array $users, array $items) : array { - return $users->mult($items)->getMatrix(); + $matrix = []; + + foreach ($users as $uid => $userrow) { + foreach ($items as $iid => $itemrow) { + $matrix[$uid][$iid] = 0.0; + + $userrow = \array_values($userrow); + $itemrow = \array_values($itemrow); + + foreach ($userrow as $idx => $user) { + $matrix[$uid][$iid] += $user * $itemrow[$idx]; + } + } + } + + return $matrix; } } diff --git a/Business/Sales/MarketShareEstimation.php b/Business/Sales/MarketShareEstimation.php index 150b6986b..84bef5fe0 100755 --- a/Business/Sales/MarketShareEstimation.php +++ b/Business/Sales/MarketShareEstimation.php @@ -18,7 +18,7 @@ namespace phpOMS\Business\Sales; * Market share calculations (Zipf function) * * This class can be used to calculate the market share based on a rank or vice versa - * the rank based on a marketshare in a Zipf distributed market. + * the rank based on a market share in a Zipf distributed market. * * @package phpOMS\Business\Sales * @license OMS License 2.0 diff --git a/Business/Warehouse/OrderSuggestion.php b/Business/Warehouse/OrderSuggestion.php new file mode 100644 index 000000000..62dcadeeb --- /dev/null +++ b/Business/Warehouse/OrderSuggestion.php @@ -0,0 +1,44 @@ + cssOMS/styles.css +``` + +For JS you may also use the shorthand command `npm run build`. + Code changes must be performed in a new branch. A new branch can be created with: ```sh @@ -75,8 +127,70 @@ git checkout -b new-branch-name The name of the branch can be chosen freely however it is recommended to follow the following branch naming conventions: * `feature-*` for feature implementations +* `hotfix-*` for security related fixes/improvements * `bug-*` for bug fixes * `security-*` for security related fixes/improvements -* `general-*` for general improvements (i.e. code documentation improvements, code style improvements) +* `general-*` for general improvements (i.e. documentation, code style & performance improvements) -The senior developer who performs the code review merges the change request into the `develop` branch upon approval. \ No newline at end of file +```mermaid +%%{init: { 'gitGraph': {'mainBranchName': 'master'}} }%% + gitGraph + commit + branch hotfix-xxx + commit + checkout master + branch develop + checkout master + merge hotfix-xxx + checkout develop + branch bug-xxx + commit + commit + checkout hotfix-xxx + commit + checkout master + merge hotfix-xxx + checkout develop + merge bug-xxx + commit + checkout develop + branch feature-xxx + commit + commit + commit + checkout develop + merge feature-xxx + checkout master + merge develop + checkout develop + branch general-xxx + commit + checkout develop + merge general-xxx + branch security-xxx + commit + commit + checkout develop + merge security-xxx + checkout master + merge develop + +``` + +The senior developer who performs the code review merges the change request into the `develop` branch after their successful code review. Unsuccessful reviews lead to change requests by the original developer, other developers who can make the requested changes, changes by the senior developer who performed the review, or dismissal of the changed code. (**R10**) + +## Approved dependencies + +### Customer dependencies + +Developers may only rely on the dependencies defined in [Approved Customer Software]() when developing a solution. If new software should be added to this list or a different version is required developers should make a request with their team leader/head of department who forwards this requests if appropriate to the CTO and explain the reasoning for the different dependency needs. The CTO can decide if the dependency will be accepted. (**R11**) + +### Developer dependencies + +Developers may only rely on the dependencies defined in [IT Equipment & Software](). If new software should be added to this list or a different version is required developers should make a request with their team leader/head of department who forwards this requests if appropriate to the CTO and explain the reasoning for the different dependency needs. The CTO can decide if the dependency will be accepted. Changing the package managers such as `composer.json` or `package.json` is not allowed by anyone else than the CTO. (**R12**) + +## Other related documents + +* [Confidentiality Policy](../Policies%20&%20Guidelines/Confidentiality%20Policy.md) +* [Organization Activity Policy](../Policies%20&%20Guidelines/Organization%20Activity%20Policy.md) +* [Tutorials](./Development/Tutorials) \ No newline at end of file diff --git a/Config/SettingsInterface.php b/Config/SettingsInterface.php index 0e2b32536..d5c66caad 100755 --- a/Config/SettingsInterface.php +++ b/Config/SettingsInterface.php @@ -45,12 +45,12 @@ interface SettingsInterface extends OptionsInterface */ public function get( mixed $ids = null, - string | array $names = null, - int $unit = null, - int $app = null, - string $module = null, - int $group = null, - int $account = null + string | array|null $names = null, + ?int $unit = null, + ?int $app = null, + ?string $module = null, + ?int $group = null, + ?int $account = null ) : mixed; /** diff --git a/Contract/StreamInterface.php b/Contract/StreamInterface.php index 3233d2e33..21a64771a 100755 --- a/Contract/StreamInterface.php +++ b/Contract/StreamInterface.php @@ -51,7 +51,7 @@ interface StreamInterface * * @since 1.0.0 */ - public function getMetaData(string $key = null); + public function getMetaData(?string $key = null); /** * Get the stream resource @@ -72,7 +72,7 @@ interface StreamInterface * * @since 1.0.0 */ - public function setStream($stream, int $size = null) : self; + public function setStream($stream, ?int $size = null) : self; /** * Detach the current stream resource @@ -264,7 +264,7 @@ interface StreamInterface * * @since 1.0.0 */ - public function readLine(int $maxLength = null) : ?string; + public function readLine(?int $maxLength = null) : ?string; /** * Set custom data on the stream diff --git a/DataStorage/Cache/Connection/FileCache.php b/DataStorage/Cache/Connection/FileCache.php index e5c210295..1c4394631 100755 --- a/DataStorage/Cache/Connection/FileCache.php +++ b/DataStorage/Cache/Connection/FileCache.php @@ -94,7 +94,7 @@ final class FileCache extends ConnectionAbstract * * @since 1.0.0 */ - public function connect(array $data = null) : void + public function connect(?array $data = null) : void { $this->dbdata = $data; diff --git a/DataStorage/Cache/Connection/MemCached.php b/DataStorage/Cache/Connection/MemCached.php index 5051eafb6..16a5e3051 100755 --- a/DataStorage/Cache/Connection/MemCached.php +++ b/DataStorage/Cache/Connection/MemCached.php @@ -74,7 +74,7 @@ final class MemCached extends ConnectionAbstract * * @since 1.0.0 */ - public function connect(array $data = null) : void + public function connect(?array $data = null) : void { $this->dbdata = isset($data) ? $data : $this->dbdata; diff --git a/DataStorage/Cache/Connection/NullCache.php b/DataStorage/Cache/Connection/NullCache.php index f185cb45c..17900cb46 100755 --- a/DataStorage/Cache/Connection/NullCache.php +++ b/DataStorage/Cache/Connection/NullCache.php @@ -27,7 +27,7 @@ final class NullCache extends ConnectionAbstract /** * {@inheritdoc} */ - public function connect(array $data = null) : void + public function connect(?array $data = null) : void { } diff --git a/DataStorage/Cache/Connection/RedisCache.php b/DataStorage/Cache/Connection/RedisCache.php index 6d5f66f10..2fad593b0 100755 --- a/DataStorage/Cache/Connection/RedisCache.php +++ b/DataStorage/Cache/Connection/RedisCache.php @@ -66,7 +66,7 @@ final class RedisCache extends ConnectionAbstract * * @since 1.0.0 */ - public function connect(array $data = null) : void + public function connect(?array $data = null) : void { $this->dbdata = isset($data) ? $data : $this->dbdata; diff --git a/DataStorage/Cache/Exception/InvalidConnectionConfigException.php b/DataStorage/Cache/Exception/InvalidConnectionConfigException.php index 6de40a997..109b47a15 100755 --- a/DataStorage/Cache/Exception/InvalidConnectionConfigException.php +++ b/DataStorage/Cache/Exception/InvalidConnectionConfigException.php @@ -33,7 +33,7 @@ final class InvalidConnectionConfigException extends \InvalidArgumentException * * @since 1.0.0 */ - public function __construct(string $message = '', int $code = 0, \Exception $previous = null) + public function __construct(string $message = '', int $code = 0, ?\Exception $previous = null) { parent::__construct('Invalid/missing config value for "' . $message . '".', $code, $previous); } diff --git a/DataStorage/Cookie/CookieJar.php b/DataStorage/Cookie/CookieJar.php index 96998654a..2e0f022a1 100755 --- a/DataStorage/Cookie/CookieJar.php +++ b/DataStorage/Cookie/CookieJar.php @@ -99,7 +99,7 @@ final class CookieJar mixed $value, int $expire = 86400, string $path = '/', - string $domain = null, + ?string $domain = null, bool $secure = false, bool $httpOnly = true, bool $overwrite = true diff --git a/DataStorage/DataStorageConnectionInterface.php b/DataStorage/DataStorageConnectionInterface.php index ef821fe67..4fa4d10ea 100755 --- a/DataStorage/DataStorageConnectionInterface.php +++ b/DataStorage/DataStorageConnectionInterface.php @@ -35,7 +35,7 @@ interface DataStorageConnectionInterface * * @since 1.0.0 */ - public function connect(array $data = null) : void; + public function connect(?array $data = null) : void; /** * Get the datastorage type. diff --git a/DataStorage/Database/BuilderAbstract.php b/DataStorage/Database/BuilderAbstract.php index e793d55d5..34a120f64 100755 --- a/DataStorage/Database/BuilderAbstract.php +++ b/DataStorage/Database/BuilderAbstract.php @@ -35,14 +35,6 @@ abstract class BuilderAbstract */ protected bool $isReadOnly = false; - /** - * Grammar. - * - * @var GrammarAbstract - * @since 1.0.0 - */ - protected GrammarAbstract $grammar; - /** * Database connection. * diff --git a/DataStorage/Database/Connection/ConnectionAbstract.php b/DataStorage/Database/Connection/ConnectionAbstract.php index 10a53f8e3..e28f57c44 100755 --- a/DataStorage/Database/Connection/ConnectionAbstract.php +++ b/DataStorage/Database/Connection/ConnectionAbstract.php @@ -161,7 +161,7 @@ abstract class ConnectionAbstract implements ConnectionInterface * * @since 1.0.0 */ - abstract public function connect(array $dbdata = null) : void; + abstract public function connect(?array $dbdata = null) : void; /** * Object destructor. diff --git a/DataStorage/Database/Connection/MysqlConnection.php b/DataStorage/Database/Connection/MysqlConnection.php index 7788b320e..a7671ccdc 100755 --- a/DataStorage/Database/Connection/MysqlConnection.php +++ b/DataStorage/Database/Connection/MysqlConnection.php @@ -58,7 +58,7 @@ final class MysqlConnection extends ConnectionAbstract /** * {@inheritdoc} */ - public function connect(array $dbdata = null) : void + public function connect(?array $dbdata = null) : void { if ($this->status === DatabaseStatus::OK) { return; diff --git a/DataStorage/Database/Connection/NullConnection.php b/DataStorage/Database/Connection/NullConnection.php index dc69cb11a..6194a1671 100755 --- a/DataStorage/Database/Connection/NullConnection.php +++ b/DataStorage/Database/Connection/NullConnection.php @@ -27,7 +27,7 @@ final class NullConnection extends ConnectionAbstract /** * {@inheritdoc} */ - public function connect(array $dbdata = null) : void + public function connect(?array $dbdata = null) : void { } diff --git a/DataStorage/Database/Connection/PostgresConnection.php b/DataStorage/Database/Connection/PostgresConnection.php index a04a93bfe..5167db1fb 100755 --- a/DataStorage/Database/Connection/PostgresConnection.php +++ b/DataStorage/Database/Connection/PostgresConnection.php @@ -58,7 +58,7 @@ final class PostgresConnection extends ConnectionAbstract /** * {@inheritdoc} */ - public function connect(array $dbdata = null) : void + public function connect(?array $dbdata = null) : void { if ($this->status === DatabaseStatus::OK) { return; diff --git a/DataStorage/Database/Connection/SQLiteConnection.php b/DataStorage/Database/Connection/SQLiteConnection.php index 0fa2354c5..7a731e129 100755 --- a/DataStorage/Database/Connection/SQLiteConnection.php +++ b/DataStorage/Database/Connection/SQLiteConnection.php @@ -66,7 +66,7 @@ final class SQLiteConnection extends ConnectionAbstract * * @since 1.0.0 */ - public function connect(array $dbdata = null) : void + public function connect(?array $dbdata = null) : void { if ($this->status === DatabaseStatus::OK) { return; diff --git a/DataStorage/Database/Connection/SqlServerConnection.php b/DataStorage/Database/Connection/SqlServerConnection.php index f76a92173..8a172c17b 100755 --- a/DataStorage/Database/Connection/SqlServerConnection.php +++ b/DataStorage/Database/Connection/SqlServerConnection.php @@ -58,7 +58,7 @@ final class SqlServerConnection extends ConnectionAbstract /** * {@inheritdoc} */ - public function connect(array $dbdata = null) : void + public function connect(?array $dbdata = null) : void { if ($this->status === DatabaseStatus::OK) { return; diff --git a/DataStorage/Database/Exception/InvalidConnectionConfigException.php b/DataStorage/Database/Exception/InvalidConnectionConfigException.php index 40413a2ae..633e6e0b5 100755 --- a/DataStorage/Database/Exception/InvalidConnectionConfigException.php +++ b/DataStorage/Database/Exception/InvalidConnectionConfigException.php @@ -33,7 +33,7 @@ final class InvalidConnectionConfigException extends \InvalidArgumentException * * @since 1.0.0 */ - public function __construct(string $message = '', int $code = 0, \Exception $previous = null) + public function __construct(string $message = '', int $code = 0, ?\Exception $previous = null) { parent::__construct('Missing config value for "' . $message . '".', $code, $previous); } diff --git a/DataStorage/Database/Exception/InvalidDatabaseTypeException.php b/DataStorage/Database/Exception/InvalidDatabaseTypeException.php index 53644eae1..d5cdb235b 100755 --- a/DataStorage/Database/Exception/InvalidDatabaseTypeException.php +++ b/DataStorage/Database/Exception/InvalidDatabaseTypeException.php @@ -33,7 +33,7 @@ final class InvalidDatabaseTypeException extends \InvalidArgumentException * * @since 1.0.0 */ - public function __construct(string $message = '', int $code = 0, \Exception $previous = null) + public function __construct(string $message = '', int $code = 0, ?\Exception $previous = null) { parent::__construct('Invalid database type "' . $message . '".', $code, $previous); } diff --git a/DataStorage/Database/Exception/InvalidMapperException.php b/DataStorage/Database/Exception/InvalidMapperException.php index 6ac6ec381..5f086372e 100755 --- a/DataStorage/Database/Exception/InvalidMapperException.php +++ b/DataStorage/Database/Exception/InvalidMapperException.php @@ -33,7 +33,7 @@ final class InvalidMapperException extends \RuntimeException * * @since 1.0.0 */ - public function __construct(string $message = '', int $code = 0, \Exception $previous = null) + public function __construct(string $message = '', int $code = 0, ?\Exception $previous = null) { if ($message === '') { parent::__construct('Empty mapper.', $code, $previous); diff --git a/DataStorage/Database/GrammarAbstract.php b/DataStorage/Database/GrammarAbstract.php index 4ca66d527..5fcd1891a 100755 --- a/DataStorage/Database/GrammarAbstract.php +++ b/DataStorage/Database/GrammarAbstract.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace phpOMS\DataStorage\Database; use phpOMS\Contract\SerializableInterface; -use phpOMS\DataStorage\Database\Query\Column; +use phpOMS\DataStorage\Database\Query\ColumnName; use phpOMS\DataStorage\Database\Query\Parameter; /** @@ -60,22 +60,6 @@ abstract class GrammarAbstract */ public string $systemIdentifierEnd = '"'; - /** - * And operator. - * - * @var string - * @since 1.0.0 - */ - public string $and = 'AND'; - - /** - * Or operator. - * - * @var string - * @since 1.0.0 - */ - public string $or = 'OR'; - /** * Special keywords. * @@ -87,9 +71,6 @@ abstract class GrammarAbstract 'MAX(', 'MIN(', 'SUM(', - 'DATE(', - 'YEAR(', - 'MONTH(', ]; /** @@ -115,30 +96,7 @@ abstract class GrammarAbstract } /** - * Compile to query. - * - * @param BuilderAbstract $query Builder - * - * @return string - * - * @since 1.0.0 - */ - public function compileQuery(BuilderAbstract $query) : string - { - $components = $this->compileComponents($query); - $queryString = ''; - - foreach ($components as $component) { - if ($component !== '') { - $queryString .= $component . ' '; - } - } - - return \substr($queryString, 0, -1) . ';'; - } - - /** - * Compile post querys. + * Compile post queries. * * These are queries, which should be run after the main query (e.g. table alters, trigger definitions etc.) * @@ -148,7 +106,7 @@ abstract class GrammarAbstract * * @since 1.0.0 */ - public function compilePostQuerys(BuilderAbstract $query) : array + public function compilePostQueries(BuilderAbstract $query) : array { return []; } @@ -164,7 +122,7 @@ abstract class GrammarAbstract * * @since 1.0.0 */ - abstract protected function compileComponents(BuilderAbstract $query) : array; + abstract public function compileComponents(BuilderAbstract $query) : array; /** * Get date format. @@ -190,20 +148,20 @@ abstract class GrammarAbstract * * @since 1.0.0 */ - protected function expressionizeTableColumn(array $elements, bool $column = true) : string + public function expressionizeTableColumn(array $elements, bool $column = true) : string { $expression = ''; foreach ($elements as $key => $element) { if (\is_string($element)) { - $expression .= $this->compileSystem($element) - . (\is_string($key) ? ' as ' . $key : '') . ', '; - } elseif ($element instanceof \Closure) { - $expression .= $element() . (\is_string($key) ? ' as ' . $key : '') . ', '; - } elseif ($element instanceof BuilderAbstract) { - $expression .= $element->toSql() . (\is_string($key) ? ' as ' . $key : '') . ', '; + $expression .= $this->compileSystem($element) . (\is_string($key) ? ' AS ' . $key : '') . ', '; } elseif (\is_int($element)) { + // example: select 1 $expression .= $element . ', '; + } elseif ($element instanceof BuilderAbstract) { + $expression .= $element->toSql() . (\is_string($key) ? ' AS ' . $key : '') . ', '; + } elseif ($element instanceof \Closure) { + $expression .= $element() . (\is_string($key) ? ' AS ' . $key : '') . ', '; } else { throw new \InvalidArgumentException(); } @@ -237,9 +195,9 @@ abstract class GrammarAbstract $identifierEnd = ''; } elseif ((\stripos($system, '.')) !== false) { // This is actually slower than \explode(), despite knowing the first index - //$split = [\substr($system, 0, $pos), \substr($system, $pos + 1)]; + // $split = [\substr($system, 0, $pos), \substr($system, $pos + 1)]; - // Faster! But might requires more memory? + // Faster! But might require more memory? $split = \explode('.', $system); $identifierTwoStart = $identifierStart; @@ -294,8 +252,8 @@ abstract class GrammarAbstract return (string) ((int) $value); } elseif (\is_float($value)) { return \rtrim(\rtrim(\number_format($value, 5, '.', ''), '0'), '.'); - } elseif ($value instanceof Column) { - return '(' . \rtrim($this->compileColumnQuery($value), ';') . ')'; + } elseif ($value instanceof ColumnName) { + return $this->compileSystem($value->name); } elseif ($value instanceof BuilderAbstract) { return '(' . \rtrim($value->toSql(), ';') . ')'; } elseif ($value instanceof \JsonSerializable) { @@ -310,18 +268,4 @@ abstract class GrammarAbstract throw new \InvalidArgumentException(\gettype($value)); } } - - /** - * Compile column query. - * - * @param Column $column Where query - * - * @return string - * - * @since 1.0.0 - */ - protected function compileColumnQuery(Column $column) : string - { - return $column->toSql(); - } } diff --git a/DataStorage/Database/Mapper/DataMapperAbstract.php b/DataStorage/Database/Mapper/DataMapperAbstract.php index ef2e963db..ff212d0fc 100755 --- a/DataStorage/Database/Mapper/DataMapperAbstract.php +++ b/DataStorage/Database/Mapper/DataMapperAbstract.php @@ -56,13 +56,28 @@ abstract class DataMapperAbstract */ protected int $depth = 1; + /** + * Mapper join alias. + * + * Mappers may have relations to other models (e.g. belongsTo, ownsOne) which can have other relations, ... + * If a mapper relates to the same model multiple times e.g. createdBy and lastModifiedBy we need to create + * separate joins because both could have different relations. However $depth only differentiates for + * different relation depth, not when the same table is referenced on the same depth/level. + * + * With the join alias we can reference the same table multiple times in a join! + * + * @var int + * @since 1.0.0 + */ + protected string $joinAlias = ''; + /** * Relations which should be loaded * * @var array * @since 1.0.0 */ - protected array $with = []; + public array $with = []; /** * Sort order @@ -143,6 +158,15 @@ abstract class DataMapperAbstract $this->db = $db; } + protected string $indexedBy = ''; + + public function indexedBy(string $index) : self + { + $this->indexedBy = $index; + + return $this; + } + /** * Define a query which is merged with the internal query generation. * @@ -152,7 +176,7 @@ abstract class DataMapperAbstract * * @since 1.0.0 */ - public function query(Builder $query = null) : self + public function query(?Builder $query = null) : self { $this->query = $query; diff --git a/DataStorage/Database/Mapper/DataMapperFactory.php b/DataStorage/Database/Mapper/DataMapperFactory.php index 3cf431d76..babca838a 100755 --- a/DataStorage/Database/Mapper/DataMapperFactory.php +++ b/DataStorage/Database/Mapper/DataMapperFactory.php @@ -108,14 +108,6 @@ class DataMapperFactory */ public const TABLE = ''; - /** - * Parent column. - * - * @var class-string - * @since 1.0.0 - */ - public const PARENT = ''; - /** * Model to use by the mapper. * @@ -187,7 +179,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function reader(ConnectionAbstract $db = null) : ReadMapper + public static function reader(?ConnectionAbstract $db = null) : ReadMapper { return new ReadMapper(new static(), $db ?? self::$db); } @@ -201,7 +193,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function get(ConnectionAbstract $db = null) : ReadMapper + public static function get(?ConnectionAbstract $db = null) : ReadMapper { /** @var ReadMapper $reader */ $reader = new ReadMapper(new static(), $db ?? self::$db); @@ -218,7 +210,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function yield(ConnectionAbstract $db = null) : ReadMapper + public static function yield(?ConnectionAbstract $db = null) : ReadMapper { /** @var ReadMapper $reader */ $reader = new ReadMapper(new static(), $db ?? self::$db); @@ -235,7 +227,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function getRaw(ConnectionAbstract $db = null) : ReadMapper + public static function getRaw(?ConnectionAbstract $db = null) : ReadMapper { /** @var ReadMapper $reader */ $reader = new ReadMapper(new static(), $db ?? self::$db); @@ -252,7 +244,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function getRandom(ConnectionAbstract $db = null) : ReadMapper + public static function getRandom(?ConnectionAbstract $db = null) : ReadMapper { return (new ReadMapper(new static(), $db ?? self::$db))->getRandom(); } @@ -266,7 +258,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function count(ConnectionAbstract $db = null) : ReadMapper + public static function count(?ConnectionAbstract $db = null) : ReadMapper { return (new ReadMapper(new static(), $db ?? self::$db))->count(); } @@ -280,7 +272,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function sum(ConnectionAbstract $db = null) : ReadMapper + public static function sum(?ConnectionAbstract $db = null) : ReadMapper { return (new ReadMapper(new static(), $db ?? self::$db))->sum(); } @@ -294,7 +286,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function exists(ConnectionAbstract $db = null) : ReadMapper + public static function exists(?ConnectionAbstract $db = null) : ReadMapper { return (new ReadMapper(new static(), $db ?? self::$db))->exists(); } @@ -308,7 +300,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function has(ConnectionAbstract $db = null) : ReadMapper + public static function has(?ConnectionAbstract $db = null) : ReadMapper { return (new ReadMapper(new static(), $db ?? self::$db))->has(); } @@ -322,7 +314,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function getQuery(ConnectionAbstract $db = null) : Builder + public static function getQuery(?ConnectionAbstract $db = null) : Builder { return (new ReadMapper(new static(), $db ?? self::$db))->getQuery(); } @@ -336,7 +328,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function getAll(ConnectionAbstract $db = null) : ReadMapper + public static function getAll(?ConnectionAbstract $db = null) : ReadMapper { /** @var ReadMapper $reader */ $reader = new ReadMapper(new static(), $db ?? self::$db); @@ -353,7 +345,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function writer(ConnectionAbstract $db = null) : WriteMapper + public static function writer(?ConnectionAbstract $db = null) : WriteMapper { return new WriteMapper(new static(), $db ?? self::$db); } @@ -367,7 +359,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function create(ConnectionAbstract $db = null) : WriteMapper + public static function create(?ConnectionAbstract $db = null) : WriteMapper { return (new WriteMapper(new static(), $db ?? self::$db))->create(); } @@ -381,7 +373,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function updater(ConnectionAbstract $db = null) : UpdateMapper + public static function updater(?ConnectionAbstract $db = null) : UpdateMapper { return new UpdateMapper(new static(), $db ?? self::$db); } @@ -395,7 +387,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function update(ConnectionAbstract $db = null) : UpdateMapper + public static function update(?ConnectionAbstract $db = null) : UpdateMapper { return (new UpdateMapper(new static(), $db ?? self::$db))->update(); } @@ -409,7 +401,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function remover(ConnectionAbstract $db = null) : DeleteMapper + public static function remover(?ConnectionAbstract $db = null) : DeleteMapper { return new DeleteMapper(new static(), $db ?? self::$db); } @@ -423,7 +415,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function delete(ConnectionAbstract $db = null) : DeleteMapper + public static function delete(?ConnectionAbstract $db = null) : DeleteMapper { return (new DeleteMapper(new static(), $db ?? self::$db))->delete(); } @@ -471,7 +463,7 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function createBaseModel(array $data = null) : object + public static function createBaseModel(?array $data = null) : object { if (empty(static::FACTORY)) { $class = empty(static::MODEL) ? \substr(static::class, 0, -6) : static::MODEL; @@ -482,6 +474,25 @@ class DataMapperFactory return static::FACTORY::createWith($data); } + public static function hasFactory() : bool + { + return !empty(static::FACTORY); + } + + /** + * Create the empty base model + * + * @param null|array $data Data to use for initialization + * + * @return string + * + * @since 1.0.0 + */ + public static function getBaseModelClass() : string + { + return empty(static::MODEL) ? \substr(static::class, 0, -6) : static::MODEL; + } + /** * Get id of object * @@ -493,14 +504,12 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function getObjectId(object $obj, string $member = null, \ReflectionClass &$refClass = null) : mixed + public static function getObjectId(object $obj, ?string $member = null, ?\ReflectionClass &$refClass = null) : mixed { $propertyName = $member ?? static::COLUMNS[static::PRIMARYFIELD]['internal']; if (static::COLUMNS[static::PRIMARYFIELD]['private'] ?? false) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $refProp = $refClass->getProperty($propertyName); @@ -521,15 +530,13 @@ class DataMapperFactory * * @since 1.0.0 */ - public static function setObjectId(object $obj, mixed $objId, \ReflectionClass &$refClass = null) : void + public static function setObjectId(object $obj, mixed $objId, ?\ReflectionClass &$refClass = null) : void { $propertyName = static::COLUMNS[static::PRIMARYFIELD]['internal']; \settype($objId, static::COLUMNS[static::PRIMARYFIELD]['type']); if (static::COLUMNS[static::PRIMARYFIELD]['private'] ?? false) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $refProp = $refClass->getProperty($propertyName); $refProp->setValue($obj, $objId); @@ -569,7 +576,7 @@ class DataMapperFactory * @param int $pageLimit Limit result set * @param string $sortBy Model member name to sort by * @param string $sortOrder Sort order - * @param array $searchFields Fields to search in. ([] = all) @todo: maybe change to all which have autocomplete = true defined? + * @param array $searchFields Fields to search in. ([] = all) @todo maybe change to all which have autocomplete = true defined? * @param array $filters Additional search filters applied ['type', 'value1', 'logic1', 'value2', 'logic2'] * * @return array{hasPrevious:bool, hasNext:bool, data:object[]} @@ -577,18 +584,18 @@ class DataMapperFactory * @since 1.0.0 */ public static function find( - string $search = null, - DataMapperAbstract $mapper = null, + ?string $search = null, + ?DataMapperAbstract $mapper = null, int $id = 0, string $secondaryId = '', - string $type = null, + ?string $type = null, int $pageLimit = 25, - string $sortBy = null, + ?string $sortBy = null, string $sortOrder = OrderType::DESC, array $searchFields = [], array $filters = [] ) : array { - $mapper ??= static::getAll(); + $mapper ??= static::getAll(); $sortOrder = \strtoupper($sortOrder); $data = []; @@ -636,7 +643,7 @@ class DataMapperFactory } } - // @todo: how to handle columns which are NOT members (columns which are manipulated) + // @todo how to handle columns which are NOT members (columns which are manipulated) // Maybe pass callback array which can handle these cases? if ($type === 'p') { @@ -688,26 +695,15 @@ class DataMapperFactory --$count; } } else { - if (\reset($data)->getId() === $id) { + if (\reset($data)->id === $id) { \array_shift($data); $hasNext = true; --$count; } if ($count > $pageLimit) { - // @todo: can be maybe removed? - /* - if (!$hasNext) { - \array_pop($data); - $hasNext = true; - --$count; - } - */ - - if ($count > $pageLimit) { - $hasPrevious = true; - \array_pop($data); - } + $hasPrevious = true; + \array_pop($data); } $data = \array_reverse($data); @@ -742,7 +738,7 @@ class DataMapperFactory ]; } - if (\reset($data)->getId() === $id) { + if (\reset($data)->id === $id) { \array_shift($data); $hasPrevious = true; --$count; diff --git a/DataStorage/Database/Mapper/DeleteMapper.php b/DataStorage/Database/Mapper/DeleteMapper.php index c68ab7877..992deaaf5 100755 --- a/DataStorage/Database/Mapper/DeleteMapper.php +++ b/DataStorage/Database/Mapper/DeleteMapper.php @@ -78,10 +78,10 @@ final class DeleteMapper extends DataMapperAbstract return null; } - $this->deleteSingleRelation($obj, $this->mapper::BELONGS_TO, $refClass); + $this->deleteSingleRelation($obj, $this->mapper::OWNS_ONE, $refClass); $this->deleteHasMany($obj, $objId, $refClass); $this->deleteModel($objId); - $this->deleteSingleRelation($obj, $this->mapper::OWNS_ONE, $refClass); + $this->deleteSingleRelation($obj, $this->mapper::BELONGS_TO, $refClass); return $objId; } @@ -119,7 +119,7 @@ final class DeleteMapper extends DataMapperAbstract * * @since 1.0.0 */ - private function deleteSingleRelation(object $obj, array $relation, \ReflectionClass &$refClass = null) : void + private function deleteSingleRelation(object $obj, array $relation, ?\ReflectionClass &$refClass = null) : void { if (empty($relation)) { return; @@ -141,9 +141,7 @@ final class DeleteMapper extends DataMapperAbstract $value = null; if ($isPrivate) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $refProp = $refClass->getProperty($member); $value = $refProp->getValue($obj); @@ -166,14 +164,13 @@ final class DeleteMapper extends DataMapperAbstract * * @since 1.0.0 */ - private function deleteHasMany(object $obj, mixed $objId, \ReflectionClass &$refClass = null) : void + private function deleteHasMany(object $obj, mixed $objId, ?\ReflectionClass &$refClass = null) : void { if (empty($this->mapper::HAS_MANY)) { return; } foreach ($this->mapper::HAS_MANY as $member => $rel) { - // always if (!isset($this->with[$member]) && !isset($rel['external'])) { continue; } @@ -183,9 +180,7 @@ final class DeleteMapper extends DataMapperAbstract $values = null; if ($isPrivate) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $refProp = $refClass->getProperty($member); $values = $refProp->getValue($obj); @@ -237,7 +232,7 @@ final class DeleteMapper extends DataMapperAbstract * * @since 1.0.0 */ - public function deleteRelationTable(string $member, array $objIds = null, mixed $objId) : void + public function deleteRelationTable(string $member, ?array $objIds = null, mixed $objId) : void { if ((empty($objIds) && $objIds !== null) || $this->mapper::HAS_MANY[$member]['table'] === $this->mapper::TABLE @@ -252,7 +247,7 @@ final class DeleteMapper extends DataMapperAbstract ->where($this->mapper::HAS_MANY[$member]['table'] . '.' . $this->mapper::HAS_MANY[$member]['self'], '=', $objId); if ($objIds !== null) { - $relQuery->where($this->mapper::HAS_MANY[$member]['table'] . '.' . $this->mapper::HAS_MANY[$member]['external'], 'in', $objIds); + $relQuery->where($this->mapper::HAS_MANY[$member]['table'] . '.' . $this->mapper::HAS_MANY[$member]['external'], 'IN', $objIds); } $sth = $this->db->con->prepare($relQuery->toSql()); diff --git a/DataStorage/Database/Mapper/MapperType.php b/DataStorage/Database/Mapper/MapperType.php index 158ea6f2c..8fde41f96 100755 --- a/DataStorage/Database/Mapper/MapperType.php +++ b/DataStorage/Database/Mapper/MapperType.php @@ -38,6 +38,13 @@ abstract class MapperType extends Enum public const GET_RANDOM = 11; + // @IMPORTANT All read operations which use column names must have an ID < 12 + // In the read mapper exists a line which checks for < COUNT_MODELS to decide if columns should be selected. + // The reason for this is that **pure** count, sum, ... don't want to select additional column names. + // By doing this we avoid loading all the unwanted columns coming from the `with()` relation. + + // -------------------------------------------- // + public const COUNT_MODELS = 12; public const SUM_MODELS = 13; diff --git a/DataStorage/Database/Mapper/ReadMapper.php b/DataStorage/Database/Mapper/ReadMapper.php index 7d696467d..6fec4a196 100755 --- a/DataStorage/Database/Mapper/ReadMapper.php +++ b/DataStorage/Database/Mapper/ReadMapper.php @@ -26,8 +26,9 @@ use phpOMS\Utils\ArrayUtils; * @link https://jingga.app * @since 1.0.0 * - * @todo Add memory cache per read mapper parent call (These should be cached: attribute types, file types, etc.) * @todo Add getArray functions to get array instead of object + * https://github.com/Karaka-Management/phpOMS/issues/350 + * * @todo Allow to define columns in all functions instead of members? * * @template R @@ -182,7 +183,6 @@ final class ReadMapper extends DataMapperAbstract * @return self * * @since 1.0.0 - * @todo: consider to accept properties instead and then check ::COLUMNS which contian the property and ADD that array into $this->columns. Maybe also consider a rename from columns() to property() */ public function columns(array $columns) : self { @@ -191,6 +191,24 @@ final class ReadMapper extends DataMapperAbstract return $this; } + /** + * Define the properties to load + * + * @param array $properties Properties to load + * + * @return self + * + * @since 1.0.0 + */ + public function properties(array $properties) : self + { + foreach ($properties as $property) { + $this->columns[] = $this->mapper::getColumnByMember($property); + } + + return $this; + } + /** * Execute mapper * @@ -216,7 +234,7 @@ final class ReadMapper extends DataMapperAbstract /** @var null|Builder ...$options */ return $this->executeGetAll(...$options); case MapperType::GET_RANDOM: - return $this->executeGetRaw(); + return $this->executeRandom(); case MapperType::COUNT_MODELS: return $this->executeCount(); case MapperType::SUM_MODELS: @@ -241,53 +259,50 @@ final class ReadMapper extends DataMapperAbstract * * @since 1.0.0 */ - public function executeGet(Builder $query = null) : mixed + public function executeGet(?Builder $query = null) : mixed { - $primaryKeys = []; - $memberOfPrimaryField = $this->mapper::COLUMNS[$this->mapper::PRIMARYFIELD]['internal']; + $objs = []; + $indexed = []; - if (isset($this->where[$memberOfPrimaryField])) { - $keys = $this->where[$memberOfPrimaryField][0]['value']; - $primaryKeys = \array_merge(\is_array($keys) ? $keys : [$keys], $primaryKeys); - } + $hasFactory = $this->mapper::hasFactory(); + $baseClass = $hasFactory ? null : $this->mapper::getBaseModelClass(); - // Get initialized objects from memory cache. - $obj = []; - - // Get remaining objects (not available in memory cache) or remaining where clauses. - //$dbData = $this->executeGetRaw($query); - - $ids = []; foreach ($this->executeGetRawYield($query) as $row) { if ($row === []) { continue; } - $value = $row[$this->mapper::PRIMARYFIELD . '_d' . $this->depth]; - $obj[$value] = $this->mapper::createBaseModel($row); + $value = $row[$this->mapper::PRIMARYFIELD . '_d' . $this->depth . $this->joinAlias]; + $objs[$value] = $hasFactory ? $this->mapper::createBaseModel($row) : new $baseClass(); + $objs[$value] = $this->populateAbstract($row, $objs[$value]); - $obj[$value] = $this->populateAbstract($row, $obj[$value]); + if (!empty($this->indexedBy) && isset($row[$this->indexedBy . '_d' . $this->depth . $this->joinAlias])) { + if (!isset($indexed[$row[$this->indexedBy . '_d' . $this->depth . $this->joinAlias]])) { + $indexed[$row[$this->indexedBy . '_d' . $this->depth . $this->joinAlias]] = []; + } - $ids[] = $value; - - // @todo: This is too slow, since it creates a query for every $row x relation type. - // Pulling it out would be nice. - // The problem with solving this is that in a many-to-many relationship a relation table is used - // BUT the relation data is not available in the object itself meaning after retrieving the object - // it cannot get assigned to the correct parent object. - // Other relation types are easy because either the parent or child object contain the relation info. - $this->loadHasManyRelations($obj[$value]); + $indexed[$row[$this->indexedBy . '_d' . $this->depth . $this->joinAlias]][] = $objs[$value]; + } } - $countResulsts = \count($obj); + if (!empty($this->with) && !empty($objs)) { + $this->loadHasManyRelations($objs); + } - if ($countResulsts === 0) { + if (!empty($this->indexedBy)) { + return $indexed; + } elseif ($this->type === MapperType::GET_ALL) { + return $objs; + } + + $countResults = \count($objs); + if ($countResults === 0) { return $this->mapper::createNullModel(); - } elseif ($countResulsts === 1) { - return \reset($obj); + } elseif ($countResults === 1) { + return \reset($objs); } - return $obj; + return $objs; } /** @@ -299,20 +314,15 @@ final class ReadMapper extends DataMapperAbstract * * @since 1.0.0 */ - public function executeGetYield(Builder $query = null) + public function executeGetYield(?Builder $query = null) { - $primaryKeys = []; - $memberOfPrimaryField = $this->mapper::COLUMNS[$this->mapper::PRIMARYFIELD]['internal']; - - if (isset($this->where[$memberOfPrimaryField])) { - $keys = $this->where[$memberOfPrimaryField][0]['value']; - $primaryKeys = \array_merge(\is_array($keys) ? $keys : [$keys], $primaryKeys); - } - foreach ($this->executeGetRawYield($query) as $row) { $obj = $this->mapper::createBaseModel($row); $obj = $this->populateAbstract($row, $obj); - $this->loadHasManyRelations($obj); + + if (!empty($this->with)) { + $this->loadHasManyRelations([$obj]); + } yield $obj; } @@ -329,21 +339,18 @@ final class ReadMapper extends DataMapperAbstract * * @since 1.0.0 */ - public function executeGetRaw(Builder $query = null) : array + public function executeGetRaw(?Builder $query = null) : array { $query ??= $this->getQuery(); + $results = false; try { - $results = false; - $sth = $this->db->con->prepare($query->toSql()); if ($sth !== false) { $sth->execute(); $results = $sth->fetchAll(\PDO::FETCH_ASSOC); } } catch (\Throwable $t) { - $results = false; - \phpOMS\Log\FileLogger::getInstance()->error( \phpOMS\Log\FileLogger::MSG_FULL, [ 'message' => $t->getMessage() . ':' . $query->toSql(), @@ -367,11 +374,21 @@ final class ReadMapper extends DataMapperAbstract * * @since 1.0.0 */ - public function executeGetRawYield(Builder $query = null) + public function executeGetRawYield(?Builder $query = null) { $query ??= $this->getQuery(); try { + /* + \phpOMS\Log\FileLogger::getInstance()->info( + \phpOMS\Log\FileLogger::MSG_FULL, [ + 'message' => $query->toSql(), + 'line' => __LINE__, + 'file' => self::class, + ] + ); + */ + $sth = $this->db->con->prepare($query->toSql()); if ($sth === false) { yield []; @@ -408,7 +425,7 @@ final class ReadMapper extends DataMapperAbstract * * @since 1.0.0 */ - public function executeGetAll(Builder $query = null) : array + public function executeGetAll(?Builder $query = null) : array { $result = $this->executeGet($query); @@ -433,7 +450,7 @@ final class ReadMapper extends DataMapperAbstract $query = $this->getQuery( null, [ - 'COUNT(' . (empty($this->columns) ? '*' : \implode($this->columns)) . ')' => 'count' + 'COUNT(' . (empty($this->columns) ? '*' : \implode(',', $this->columns)) . ')' => 'count', ] ); @@ -443,7 +460,7 @@ final class ReadMapper extends DataMapperAbstract /** * Sum the number of elements * - * @return int + * @return int|float * * @since 1.0.0 */ @@ -452,11 +469,16 @@ final class ReadMapper extends DataMapperAbstract $query = $this->getQuery( null, [ - 'SUM(' . (empty($this->columns) ? '*' : \implode($this->columns)) . ')' => 'sum' + 'SUM(' . (empty($this->columns) ? '*' : \implode(',', $this->columns)) . ')' => 'sum', ] ); - return $query->execute()?->fetchColumn(); + $result = $query->execute()?->fetchColumn(); + if (empty($result)) { + return 0; + } + + return \stripos($result, '.') === false ? (int) $result : (float) $result; } /** @@ -514,76 +536,86 @@ final class ReadMapper extends DataMapperAbstract * * @since 1.0.0 */ - public function getQuery(Builder $query = null, array $columns = []) : Builder + public function getQuery(?Builder $query = null, array $columns = []) : Builder { $query ??= $this->query ?? new Builder($this->db, true); - $columns = empty($columns) - ? (empty($this->columns) ? $this->mapper::COLUMNS : $this->columns) - : $columns; + + if (empty($columns) && $this->type < MapperType::COUNT_MODELS) { + $columns = empty($this->columns) ? $this->mapper::COLUMNS : $this->columns; + } foreach ($columns as $key => $values) { - if (\is_string($values) || \is_int($values)) { - if (\is_int($key)) { - $query->select($values); - } else { - $query->selectAs($key, $values); - } - } elseif (($values['writeonly'] ?? false) === false || isset($this->with[$values['internal']])) { + if (\is_array($values) + && (($values['writeonly'] ?? false) === false || isset($this->with[$values['internal']])) + ) { if (\is_int($key)) { $query->select($key); } else { - $query->selectAs($this->mapper::TABLE . '_d' . $this->depth . '.' . $key, $key . '_d' . $this->depth); + $query->selectAs( + $this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $key, + $key . '_d' . $this->depth . $this->joinAlias + ); } + } elseif (\is_int($values)) { + $query->select($values); + } elseif (\is_string($values)) { + $query->selectAs($key, $values); } } if (empty($query->from)) { - $query->fromAs($this->mapper::TABLE, $this->mapper::TABLE . '_d' . $this->depth); + $query->fromAs($this->mapper::TABLE, $this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias); } - // Join tables manually without using "with()" (NOT has many/owns one etc.) + // Join tables manually without using "with()" (NOT hasMany/owns one etc.) // This is necessary for special cases, e.g. when joining in the other direction // Example: Show all profiles who have written a news article. // "with()" only allows to go from articles to accounts but we want to go the other way + // + // @feature Create join functionality for mappers which supports joining and filtering based on other tables + // Example: show all profiles which have written a news article + // https://github.com/Karaka-Management/phpOMS/issues/253 foreach ($this->join as $member => $values) { if (($col = $this->mapper::getColumnByMember($member)) === null) { continue; } /* variable in model */ - // @todo: join handling is extremely ugly, needs to be refactored + // @todo join handling is extremely ugly, needs to be refactored foreach ($values as $join) { - // @todo: the has many, etc. if checks only work if it is a relation on the first level, if we have a deeper where condition nesting this fails + // @todo the hasMany, etc. if checks only work if it is a relation on the first level, if we have a deeper where condition nesting this fails if ($join['child'] !== '') { continue; } if (isset($join['mapper']::HAS_MANY[$join['value']])) { if (isset($join['mapper']::HAS_MANY[$join['value']]['external'])) { + $relJoinTable = $join['mapper']::HAS_MANY[$join['value']]['table']; + // join with relation table - $query->join($join['mapper']::HAS_MANY[$join['value']]['table'], $join['type'], $join['mapper']::HAS_MANY[$join['value']]['table'] . '_d' . ($this->depth + 1)) + $query->join($relJoinTable, $join['type'], $relJoinTable . '_d' . ($this->depth + 1) . $this->joinAlias) ->on( - $this->mapper::TABLE . '_d' . $this->depth . '.' . $col, + $this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $col, '=', - $join['mapper']::HAS_MANY[$join['value']]['table'] . '_d' . ($this->depth + 1) . '.' . $join['mapper']::HAS_MANY[$join['value']]['external'], + $relJoinTable . '_d' . ($this->depth + 1) . $this->joinAlias . '.' . $join['mapper']::HAS_MANY[$join['value']]['external'], 'AND', - $join['mapper']::HAS_MANY[$join['value']]['table'] . '_d' . ($this->depth + 1) + $relJoinTable . '_d' . ($this->depth + 1) . $this->joinAlias ); // join with model table - $query->join($join['mapper']::TABLE, $join['type'], $join['mapper']::TABLE . '_d' . ($this->depth + 1)) + $query->join($join['mapper']::TABLE, $join['type'], $join['mapper']::TABLE . '_d' . ($this->depth + 1) . $this->joinAlias) ->on( - $join['mapper']::HAS_MANY[$join['value']]['table'] . '_d' . ($this->depth + 1) . '.' . $join['mapper']::HAS_MANY[$join['value']]['self'], + $relJoinTable . '_d' . ($this->depth + 1) . $this->joinAlias . '.' . $join['mapper']::HAS_MANY[$join['value']]['self'], '=', - $join['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . $join['mapper']::PRIMARYFIELD, + $join['mapper']::TABLE . '_d' . ($this->depth + 1) . $this->joinAlias . '.' . $join['mapper']::PRIMARYFIELD, 'AND', - $join['mapper']::TABLE . '_d' . ($this->depth + 1) + $join['mapper']::TABLE . '_d' . ($this->depth + 1) . $this->joinAlias ); if (isset($this->on[$join['value']])) { foreach ($this->on[$join['value']] as $on) { $query->where( - $join['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . $join['mapper']::getColumnByMember($on['member']), + $join['mapper']::TABLE . '_d' . ($this->depth + 1) . $this->joinAlias . '.' . $join['mapper']::getColumnByMember($on['member']), '=', $on['value'], 'AND' @@ -592,13 +624,13 @@ final class ReadMapper extends DataMapperAbstract } } } else { - $query->join($join['mapper']::TABLE, $join['type'], $join['mapper']::TABLE . '_d' . ($this->depth + 1)) + $query->join($join['mapper']::TABLE, $join['type'], $join['mapper']::TABLE . '_d' . ($this->depth + 1) . $this->joinAlias) ->on( - $this->mapper::TABLE . '_d' . $this->depth . '.' . $col, + $this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $col, '=', - $join['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . $join['mapper']::getColumnByMember($join['value']), + $join['mapper']::TABLE . '_d' . ($this->depth + 1) . $this->joinAlias . '.' . $join['mapper']::getColumnByMember($join['value']), 'AND', - $join['mapper']::TABLE . '_d' . ($this->depth + 1) + $join['mapper']::TABLE . '_d' . ($this->depth + 1) . $this->joinAlias ); } } @@ -625,12 +657,12 @@ final class ReadMapper extends DataMapperAbstract /* variable in model */ $previous = null; foreach ($values as $where) { - // @todo: the has many, etc. if checks only work if it is a relation on the first level, if we have a deeper where condition nesting this fails + // @todo the hasMany, etc. if checks only work if it is a relation on the first level, if we have a deeper where condition nesting this fails if ($where['child'] !== '') { continue; } - $comparison = \is_array($where['value']) && \count($where['value']) > 1 ? 'in' : $where['logic']; + $comparison = \is_array($where['value']) && \count($where['value']) > 1 ? 'IN' : $where['logic']; if ($where['comparison'] === 'ALT') { // This uses an alternative value if the previous value(s) in the where clause don't exist (e.g. for localized results where you allow a user language, alternatively a primary language, and then alternatively any language if the first two don't exist). @@ -651,21 +683,21 @@ final class ReadMapper extends DataMapperAbstract ) */ $where1 = new Where($this->db); - $where1->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, $comparison, $where['value'], 'and'); + $where1->where($this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $col, $comparison, $where['value'], 'and'); $where2 = new Builder($this->db); - $where2->select('1') // @todo: why is this in quotes? - ->from($this->mapper::TABLE . '_d' . $this->depth) - ->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, 'in', $alt); + $where2->select(1) + ->from($this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias) + ->where($this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $col, 'in', $alt); - $where1->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, 'not exists', $where2, 'and'); + $where1->where($this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $col, 'not exists', $where2, 'and'); - $query->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, $comparison, $where1, 'or'); + $query->where($this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $col, $comparison, $where1, 'or'); $alt[] = $where['value']; } else { $previous = $where; - $query->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, $comparison, $where['value'], $where['comparison']); + $query->where($this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $col, $comparison, $where['value'], $where['comparison']); } } } @@ -676,7 +708,11 @@ final class ReadMapper extends DataMapperAbstract if ((isset($this->mapper::OWNS_ONE[$member]) || isset($this->mapper::BELONGS_TO[$member])) || (!isset($this->mapper::HAS_MANY[$member]['external']) && isset($this->mapper::HAS_MANY[$member]['column'])) ) { - $rel = $this->mapper::OWNS_ONE[$member] ?? ($this->mapper::BELONGS_TO[$member] ?? ($this->mapper::HAS_MANY[$member] ?? null)); + $rel = $this->mapper::OWNS_ONE[$member] ?? ( + $this->mapper::BELONGS_TO[$member] ?? ( + $this->mapper::HAS_MANY[$member] ?? null + ) + ); } else { continue; } @@ -687,32 +723,37 @@ final class ReadMapper extends DataMapperAbstract } if (isset($this->mapper::OWNS_ONE[$member]) || isset($this->mapper::BELONGS_TO[$member])) { - $query->leftJoin($rel['mapper']::TABLE, $rel['mapper']::TABLE . '_d' . ($this->depth + 1)) + $tableAlias = $rel['mapper']::TABLE . '_d' . ($this->depth + 1) . '_' . $member; + $query->leftJoin($rel['mapper']::TABLE, $tableAlias) ->on( - $this->mapper::TABLE . '_d' . $this->depth . '.' . $rel['external'], '=', - $rel['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . ( + $this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $rel['external'], '=', + $tableAlias . '.' . ( isset($rel['by']) ? $rel['mapper']::getColumnByMember($rel['by']) : $rel['mapper']::PRIMARYFIELD ), 'and', - $rel['mapper']::TABLE . '_d' . ($this->depth + 1) + $tableAlias ); } elseif (!isset($this->mapper::HAS_MANY[$member]['external']) && isset($this->mapper::HAS_MANY[$member]['column'])) { // get HasManyQuery (but only for elements which have a 'column' defined) + $tableAlias = $rel['mapper']::TABLE . '_d' . ($this->depth + 1) . '_' . $member; - // @todo: handle self and self === null - $query->leftJoin($rel['mapper']::TABLE, $rel['mapper']::TABLE . '_d' . ($this->depth + 1)) + // @todo handle self and self === null + $query->leftJoin($rel['mapper']::TABLE, $tableAlias) ->on( - $this->mapper::TABLE . '_d' . $this->depth . '.' . ($rel['external'] ?? $this->mapper::PRIMARYFIELD), '=', - $rel['mapper']::TABLE . '_d' . ($this->depth + 1) . '.' . ( + $this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . ($rel['external'] ?? $this->mapper::PRIMARYFIELD), '=', + $tableAlias . '.' . ( isset($rel['by']) ? $rel['mapper']::getColumnByMember($rel['by']) : $rel['self'] ), 'and', - $rel['mapper']::TABLE . '_d' . ($this->depth + 1) + $tableAlias ); } /** @var self $relMapper */ $relMapper = $this->createRelationMapper($rel['mapper']::reader(db: $this->db), $member); $relMapper->depth = $this->depth + 1; + $relMapper->type = $this->type; + $relMapper->joinAlias = '_' . $member; + // Here we go further into the depth of the model (e.g. a hasMany/ownsOne can again have ownsOne...) $query = $relMapper->getQuery( $query, isset($rel['column']) ? [$rel['mapper']::getColumnByMember($rel['column']) => []] : [] @@ -731,8 +772,10 @@ final class ReadMapper extends DataMapperAbstract continue; } - $query->orderBy($this->mapper::TABLE . '_d' . $this->depth . '.' . $column, $sort['order']); + $query->orderBy($this->mapper::TABLE . '_d' . $this->depth . $this->joinAlias . '.' . $column, $sort['order']); + // @bug It looks like that only one sort parameter is supported despite SQL supporting multiple + // https://github.com/Karaka-Management/phpOMS/issues/364 break; // there is only one root element (one element with child === '') } } @@ -769,29 +812,28 @@ final class ReadMapper extends DataMapperAbstract { $refClass = null; - foreach ($this->mapper::COLUMNS as $column => $def) { - $alias = $column . '_d' . $this->depth; + $aValue = null; + $arrayPath = ''; + foreach ($this->mapper::COLUMNS as $column => $def) { + $alias = $column . '_d' . $this->depth . $this->joinAlias; if (!\array_key_exists($alias, $result)) { continue; } - $value = $result[$alias]; - + $value = $result[$alias]; $hasPath = false; - $aValue = []; - $arrayPath = ''; $refProp = null; $isPrivate = $def['private'] ?? false; - $member = ''; + $member = $def['internal']; if ($isPrivate && $refClass === null) { $refClass = new \ReflectionClass($obj); } - if (\stripos($def['internal'], '/') !== false) { + if (\stripos($member, '/') !== false) { $hasPath = true; - $path = \explode('/', \ltrim($def['internal'], '/')); + $path = \explode('/', \ltrim($member, '/')); $member = $path[0]; if ($isPrivate) { @@ -803,15 +845,12 @@ final class ReadMapper extends DataMapperAbstract \array_shift($path); $arrayPath = \implode('/', $path); - } else { - if ($isPrivate) { - $refProp = $refClass->getProperty($def['internal']); - } - - $member = $def['internal']; + } elseif ($isPrivate) { + $refProp = $refClass->getProperty($member); } - if (isset($this->mapper::OWNS_ONE[$def['internal']])) { + $type = $def['type']; + if (isset($this->mapper::OWNS_ONE[$member])) { $default = null; if (!isset($this->with[$member]) && ($isPrivate ? $refProp->isInitialized($obj) : isset($obj->{$member})) @@ -819,18 +858,8 @@ final class ReadMapper extends DataMapperAbstract $default = $isPrivate ? $refProp->getValue($obj) : $obj->{$member}; } - $value = $this->populateOwnsOne($def['internal'], $result, $default); - - // loads has many relations. other relations are loaded in the populateOwnsOne - if (\is_object($value) && isset($this->mapper::OWNS_ONE[$def['internal']]['mapper'])) { - $this->mapper::OWNS_ONE[$def['internal']]['mapper']::reader(db: $this->db)->loadHasManyRelations($value); - } - - if (empty($value)) { - // @todo: find better solution. this was because of a bug with the sales billing list query depth = 4. The address was set (from the client, referral or creator) but then somehow there was a second address element which was all null and null cannot be asigned to a string variable (e.g. country). The problem with this solution is that if the model expects an initialization (e.g. at lest set the elements to null, '', 0 etc.) this is now not done. - $value = $isPrivate ? $refProp->getValue($obj) : $obj->{$member}; - } - } elseif (isset($this->mapper::BELONGS_TO[$def['internal']])) { + $value = $this->populateOwnsOne($member, $result, $default); + } elseif (isset($this->mapper::BELONGS_TO[$member])) { $default = null; if (!isset($this->with[$member]) && ($isPrivate ? $refProp->isInitialized($obj) : isset($obj->{$member})) @@ -838,44 +867,39 @@ final class ReadMapper extends DataMapperAbstract $default = $isPrivate ? $refProp->getValue($obj) : $obj->{$member}; } - $value = $this->populateBelongsTo($def['internal'], $result, $default); - - // loads has many relations. other relations are loaded in the populateBelongsTo - if (\is_object($value) && isset($this->mapper::BELONGS_TO[$def['internal']]['mapper'])) { - $this->mapper::BELONGS_TO[$def['internal']]['mapper']::reader(db: $this->db)->loadHasManyRelations($value); - } - } elseif (\in_array($def['type'], ['string', 'compress', 'int', 'float', 'bool'])) { - if ($value !== null && $def['type'] === 'compress') { - $def['type'] = 'string'; - + $value = $this->populateBelongsTo($member, $result, $default); + } elseif (\in_array($type, ['string', 'compress', 'int', 'float', 'bool'])) { + if ($value !== null && $type === 'compress') { + $type = 'string'; $value = \gzinflate($value); } - $mValue = $isPrivate ? $refProp->getValue($obj) : $obj->{$member}; - if ($value !== null || $mValue !== null) { - \settype($value, $def['type']); + if ($value !== null + || ($isPrivate ? $refProp->getValue($obj) !== null : $obj->{$member} !== null) + ) { + \settype($value, $type); } if ($hasPath) { $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); } - } elseif ($def['type'] === 'DateTime') { + } elseif ($type === 'DateTime') { $value = $value === null ? null : new \DateTime($value); if ($hasPath) { $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); } - } elseif ($def['type'] === 'DateTimeImmutable') { + } elseif ($type === 'DateTimeImmutable') { $value = $value === null ? null : new \DateTimeImmutable($value); if ($hasPath) { $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); } - } elseif ($def['type'] === 'Json') { + } elseif ($type === 'Json') { if ($hasPath) { $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); } $value = \json_decode($value, true); - } elseif ($def['type'] === 'Serializable') { + } elseif ($type === 'Serializable') { $mObj = $isPrivate ? $refProp->getValue($obj) : $obj->{$member}; if ($mObj !== null && $value !== null) { @@ -891,18 +915,32 @@ final class ReadMapper extends DataMapperAbstract } } - foreach ($this->mapper::HAS_MANY as $member => $def) { - $column = $def['mapper']::getColumnByMember($def['column'] ?? $member); - $alias = $column . '_d' . ($this->depth + 1); + if (empty($this->with)) { + return $obj; + } - if (!\array_key_exists($alias, $result) || !isset($def['column'])) { + // This is only for hasMany elements where only one hasMany child object is loaded + // Example: A model usually only loads one l11n element despite having localizations for multiple languages + // @todo The code below is basically a copy of the foreach from above. + // Maybe we can combine them in a smart way without adding much overhead + foreach ($this->mapper::HAS_MANY as $member => $def) { + // Only if column is defined do we have a pseudo 1-to-1 relation + // The content of the column will be loaded directly in the member variable + if (!isset($this->with[$member]) + || !isset($def['column']) + ) { + continue; + } + + $column = $def['mapper']::getColumnByMember($def['column'] ?? $member); + $alias = $column . '_d' . ($this->depth + 1) . '_' . $member; + + if (!\array_key_exists($alias, $result)) { continue; } $value = $result[$alias]; $hasPath = false; - $aValue = null; - $arrayPath = '/'; $refProp = null; $isPrivate = $def['private'] ?? false; @@ -912,47 +950,55 @@ final class ReadMapper extends DataMapperAbstract if (\stripos($member, '/') !== false) { $hasPath = true; - $path = \explode('/', $member); + $path = \explode('/', \ltrim($member, '/')); $member = $path[0]; if ($isPrivate) { $refProp = $refClass->getProperty($path[0]); + $aValue = $refProp->getValue($obj); + } else { + $aValue = $obj->{$path[0]}; } \array_shift($path); $arrayPath = \implode('/', $path); - $aValue = $isPrivate ? $refProp->getValue($obj) : $obj->{$path[0]}; } elseif ($isPrivate) { $refProp = $refClass->getProperty($member); } - if (\in_array($def['mapper']::COLUMNS[$column]['type'], ['string', 'int', 'float', 'bool'])) { + $type = $def['mapper']::COLUMNS[$column]['type']; + if (\in_array($type, ['string', 'compress', 'int', 'float', 'bool'])) { + if ($value !== null && $type === 'compress') { + $type = 'string'; + $value = \gzinflate($value); + } + if ($value !== null || ($isPrivate ? $refProp->getValue($obj) !== null : $obj->{$member} !== null) ) { - \settype($value, $def['mapper']::COLUMNS[$column]['type']); + \settype($value, $type); } if ($hasPath) { $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); } - } elseif ($def['mapper']::COLUMNS[$column]['type'] === 'DateTime') { - $value ??= new \DateTime($value); + } elseif ($type === 'DateTime') { + $value = $value === null ? null : new \DateTime($value); if ($hasPath) { $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); } - } elseif ($def['mapper']::COLUMNS[$column]['type'] === 'DateTimeImmutable') { - $value ??= new \DateTimeImmutable($value); + } elseif ($type === 'DateTimeImmutable') { + $value = $value === null ? null : new \DateTimeImmutable($value); if ($hasPath) { $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); } - } elseif ($def['mapper']::COLUMNS[$column]['type'] === 'Json') { + } elseif ($type === 'Json') { if ($hasPath) { $value = ArrayUtils::setArray($arrayPath, $aValue, $value, '/', true); } $value = \json_decode($value, true); - } elseif ($def['mapper']::COLUMNS[$column]['type'] === 'Serializable') { + } elseif ($type === 'Serializable') { $mObj = $isPrivate ? $refProp->getValue($obj) : $obj->{$member}; if ($mObj !== null && $value !== null) { @@ -980,9 +1026,6 @@ final class ReadMapper extends DataMapperAbstract * * @return mixed * - * @todo: in the future we could pass not only the $id ref but all of the data as a join!!! and save an additional select!!! - * @todo: parent and child elements however must be loaded because they are not loaded - * * @since 1.0.0 */ public function populateOwnsOne(string $member, array $result, mixed $default = null) : mixed @@ -991,26 +1034,23 @@ final class ReadMapper extends DataMapperAbstract $mapper = $this->mapper::OWNS_ONE[$member]['mapper']; if (!isset($this->with[$member])) { - if (\array_key_exists($this->mapper::OWNS_ONE[$member]['external'] . '_d' . ($this->depth), $result)) { + if (\array_key_exists($this->mapper::OWNS_ONE[$member]['external'] . '_d' . $this->depth . $this->joinAlias, $result)) { return isset($this->mapper::OWNS_ONE[$member]['column']) - ? $result[$this->mapper::OWNS_ONE[$member]['external'] . '_d' . ($this->depth)] - : $mapper::createNullModel($result[$this->mapper::OWNS_ONE[$member]['external'] . '_d' . ($this->depth)]); + ? $result[$this->mapper::OWNS_ONE[$member]['external'] . '_d' . $this->depth . $this->joinAlias] + : $mapper::createNullModel($result[$this->mapper::OWNS_ONE[$member]['external'] . '_d' . $this->depth . $this->joinAlias]); } else { return $default; } - } - - if (isset($this->mapper::OWNS_ONE[$member]['column'])) { - return $result[$mapper::getColumnByMember($this->mapper::OWNS_ONE[$member]['column']) . '_d' . $this->depth]; - } - - if (!isset($result[$mapper::PRIMARYFIELD . '_d' . ($this->depth + 1)])) { + } elseif (isset($this->mapper::OWNS_ONE[$member]['column'])) { + return $result[$mapper::getColumnByMember($this->mapper::OWNS_ONE[$member]['column']) . '_d' . $this->depth . '_' . $member]; + } elseif (!isset($result[$mapper::PRIMARYFIELD . '_d' . ($this->depth + 1) . '_' . $member])) { return $mapper::createNullModel(); } /** @var self $ownsOneMapper */ - $ownsOneMapper = $this->createRelationMapper($mapper::get($this->db), $member); - $ownsOneMapper->depth = $this->depth + 1; + $ownsOneMapper = $this->createRelationMapper($mapper::get($this->db), $member); + $ownsOneMapper->depth = $this->depth + 1; + $ownsOneMapper->joinAlias = '_' . $member; return $ownsOneMapper->populateAbstract($result, $mapper::createBaseModel($result)); } @@ -1024,9 +1064,6 @@ final class ReadMapper extends DataMapperAbstract * * @return mixed * - * @todo: in the future we could pass not only the $id ref but all of the data as a join!!! and save an additional select!!! - * @todo: only the belongs to model gets populated the children of the belongsto model are always null models. either this function needs to call the get for the children, it should call get for the belongs to right away like the has many, or i find a way to recursevily load the data for all sub models and then populate that somehow recursively, probably too complex. - * * @since 1.0.0 */ public function populateBelongsTo(string $member, array $result, mixed $default = null) : mixed @@ -1035,34 +1072,31 @@ final class ReadMapper extends DataMapperAbstract $mapper = $this->mapper::BELONGS_TO[$member]['mapper']; if (!isset($this->with[$member])) { - if (\array_key_exists($this->mapper::BELONGS_TO[$member]['external'] . '_d' . ($this->depth), $result)) { + if (\array_key_exists($this->mapper::BELONGS_TO[$member]['external'] . '_d' . $this->depth . $this->joinAlias, $result)) { return isset($this->mapper::BELONGS_TO[$member]['column']) - ? $result[$this->mapper::BELONGS_TO[$member]['external'] . '_d' . ($this->depth)] - : $mapper::createNullModel($result[$this->mapper::BELONGS_TO[$member]['external'] . '_d' . ($this->depth)]); + ? $result[$this->mapper::BELONGS_TO[$member]['external'] . '_d' . $this->depth . $this->joinAlias] + : $mapper::createNullModel($result[$this->mapper::BELONGS_TO[$member]['external'] . '_d' . $this->depth . $this->joinAlias]); } else { return $default; } - } - - if (isset($this->mapper::BELONGS_TO[$member]['column'])) { - return $result[$mapper::getColumnByMember($this->mapper::BELONGS_TO[$member]['column']) . '_d' . $this->depth]; - } - - if (!isset($result[$mapper::PRIMARYFIELD . '_d' . ($this->depth + 1)])) { + } elseif (isset($this->mapper::BELONGS_TO[$member]['column'])) { + return $result[$mapper::getColumnByMember($this->mapper::BELONGS_TO[$member]['column']) . '_d' . $this->depth . '_' . $member]; + } elseif (!isset($result[$mapper::PRIMARYFIELD . '_d' . ($this->depth + 1) . '_' . $member])) { return $mapper::createNullModel(); - } + } elseif (isset($this->mapper::BELONGS_TO[$member]['by'])) { + // get the belongs to based on a different column (not primary key) + // this is often used if the value is actually a different model: + // you want the profile but the account id is referenced + // in this case you can get the profile by loading the profile based on the account reference column - // get the belongs to based on a different column (not primary key) - // this is often used if the value is actually a different model: - // you want the profile but the account id is referenced - // in this case you can get the profile by loading the profile based on the account reference column - if (isset($this->mapper::BELONGS_TO[$member]['by'])) { /** @var self $belongsToMapper */ $belongsToMapper = $this->createRelationMapper($mapper::get($this->db), $member); $belongsToMapper->depth = $this->depth + 1; + $belongsToMapper->joinAlias = '_' . $member; + $belongsToMapper->where( $this->mapper::BELONGS_TO[$member]['by'], - $result[$mapper::getColumnByMember($this->mapper::BELONGS_TO[$member]['by']) . '_d' . ($this->depth + 1)], + $result[$mapper::getColumnByMember($this->mapper::BELONGS_TO[$member]['by']) . '_d' . ($this->depth + 1) . '_' . $member], '=' ); @@ -1070,8 +1104,9 @@ final class ReadMapper extends DataMapperAbstract } /** @var self $belongsToMapper */ - $belongsToMapper = $this->createRelationMapper($mapper::get($this->db), $member); - $belongsToMapper->depth = $this->depth + 1; + $belongsToMapper = $this->createRelationMapper($mapper::get($this->db), $member); + $belongsToMapper->depth = $this->depth + 1; + $belongsToMapper->joinAlias = '_' . $member; return $belongsToMapper->populateAbstract($result, $mapper::createBaseModel($result)); } @@ -1079,27 +1114,32 @@ final class ReadMapper extends DataMapperAbstract /** * Fill object with relations * - * @param object $obj Object to fill + * @param object[] $objs Object to fill * * @return void * * @since 1.0.0 */ - public function loadHasManyRelations(object $obj) : void + public function loadHasManyRelations(array $objs) : void { - if (empty($this->with)) { - return; + $primaryKeys = []; + foreach ($objs as $idx => $obj) { + $key = $this->mapper::getObjectId($obj); + + if (!empty($key)) { + $primaryKeys[$idx] = $key; + } } - $primaryKey = $this->mapper::getObjectId($obj); - if (empty($primaryKey)) { + if (empty($primaryKeys)) { return; } $refClass = null; - // @todo: check if there are more cases where the relation is already loaded with joins etc. - // there can be pseudo has many elements like localizations. They are has manies but these are already loaded with joins! + // @todo Check if there are more cases where the relation is already loaded with joins etc. + // There can be pseudo hasMany elements like localizations. + // They are hasMany but these are already loaded with joins! foreach ($this->with as $member => $withData) { if (isset($this->mapper::HAS_MANY[$member])) { $many = $this->mapper::HAS_MANY[$member]; @@ -1110,66 +1150,87 @@ final class ReadMapper extends DataMapperAbstract $isPrivate = $withData['private'] ?? false; $objectMapper = $this->createRelationMapper($many['mapper']::get(db: $this->db), $member); - if ($many['external'] === null/* same as $many['table'] !== $many['mapper']::TABLE */) { - $objectMapper->where($many['mapper']::COLUMNS[$many['self']]['internal'], $primaryKey); + if ($many['external'] === null) { + $objectMapper->where($many['mapper']::COLUMNS[$many['self']]['internal'], $primaryKeys); + $objectMapper->indexedBy($many['self']); } else { $query = new Builder($this->db, true); - $query->leftJoin($many['table']) + $query + ->selectAs($many['table'] . '.' . $many['self'], $many['self'] . '_d' . $this->depth . $this->joinAlias) + ->leftJoin($many['table']) ->on($many['mapper']::TABLE . '_d1.' . $many['mapper']::PRIMARYFIELD, '=', $many['table'] . '.' . $many['external']) - ->where($many['table'] . '.' . $many['self'], '=', $primaryKey); + ->where($many['table'] . '.' . $many['self'], 'IN', $primaryKeys); // Cannot use join, because join only works on members and we don't have members for a relation table - // This is why we need to create a "base" query which contians the join on table columns + // This is why we need to create a "base" query which contains the join on table columns $objectMapper->query($query); + $objectMapper->indexedBy($many['self']); } $objects = $objectMapper->execute(); - if (empty($objects) || (!\is_array($objects) && $objects->id === 0)) { + if (empty($objects) || !\is_array($objects)) { continue; } if ($isPrivate) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); - $refProp = $refClass->getProperty($member); - $refProp->setValue($obj, !\is_array($objects) && ($many['conditional'] ?? false) === false - ? [$many['mapper']::getObjectId($objects) => $objects] - : $objects // if conditional === true the obj will be asigned (e.g. has many localizations but only one is loaded for the model) - ); + foreach ($primaryKeys as $idx => $key) { + if (!isset($objects[$key])) { + continue; + } + + $refProp = $refClass->getProperty($member); + $refProp->setValue($objs[$idx], !\is_array($objects[$key]) && ($many['conditional'] ?? false) === false + ? [$many['mapper']::getObjectId($objects[$key]) => $objects[$key]] + : $objects[$key] // if conditional === true the obj will be assigned (e.g. hasMany localizations but only one is loaded for the model) + ); + } } else { - $obj->{$member} = !\is_array($objects) && ($many['conditional'] ?? false) === false - ? [$many['mapper']::getObjectId($objects) => $objects] - : $objects; // if conditional === true the obj will be asigned (e.g. has many localizations but only one is loaded for the model) + foreach ($primaryKeys as $idx => $key) { + if (!isset($objects[$key])) { + continue; + } + + $objs[$idx]->{$member} = !\is_array($objects[$key]) && ($many['conditional'] ?? false) === false + ? [$many['mapper']::getObjectId($objects[$key]) => $objects[$key]] + : $objects[$key]; // if conditional === true the obj will be assigned (e.g. hasMany localizations but only one is loaded for the model) + } } continue; } elseif (isset($this->mapper::OWNS_ONE[$member]) || isset($this->mapper::BELONGS_TO[$member]) ) { - $relation = isset($this->mapper::OWNS_ONE[$member]) - ? $this->mapper::OWNS_ONE[$member] - : $this->mapper::BELONGS_TO[$member]; - if (\count($withData) < 2) { continue; } + $relation = isset($this->mapper::OWNS_ONE[$member]) + ? $this->mapper::OWNS_ONE[$member] + : $this->mapper::BELONGS_TO[$member]; + /** @var ReadMapper $relMapper */ $relMapper = $this->createRelationMapper($relation['mapper']::reader($this->db), $member); - $isPrivate = $withData['private'] ?? false; + $isPrivate = $relation['private'] ?? false; + $tempObjs = []; + if ($isPrivate) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $refProp = $refClass->getProperty($member); - $relMapper->loadHasManyRelations($refProp->getValue($obj)); + + foreach ($objs as $obj) { + $tempObjs[] = $refProp->getValue($obj); + } } else { - $relMapper->loadHasManyRelations($obj->{$member}); + foreach ($objs as $obj) { + $tempObjs[] = $obj->{$member}; + } } + + $relMapper->loadHasManyRelations($tempObjs); } } } @@ -1196,8 +1257,9 @@ final class ReadMapper extends DataMapperAbstract $refClass = null; - // @todo: check if there are more cases where the relation is already loaded with joins etc. - // there can be pseudo has many elements like localizations. They are has manies but these are already loaded with joins! + // @performance Check if there are more cases where the relation is already loaded with joins etc. + // There can be pseudo hasMany elements like localizations. They are hasMany but these are already loaded with joins! + // Variation of https://github.com/Karaka-Management/phpOMS/issues/363 foreach ($this->with as $member => $withData) { if (isset($this->mapper::HAS_MANY[$member])) { $many = $this->mapper::HAS_MANY[$member]; @@ -1205,8 +1267,7 @@ final class ReadMapper extends DataMapperAbstract continue; } - // @todo: withData doesn't store this directly, it is in [0]['private] ?!?! - $isPrivate = $withData['private'] ?? false; + $isPrivate = $many['private'] ?? false; $objectMapper = $this->createRelationMapper($many['mapper']::exists(db: $this->db), $member); if ($many['external'] === null/* same as $many['table'] !== $many['mapper']::TABLE */) { @@ -1218,35 +1279,30 @@ final class ReadMapper extends DataMapperAbstract ->where($many['table'] . '.' . $many['self'], '=', $primaryKey); // Cannot use join, because join only works on members and we don't have members for a relation table - // This is why we need to create a "base" query which contians the join on table columns + // This is why we need to create a "base" query which contains the join on table columns $objectMapper->query($query); } $objects = $objectMapper->execute(); - if (empty($objects) || $objects === false) { - return false; - } - return true; + return !empty($objects) && $objects !== false; } elseif (isset($this->mapper::OWNS_ONE[$member]) || isset($this->mapper::BELONGS_TO[$member]) ) { - $relation = isset($this->mapper::OWNS_ONE[$member]) - ? $this->mapper::OWNS_ONE[$member] - : $this->mapper::BELONGS_TO[$member]; - if (\count($withData) < 2) { continue; } + $relation = isset($this->mapper::OWNS_ONE[$member]) + ? $this->mapper::OWNS_ONE[$member] + : $this->mapper::BELONGS_TO[$member]; + /** @var ReadMapper $relMapper */ $relMapper = $this->createRelationMapper($relation['mapper']::reader($this->db), $member); - $isPrivate = $withData['private'] ?? false; + $isPrivate = $relation['private'] ?? false; if ($isPrivate) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $refProp = $refClass->getProperty($member); return $relMapper->hasManyRelations($refProp->getValue($obj)); diff --git a/DataStorage/Database/Mapper/UpdateMapper.php b/DataStorage/Database/Mapper/UpdateMapper.php index 881281cc8..9407d2006 100755 --- a/DataStorage/Database/Mapper/UpdateMapper.php +++ b/DataStorage/Database/Mapper/UpdateMapper.php @@ -102,7 +102,7 @@ final class UpdateMapper extends DataMapperAbstract * * @since 1.0.0 */ - private function updateModel(object $obj, mixed $objId, \ReflectionClass &$refClass = null) : void + private function updateModel(object $obj, mixed $objId, ?\ReflectionClass &$refClass = null) : void { try { // Model doesn't have anything to update @@ -115,7 +115,10 @@ final class UpdateMapper extends DataMapperAbstract ->where($this->mapper::TABLE . '.' . $this->mapper::PRIMARYFIELD, '=', $objId); foreach ($this->mapper::COLUMNS as $column) { - $propertyName = \stripos($column['internal'], '/') !== false ? \explode('/', $column['internal'])[0] : $column['internal']; + $propertyName = \stripos($column['internal'], '/') !== false + ? \explode('/', $column['internal'])[0] + : $column['internal']; + if (isset($this->mapper::HAS_MANY[$propertyName]) || $column['internal'] === $this->mapper::PRIMARYFIELD || (($column['readonly'] ?? false) && !isset($this->with[$propertyName])) @@ -129,9 +132,7 @@ final class UpdateMapper extends DataMapperAbstract $tValue = null; if ($isPrivate) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $property = $refClass->getProperty($propertyName); $tValue = $property->getValue($obj); @@ -161,9 +162,6 @@ final class UpdateMapper extends DataMapperAbstract } } - // @todo: - // @bug: Sqlite doesn't allow table_name.column_name in set queries for whatver reason. - $sth = $this->db->con->prepare($query->toSql()); if ($sth !== false) { $sth->execute(); @@ -196,6 +194,14 @@ final class UpdateMapper extends DataMapperAbstract /** @var class-string $mapper */ $mapper = $this->mapper::BELONGS_TO[$propertyName]['mapper']; + if (!isset($this->with[$propertyName])) { + $id = $mapper::getObjectId($obj); + + return empty($id) && $mapper::isNullModel($obj) + ? null + : $id; + } + /** @var self $relMapper */ $relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName); $relMapper->depth = $this->depth + 1; @@ -218,6 +224,14 @@ final class UpdateMapper extends DataMapperAbstract /** @var class-string $mapper */ $mapper = $this->mapper::OWNS_ONE[$propertyName]['mapper']; + if (!isset($this->with[$propertyName])) { + $id = $mapper::getObjectId($obj); + + return empty($id) && $mapper::isNullModel($obj) + ? null + : $id; + } + /** @var self $relMapper */ $relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName); $relMapper->depth = $this->depth + 1; @@ -238,7 +252,7 @@ final class UpdateMapper extends DataMapperAbstract * * @since 1.0.0 */ - private function updateHasMany(object $obj, mixed $objId, \ReflectionClass &$refClass = null) : void + private function updateHasMany(object $obj, mixed $objId, ?\ReflectionClass &$refClass = null) : void { if (empty($this->with) || empty($this->mapper::HAS_MANY)) { return; @@ -260,9 +274,7 @@ final class UpdateMapper extends DataMapperAbstract $values = null; if ($isPrivate) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $property = $refClass->getProperty($propertyName); $values = $property->getValue($obj); @@ -346,7 +358,7 @@ final class UpdateMapper extends DataMapperAbstract $query = new Builder($this->db); $src = $many['external'] ?? $many['mapper']::PRIMARYFIELD; - // @todo: what if a specific column name is defined instead of primaryField for the join? Fix, it should be stored in 'column' + // @todo what if a specific column name is defined instead of primaryField for the join? Fix, it should be stored in 'column' $query->select($many['table'] . '.' . $src) ->from($many['table']) ->where($many['table'] . '.' . $many['self'], '=', $objId); @@ -375,7 +387,7 @@ final class UpdateMapper extends DataMapperAbstract $this->mapper::remover(db: $this->db)->deleteRelationTable($member, $removes, $objId); } - if (!empty($adds)) { + if (!empty($adds) && isset($this->mapper::HAS_MANY[$member]['external'])) { $this->mapper::writer(db: $this->db)->createRelationTable($member, $adds, $objId); } } diff --git a/DataStorage/Database/Mapper/WriteMapper.php b/DataStorage/Database/Mapper/WriteMapper.php index bc7d5f068..fbb45bead 100755 --- a/DataStorage/Database/Mapper/WriteMapper.php +++ b/DataStorage/Database/Mapper/WriteMapper.php @@ -26,6 +26,9 @@ use phpOMS\Utils\ArrayUtils; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Lock data for concurrency (e.g. table row lock or heartbeat) + * https://github.com/Karaka-Management/Karaka/issues/152 */ final class WriteMapper extends DataMapperAbstract { @@ -74,20 +77,16 @@ final class WriteMapper extends DataMapperAbstract */ public function executeCreate(object $obj) : mixed { - $refClass = null; - - if ($this->mapper::isNullModel($obj)) { - $objId = $this->mapper::getObjectId($obj); - + $objId = $this->mapper::getObjectId($obj); + if ((!empty($objId) && $this->mapper::AUTOINCREMENT) + || $this->mapper::isNullModel($obj) + ) { return $objId === 0 ? null : $objId; } - if (!empty($id = $this->mapper::getObjectId($obj)) && $this->mapper::AUTOINCREMENT) { - $objId = $id; - } else { - $objId = $this->createModel($obj, $refClass); - $this->mapper::setObjectId($obj, $objId, $refClass); - } + $refClass = null; + $objId = $this->createModel($obj, $refClass); + $this->mapper::setObjectId($obj, $objId, $refClass); $this->createHasMany($obj, $objId, $refClass); @@ -104,7 +103,7 @@ final class WriteMapper extends DataMapperAbstract * * @since 1.0.0 */ - private function createModel(object $obj, \ReflectionClass &$refClass = null) : mixed + private function createModel(object $obj, ?\ReflectionClass &$refClass = null) : mixed { try { $query = new Builder($this->db); @@ -123,9 +122,7 @@ final class WriteMapper extends DataMapperAbstract $tValue = null; if ($column['private'] ?? false) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $property = $refClass->getProperty($propertyName); $tValue = $property->getValue($obj); @@ -231,7 +228,6 @@ final class WriteMapper extends DataMapperAbstract if (isset($this->mapper::BELONGS_TO[$propertyName]['by'])) { // has by (obj is stored as a different model e.g. model = profile but reference/db is account) - if ($this->mapper::BELONGS_TO[$propertyName]['private'] ?? false) { $refClass = new \ReflectionClass($obj); $refProp = $refClass->getProperty($this->mapper::BELONGS_TO[$propertyName]['by']); @@ -245,8 +241,11 @@ final class WriteMapper extends DataMapperAbstract $mapper = $this->mapper::BELONGS_TO[$propertyName]['mapper']; $primaryKey = $mapper::getObjectId($obj); - // @todo: the $mapper::create() might cause a problem if 'by' is set. because we don't want to create this obj but the child obj. - return empty($primaryKey) ? $mapper::create(db: $this->db)->execute($obj) : $primaryKey; + // @bug The $mapper::create() might cause a problem if 'by' is set. + // This is because we don't want to create this obj but the child obj. + return empty($primaryKey) + ? $mapper::create(db: $this->db)->execute($obj) + : $primaryKey; } /** @@ -262,7 +261,7 @@ final class WriteMapper extends DataMapperAbstract * * @since 1.0.0 */ - private function createHasMany(object $obj, mixed $objId, \ReflectionClass &$refClass = null) : void + private function createHasMany(object $obj, mixed $objId, ?\ReflectionClass &$refClass = null) : void { foreach ($this->mapper::HAS_MANY as $propertyName => $rel) { if (!isset($this->mapper::HAS_MANY[$propertyName]['mapper'])) { @@ -274,9 +273,7 @@ final class WriteMapper extends DataMapperAbstract $values = null; if ($isPrivate) { - if ($refClass === null) { - $refClass = new \ReflectionClass($obj); - } + $refClass ??= new \ReflectionClass($obj); $property = $refClass->getProperty($propertyName); $values = $property->getValue($obj); @@ -286,16 +283,12 @@ final class WriteMapper extends DataMapperAbstract /** @var class-string $mapper */ $mapper = $this->mapper::HAS_MANY[$propertyName]['mapper']; - $internalName = isset($mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]) - ? $mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['internal'] - : 'ERROR'; - - // @todo: this or $isRelPrivate is wrong, don't know which one. - $isInternalPrivate =$mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['private'] ?? false; + $internalName = $mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['internal'] ?? 'ERROR-BAD-SELF'; + $isRelPrivate = $mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['private'] ?? false; if (\is_object($values)) { // conditionals - if ($isInternalPrivate) { + if ($isRelPrivate) { $relReflectionClass = new \ReflectionClass($values); $relProperty = $relReflectionClass->getProperty($internalName); @@ -306,14 +299,13 @@ final class WriteMapper extends DataMapperAbstract $mapper::create(db: $this->db)->execute($values); continue; - } elseif (!\is_array($values)) { - // @todo: conditionals??? + } elseif (!\is_array($values) || empty($values)) { + // @todo conditionals? continue; } $objsIds = []; - $isRelPrivate = $mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['private'] ?? false; - $relReflectionClass = $isRelPrivate && !empty($values) ? new \ReflectionClass(\reset($values)) : null; + $relReflectionClass = $isRelPrivate ? new \ReflectionClass(\reset($values)) : null; foreach ($values as $key => $value) { if (!\is_object($value)) { @@ -339,14 +331,13 @@ final class WriteMapper extends DataMapperAbstract $relProperty = $relReflectionClass->getProperty($internalName); } - // @todo maybe consider to just set the column type to object, and then check for that (might be faster) if (isset($mapper::BELONGS_TO[$internalName]) || isset($mapper::OWNS_ONE[$internalName]) ) { if ($isRelPrivate) { $relProperty->setValue($value, $this->mapper::createNullModel($objId)); } else { - $value->{$internalName} = $this->mapper::createNullModel($objId); + $value->{$internalName} = $this->mapper::createNullModel($objId); } } elseif ($isRelPrivate) { $relProperty->setValue($value, $objId); @@ -355,10 +346,18 @@ final class WriteMapper extends DataMapperAbstract } } + // @performance This inserts one element at a time. SQL allows to insert multiple rows. + // The problem with this is, that we then need to manually calculate the objIds + // since lastInsertId returns the first generated id. + // However, the current use case in Jingga only rarely has multiple hasMany during the creation + // since we are calling the API individually. + // https://github.com/Karaka-Management/phpOMS/issues/370 $objsIds[$key] = $mapper::create(db: $this->db)->execute($value); } - $this->createRelationTable($propertyName, $objsIds, $objId); + if (!empty($objsIds) && isset($this->mapper::HAS_MANY[$propertyName]['external'])) { + $this->createRelationTable($propertyName, $objsIds, $objId); + } } } @@ -376,9 +375,11 @@ final class WriteMapper extends DataMapperAbstract public function createRelationTable(string $propertyName, array $objsIds, mixed $objId) : void { try { + /* This check got pulled out to avoid function call to begin with if (empty($objsIds) || !isset($this->mapper::HAS_MANY[$propertyName]['external'])) { return; } + */ $relQuery = new Builder($this->db); $relQuery->into($this->mapper::HAS_MANY[$propertyName]['table']) diff --git a/DataStorage/Database/Query/Builder.php b/DataStorage/Database/Query/Builder.php index 6356e706d..8698e2eb6 100755 --- a/DataStorage/Database/Query/Builder.php +++ b/DataStorage/Database/Query/Builder.php @@ -18,6 +18,7 @@ use phpOMS\Algorithm\Graph\DependencyResolver; use phpOMS\Contract\SerializableInterface; use phpOMS\DataStorage\Database\BuilderAbstract; use phpOMS\DataStorage\Database\Connection\ConnectionAbstract; +use phpOMS\DataStorage\Database\Query\Grammar\Grammar; /** * Database query builder. @@ -26,9 +27,22 @@ use phpOMS\DataStorage\Database\Connection\ConnectionAbstract; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @question Consider to delete the builder but create a Select, Insert, ... builder + * Then directly call the compileSelect + compileFrom ... from the toSql + * This way the object generated would be much slimmer since we don't need to initialize empty data for + * Insert etc. We also wouldn't have to call compileComponents since this would happen directly in toSql(). */ class Builder extends BuilderAbstract { + /** + * Grammar. + * + * @var Grammar + * @since 1.0.0 + */ + protected Grammar $grammar; + /** * Log queries. * @@ -62,7 +76,9 @@ class Builder extends BuilderAbstract public array $updates = []; /** - * Stupid work around because value needs to be not null for it to work in Grammar. + * Deletes. + * + * @todo Find fix for stupid work around because value needs to be not null for it to work in Grammar. * * @var array * @since 1.0.0 @@ -231,6 +247,8 @@ class Builder extends BuilderAbstract 'similar to', 'not similar to', 'in', + 'exists', + 'not exists', ]; /** @@ -352,7 +370,16 @@ class Builder extends BuilderAbstract $this->resolveJoinDependencies(); } - $query = $this->grammar->compileQuery($this); + $components = $this->grammar->compileComponents($this); + $queryString = ''; + + foreach ($components as $component) { + if ($component !== '') { + $queryString .= $component . ' '; + } + } + + $query = \substr($queryString, 0, -1) . ';'; if (self::$log) { \phpOMS\Log\FileLogger::getInstance()->debug($query); @@ -391,13 +418,13 @@ class Builder extends BuilderAbstract } // add from to existing dependencies - foreach ($this->from as $table => $from) { + foreach ($this->from as $table => $_) { $dependencies[$table] = []; } $resolved = DependencyResolver::resolve($dependencies); - // cyclomatic dependencies + // cyclic dependencies if ($resolved === null) { return; } @@ -536,7 +563,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function where(string | array | self $columns, string | array $operator = null, mixed $values = null, string | array $boolean = 'and') : self + public function where(string | array | self $columns, string | array|null $operator = null, mixed $values = null, string | array $boolean = 'and') : self { if (!\is_array($columns)) { $columns = [$columns]; @@ -575,7 +602,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function andWhere(string | array | Where $where, string | array $operator = null, mixed $values = null) : self + public function andWhere(string | array | Where $where, string | array|null $operator = null, mixed $values = null) : self { return $this->where($where, $operator, $values, 'and'); } @@ -591,7 +618,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function orWhere(string | array | self $where, string | array $operator = null, mixed $values = null) : self + public function orWhere(string | array | self $where, string | array|null $operator = null, mixed $values = null) : self { return $this->where($where, $operator, $values, 'or'); } @@ -814,81 +841,88 @@ class Builder extends BuilderAbstract */ public function __toString() { - return $this->grammar->compileQuery($this); - } + $components = $this->grammar->compileComponents($this); + $queryString = ''; - /** - * Find query. - * - * @return void - * - * @since 1.0.0 - */ - public function find() : void - { + foreach ($components as $component) { + if ($component !== '') { + $queryString .= $component . ' '; + } + } + + return \substr($queryString, 0, -1) . ';'; } /** * Count results. * - * @param string $table Table to count the result set + * @param string $column Table to count the result set * * @return Builder * * @since 1.0.0 */ - public function count(string $table = '*') : self + public function count(string $column = '*', ?string $as = null) : self { - /** - * @todo - * Don't do this as a string, create a new object $this->select(new Count($table)). - * The parser should be able to handle this much better - */ - return $this->select('COUNT(' . $table . ')'); + return $as === null + ? $this->select('COUNT(' . $column . ')') + : $this->selectAs('COUNT(' . $column . ')', $as); } /** * Select minimum. * - * @return void + * @return Builder * * @since 1.0.0 */ - public function min() : void + public function min(string $column = '*', ?string $as = null) : self { + return $as === null + ? $this->select('MIN(' . $column . ')') + : $this->selectAs('MIN(' . $column . ')', $as); } /** * Select maximum. * - * @return void + * @return Builder * * @since 1.0.0 */ - public function max() : void + public function max(string $column = '*', ?string $as = null) : self { + return $as === null + ? $this->select('MAX(' . $column . ')') + : $this->selectAs('MAX(' . $column . ')', $as); } /** * Select sum. * - * @return void + * @return Builder * * @since 1.0.0 */ - public function sum() : void + public function sum(string $column = '*', ?string $as = null) : self { + return $as === null + ? $this->select('SUM(' . $column . ')') + : $this->selectAs('SUM(' . $column . ')', $as); } /** * Select average. * - * @return void + * @return Builder * * @since 1.0.0 */ - public function avg() : void + public function avg(string $column = '*', ?string $as = null) : self { + return $as === null + ? $this->select('AVG(' . $column . ')') + : $this->selectAs('AVG(' . $column . ')', $as); } /** @@ -974,7 +1008,7 @@ class Builder extends BuilderAbstract { \end($this->values); - $key = \key($this->values); + $key = \key($this->values); $key ??= 0; if (\is_array($value)) { @@ -1062,28 +1096,6 @@ class Builder extends BuilderAbstract return $this; } - /** - * Increment value. - * - * @return void - * - * @since 1.0.0 - */ - public function increment() : void - { - } - - /** - * Decrement value. - * - * @return void - * - * @since 1.0.0 - */ - public function decrement() : void - { - } - /** * Join. * @@ -1095,7 +1107,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function join(string | self $table, string $type = JoinType::JOIN, string $alias = null) : self + public function join(string | self $table, string $type = JoinType::JOIN, ?string $alias = null) : self { $this->joins[$alias ?? $table] = ['type' => $type, 'table' => $table, 'alias' => $alias]; @@ -1112,7 +1124,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function leftJoin(string | self $table, string $alias = null) : self + public function leftJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::LEFT_JOIN, $alias); } @@ -1127,7 +1139,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function leftOuterJoin(string | self $table, string $alias = null) : self + public function leftOuterJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::LEFT_OUTER_JOIN, $alias); } @@ -1142,7 +1154,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function leftInnerJoin(string | self $table, string $alias = null) : self + public function leftInnerJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::LEFT_INNER_JOIN, $alias); } @@ -1157,7 +1169,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function rightJoin(string | self $table, string $alias = null) : self + public function rightJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::RIGHT_JOIN, $alias); } @@ -1172,7 +1184,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function rightOuterJoin(string | self $table, string $alias = null) : self + public function rightOuterJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::RIGHT_OUTER_JOIN, $alias); } @@ -1187,7 +1199,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function rightInnerJoin(string | self $table, string $alias = null) : self + public function rightInnerJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::RIGHT_INNER_JOIN, $alias); } @@ -1202,7 +1214,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function outerJoin(string | self $table, string $alias = null) : self + public function outerJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::OUTER_JOIN, $alias); } @@ -1217,7 +1229,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function innerJoin(string | self $table, string $alias = null) : self + public function innerJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::INNER_JOIN, $alias); } @@ -1232,7 +1244,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function crossJoin(string | self $table, string $alias = null) : self + public function crossJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::CROSS_JOIN, $alias); } @@ -1247,7 +1259,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function fullJoin(string | self $table, string $alias = null) : self + public function fullJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::FULL_JOIN, $alias); } @@ -1262,7 +1274,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function fullOuterJoin(string | self $table, string $alias = null) : self + public function fullOuterJoin(string | self $table, ?string $alias = null) : self { return $this->join($table, JoinType::FULL_OUTER_JOIN, $alias); } @@ -1285,7 +1297,7 @@ class Builder extends BuilderAbstract * @param string|array $columns Columns to join on * @param null|string|array $operator Comparison operator * @param null|string|array $values Values to compare with - * @param string|array $boolean Concatonator + * @param string|array $boolean Concatenation * @param null|string $table Table this belongs to * * @return Builder @@ -1294,7 +1306,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function on(string | array $columns, string | array $operator = null, mixed $values = null, string | array $boolean = 'and', string $table = null) : self + public function on(string | array $columns, string | array|null $operator = null, mixed $values = null, string | array $boolean = 'and', ?string $table = null) : self { if (!\is_array($columns)) { $columns = [$columns]; @@ -1305,7 +1317,7 @@ class Builder extends BuilderAbstract $joinCount = \count($this->joins) - 1; $i = 0; - $table ??= \array_keys($this->joins)[$joinCount]; + $table ??= \array_keys($this->joins)[$joinCount]; foreach ($columns as $column) { if (isset($operator[$i]) && !\in_array(\strtolower($operator[$i]), self::OPERATORS)) { @@ -1336,7 +1348,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function orOn(string | array $columns, string | array $operator = null, string | array $values = null) : self + public function orOn(string | array $columns, string | array|null $operator = null, string | array|null $values = null) : self { return $this->on($columns, $operator, $values, 'or'); } @@ -1352,34 +1364,21 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function andOn(string | array $columns, string | array $operator = null, string | array $values = null) : self + public function andOn(string | array $columns, string | array|null $operator = null, string | array|null $values = null) : self { return $this->on($columns, $operator, $values, 'and'); } - /** - * Merging query. - * - * Merging query in order to remove database query volume - * - * @return Builder - * - * @since 1.0.0 - */ - public function merge() : self - { - return clone($this); - } - /** * {@inheritdoc} */ public function execute() : ?\PDOStatement { $sth = null; + $sql = ''; try { - $sth = $this->connection->con->prepare($this->toSql()); + $sth = $this->connection->con->prepare($sql = $this->toSql()); if ($sth === false) { return null; } @@ -1395,7 +1394,7 @@ class Builder extends BuilderAbstract // @codeCoverageIgnoreStart \phpOMS\Log\FileLogger::getInstance()->error( \phpOMS\Log\FileLogger::MSG_FULL, [ - 'message' => $t->getMessage() . ':' . $this->toSql(), + 'message' => $t->getMessage() . ':' . $sql, 'line' => __LINE__, 'file' => self::class, ] @@ -1445,8 +1444,6 @@ class Builder extends BuilderAbstract { if (\is_string($column)) { return $column; - } elseif ($column instanceof Column) { - return $column->getColumn(); } elseif ($column instanceof SerializableInterface) { return $column->serialize(); } elseif ($column instanceof self) { diff --git a/DataStorage/Database/Query/Column.php b/DataStorage/Database/Query/Column.php deleted file mode 100755 index 1311415c5..000000000 --- a/DataStorage/Database/Query/Column.php +++ /dev/null @@ -1,27 +0,0 @@ -name = $name; + } } diff --git a/DataStorage/Database/Query/Concat.php b/DataStorage/Database/Query/Concat.php new file mode 100644 index 000000000..bfe92490f --- /dev/null +++ b/DataStorage/Database/Query/Concat.php @@ -0,0 +1,67 @@ +type = QueryType::SELECT; + } + + public function columns(string $as, string $delim, ...$columns) : void + { + $this->delim = $delim; + $this->as = $as; + + $this->select($columns); + } + + /** + * {@inheritdoc} + */ + public function toSql() : string + { + $query = $this->grammar->compileConcat($this, $this->selects); + + if (self::$log) { + \phpOMS\Log\FileLogger::getInstance()->debug($query); + } + + return $query; + } +} \ No newline at end of file diff --git a/DataStorage/Database/Query/Count.php b/DataStorage/Database/Query/Count.php deleted file mode 100755 index 80f2cee39..000000000 --- a/DataStorage/Database/Query/Count.php +++ /dev/null @@ -1,27 +0,0 @@ -expressionizeTableColumn($columns) . ') AS ' . $query->as; + } + /** * Compile select. * @@ -243,21 +256,21 @@ class Grammar extends GrammarAbstract * Compile where. * * @param Builder $query Builder - * @param array $wheres Where elmenets - * @param bool $first Is first element (usefull for nesting) + * @param array $wheres Where elements + * @param bool $first Is first element (useful for nesting) * * @return string * * @since 1.0.0 */ - protected function compileWheres(Builder $query, array $wheres, bool $first = true) : string + public function compileWheres(Builder $query, array $wheres, bool $first = true) : string { $expression = ''; foreach ($wheres as $where) { foreach ($where as $element) { $expression .= $this->compileWhereElement($element, $query, $first); - $first = false; + $first = false; } } @@ -273,7 +286,7 @@ class Grammar extends GrammarAbstract * * @param array $element Element data * @param Builder $query Query builder - * @param bool $first Is first element (usefull for nesting) + * @param bool $first Is first element (useful for nesting) * * @return string * @@ -282,58 +295,46 @@ class Grammar extends GrammarAbstract protected function compileWhereElement(array $element, Builder $query, bool $first = true) : string { $expression = ''; + $prefix = ''; if (!$first) { - $expression = ' ' . \strtoupper($element['boolean']) . ' '; + $prefix = ' ' . \strtoupper($element['boolean']) . ' '; } if (\is_string($element['column'])) { $expression .= $this->compileSystem($element['column']); - } elseif ($element['column'] instanceof \Closure) { - $expression .= $element['column'](); - } elseif ($element['column'] instanceof Where) { - $where = \rtrim($this->compileWhereQuery($element['column']), ';'); - $expression .= '(' . (\str_starts_with($where, 'WHERE ') ? \substr($where, 6) : $where) . ')'; } elseif ($element['column'] instanceof Builder) { $expression .= '(' . \rtrim($element['column']->toSql(), ';') . ')'; + } elseif ($element['column'] instanceof \Closure) { + $expression .= $element['column'](); } - if (isset($element['value']) && (!empty($element['value']) || !\is_array($element['value']))) { + // Handle null for IN (...) + // This is not allowed and must be written as (IN (...) OR IS NULL) + $isArray = \is_array($element['value']); + $hasNull = false; + if ($isArray && ($key = \array_search(null, $element['value'], true)) !== false) { + $hasNull = true; + unset($element['value'][$key]); + + if (empty($element['value'])) { + $element['operator'] = '='; + $element['value'] = null; + } + } + + if (isset($element['value']) && (!empty($element['value']) || !$isArray)) { $expression .= ' ' . \strtoupper($element['operator']) . ' ' . $this->compileValue($query, $element['value']); + + if ($hasNull) { + $expression = '(' . $expression . ' OR ' . $this->compileSystem($element['column']) . ' IS NULL)'; + } } elseif ($element['value'] === null && !($element['column'] instanceof Builder)) { - $operator = $element['operator'] === '=' ? 'IS' : 'IS NOT'; + $operator = $element['operator'] === '=' ? 'IS' : 'IS NOT'; $expression .= ' ' . $operator . ' ' . $this->compileValue($query, $element['value']); } - return $expression; - } - - /** - * Compile where query. - * - * @param Where $where Where query - * - * @return string - * - * @since 1.0.0 - */ - protected function compileWhereQuery(Where $where) : string - { - return $where->toSql(); - } - - /** - * Compile from query. - * - * @param From $from Where query - * - * @return string - * - * @since 1.0.0 - */ - protected function compileFromQuery(From $from) : string - { - return $from->toSql(); + return $prefix . $expression; } /** @@ -414,7 +415,7 @@ class Grammar extends GrammarAbstract foreach ($ons as $on) { $expression .= $this->compileOnElement($on, $query, $first); - $first = false; + $first = false; } if ($expression === '') { @@ -429,7 +430,7 @@ class Grammar extends GrammarAbstract * * @param array $element Element data * @param Builder $query Query builder - * @param bool $first Is first element (usefull for nesting) + * @param bool $first Is first element (useful for nesting) * * @return string * @@ -444,25 +445,27 @@ class Grammar extends GrammarAbstract } if (\is_string($element['column'])) { - // handle bug when no table is specified in the where column + // @bug Handle bug when no table is specified in the where column if (\count($query->from) === 1 && \stripos($element['column'], '.') === false) { $element['column'] = $query->from[0] . '.' . $element['column']; } $expression .= $this->compileSystem($element['column']); - } elseif ($element['column'] instanceof \Closure) { - $expression .= $element['column'](); } elseif ($element['column'] instanceof Builder) { $expression .= '(' . $element['column']->toSql() . ')'; - } elseif ($element['column'] instanceof Where) { - $expression .= '(' . \rtrim($this->compileWhereQuery($element['column']), ';') . ')'; + } elseif ($element['column'] instanceof \Closure) { + $expression .= $element['column'](); } - // @todo: on doesn't allow values as value (only table column names). This is bad and needs to be fixed! + // @bug The on part of a join doesn't allow string values because they conflict with column name + // Other data types are possible because they don't conflict with the data type of columns (string) + // Consider to create a ColumnName() class. + // https://github.com/Karaka-Management/phpOMS/issues/369 if (isset($element['value'])) { - $expression .= ' ' . \strtoupper($element['operator']) . ' ' . $this->compileSystem($element['value']); + $expression .= ' ' . \strtoupper($element['operator']) . ' ' + . (\is_string($element['value']) ? $this->compileSystem($element['value']) : $element['value']); } else { - $operator = $element['operator'] === '=' ? 'IS' : 'IS NOT'; + $operator = $element['operator'] === '=' ? 'IS' : 'IS NOT'; $expression .= ' ' . $operator . ' ' . $this->compileValue($query, $element['value']); } diff --git a/DataStorage/Database/Query/Grammar/GrammarInterface.php b/DataStorage/Database/Query/Grammar/GrammarInterface.php deleted file mode 100755 index b09613f33..000000000 --- a/DataStorage/Database/Query/Grammar/GrammarInterface.php +++ /dev/null @@ -1,27 +0,0 @@ -compileFrom($query, $query->from) . ' ' . $this->compileWheres($query, $query->wheres) . ' ORDER BY \rand() ' . $this->compileLimit($query, $query->limit ?? 1); + return 'SELECT ' . $expression + . ' ' . $this->compileFrom($query, $query->from) + . ' ' . $this->compileWheres($query, $query->wheres) + . ' ORDER BY RAND() ' + . $this->compileLimit($query, $query->limit ?? 1); } } diff --git a/DataStorage/Database/Query/Grammar/SQLiteGrammar.php b/DataStorage/Database/Query/Grammar/SQLiteGrammar.php index 9f7cd6f96..8ef555641 100755 --- a/DataStorage/Database/Query/Grammar/SQLiteGrammar.php +++ b/DataStorage/Database/Query/Grammar/SQLiteGrammar.php @@ -62,4 +62,22 @@ class SQLiteGrammar extends Grammar return 'SELECT ' . $expression . ' ' . $this->compileFrom($query, $query->from) . ' ORDER BY RANDOM() ' . $this->compileLimit($query, $query->limit ?? 1); } + + /** + * Create concat + * + * @param Concat $query Builder + * @param array $columns Columns + * + * @return string + * + * @since 1.0.0 + */ + public function compileConcat(\phpOMS\DataStorage\Database\Query\Concat $query, array $columns) : string + { + $sql = $this->expressionizeTableColumn($columns); + $sql = \str_replace(',', ' ||', $sql); + + return $sql . ' AS ' . $query->as; + } } diff --git a/DataStorage/Database/Query/Into.php b/DataStorage/Database/Query/Into.php deleted file mode 100755 index 9f42cc754..000000000 --- a/DataStorage/Database/Query/Into.php +++ /dev/null @@ -1,27 +0,0 @@ -type = QueryType::SELECT; } + + /** + * {@inheritdoc} + */ + public function toSql() : string + { + $query = $this->grammar->compileWheres($this, $this->wheres); + $query = \str_starts_with($query, 'WHERE ') ? \substr($query, 6) : $query; + + if (self::$log) { + \phpOMS\Log\FileLogger::getInstance()->debug($query); + } + + return $query; + } } diff --git a/DataStorage/Database/Schema/Builder.php b/DataStorage/Database/Schema/Builder.php index 29771d011..ce3787c55 100755 --- a/DataStorage/Database/Schema/Builder.php +++ b/DataStorage/Database/Schema/Builder.php @@ -16,6 +16,7 @@ namespace phpOMS\DataStorage\Database\Schema; use phpOMS\DataStorage\Database\BuilderAbstract; use phpOMS\DataStorage\Database\Connection\ConnectionAbstract; +use phpOMS\DataStorage\Database\Schema\Grammar\Grammar; /** * Database query builder. @@ -29,6 +30,14 @@ use phpOMS\DataStorage\Database\Connection\ConnectionAbstract; */ class Builder extends BuilderAbstract { + /** + * Grammar. + * + * @var Grammar + * @since 1.0.0 + */ + protected Grammar $grammar; + /** * Table to create. * @@ -260,7 +269,7 @@ class Builder extends BuilderAbstract * @param bool $isNullable Can be null * @param bool $isPrimary Is a primary field * @param bool $isUnique Is a unique field - * @param bool $autoincrement Autoincrements + * @param bool $autoincrement Auto increments * @param string $foreignTable Foreign table (in case of foreign key) * @param string $foreignKey Foreign key * @param array $meta Meta data @@ -272,7 +281,7 @@ class Builder extends BuilderAbstract public function field( string $name, string $type, $default = null, bool $isNullable = true, bool $isPrimary = false, bool $isUnique = false, bool $autoincrement = false, - string $foreignTable = null, string $foreignKey = null, array $meta = [] + ?string $foreignTable = null, ?string $foreignKey = null, array $meta = [] ) : self { $this->createFields[$name] = [ 'name' => $name, @@ -318,7 +327,7 @@ class Builder extends BuilderAbstract * * @since 1.0.0 */ - public function addConstraint(string $key, string $foreignTable, string $foreignKey, string $constraint = null) : self + public function addConstraint(string $key, string $foreignTable, string $foreignKey, ?string $constraint = null) : self { $this->alterAdd['type'] = 'CONSTRAINT'; $this->alterAdd['key'] = $key; @@ -335,9 +344,10 @@ class Builder extends BuilderAbstract public function execute() : ?\PDOStatement { $sth = null; + $sql = ''; try { - $sth = $this->connection->con->prepare($this->toSql()); + $sth = $this->connection->con->prepare($sql = $this->toSql()); if ($sth === false) { return null; } @@ -347,14 +357,19 @@ class Builder extends BuilderAbstract if ($this->hasPostQuery) { $sqls = $this->grammar->compilePostQueries($this); - foreach ($sqls as $sql) { - $this->connection->con->exec($sql); + foreach ($sqls as $post) { + $this->connection->con->exec($post); } } } catch (\Throwable $t) { // @codeCoverageIgnoreStart - \var_dump($t->getMessage()); - \var_dump($this->toSql()); + \phpOMS\Log\FileLogger::getInstance()->error( + \phpOMS\Log\FileLogger::MSG_FULL, [ + 'message' => $t->getMessage() . ':' . $sql, + 'line' => __LINE__, + 'file' => self::class, + ] + ); $sth = null; // @codeCoverageIgnoreEnd @@ -368,6 +383,15 @@ class Builder extends BuilderAbstract */ public function toSql() : string { - return $this->grammar->compileQuery($this); + $components = $this->grammar->compileComponents($this); + $queryString = ''; + + foreach ($components as $component) { + if ($component !== '') { + $queryString .= $component . ' '; + } + } + + return \substr($queryString, 0, -1) . ';'; } } diff --git a/DataStorage/Database/Schema/Grammar/Grammar.php b/DataStorage/Database/Schema/Grammar/Grammar.php index eb43c1c2f..79ce26d58 100755 --- a/DataStorage/Database/Schema/Grammar/Grammar.php +++ b/DataStorage/Database/Schema/Grammar/Grammar.php @@ -32,7 +32,7 @@ class Grammar extends GrammarAbstract /** * {@inheritdoc} */ - protected function compileComponents(BuilderAbstract $query) : array + public function compileComponents(BuilderAbstract $query) : array { /** @var SchemaBuilder $query */ diff --git a/DataStorage/Database/Schema/Grammar/GrammarInterface.php b/DataStorage/Database/Schema/Grammar/GrammarInterface.php deleted file mode 100755 index a396f2b4b..000000000 --- a/DataStorage/Database/Schema/Grammar/GrammarInterface.php +++ /dev/null @@ -1,27 +0,0 @@ -expressionizeTableColumn([$table]); + } + + /** + * {@inheritdoc} + */ protected function compileCreateFields(SchemaBuilder $query, array $fields) : string { $fieldQuery = ''; $keys = ''; foreach ($fields as $name => $field) { - $fieldQuery .= ' ' . $this->expressionizeTableColumn([$name]) . ' ' . $field['type']; + $fieldQuery .= ' ' . $this->expressionizeTableColumn([$name]) . ' ' . $this->parseFieldType($field['type']); if (isset($field['default']) || ($field['default'] === null && ($field['null'] ?? false))) { $fieldQuery .= ' DEFAULT ' . $this->compileValue($query, $field['default']); @@ -109,16 +129,16 @@ class SQLiteGrammar extends Grammar $fieldQuery .= ' ' . ($field['null'] ? '' : 'NOT ') . 'NULL'; } + if ($field['primary'] ?? false) { + $keys .= ' PRIMARY KEY'; + } + if ($field['autoincrement'] ?? false) { - $fieldQuery .= ' AUTO_INCREMENT'; + $fieldQuery .= ' AUTOINCREMENT'; } $fieldQuery .= ','; - if ($field['primary'] ?? false) { - $keys .= ' PRIMARY KEY (' . $this->expressionizeTableColumn([$name]) . '),'; - } - if ($field['unique'] ?? false) { $keys .= ' UNIQUE KEY (' . $this->expressionizeTableColumn([$name]) . '),'; } diff --git a/DataStorage/Database/SchemaMapper.php b/DataStorage/Database/SchemaMapper.php index 1ad999bf0..282fab554 100755 --- a/DataStorage/Database/SchemaMapper.php +++ b/DataStorage/Database/SchemaMapper.php @@ -26,6 +26,9 @@ use phpOMS\DataStorage\Database\Schema\Table; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @feature Implement schema modification grammar (alter tables) + * https://github.com/Karaka-Management/phpOMS/issues/275 */ class SchemaMapper { diff --git a/DataStorage/LockException.php b/DataStorage/LockException.php index 8a830d34d..abc9fa76f 100755 --- a/DataStorage/LockException.php +++ b/DataStorage/LockException.php @@ -36,7 +36,7 @@ final class LockException extends \RuntimeException * * @since 1.0.0 */ - public function __construct(string $message, int $code = 0, \Exception $previous = null) + public function __construct(string $message, int $code = 0, ?\Exception $previous = null) { parent::__construct('Interaction with "' . $message . '" already locked.', $code, $previous); } diff --git a/DataStorage/Session/HttpSession.php b/DataStorage/Session/HttpSession.php index ab5c88a21..b4e704b49 100755 --- a/DataStorage/Session/HttpSession.php +++ b/DataStorage/Session/HttpSession.php @@ -66,13 +66,13 @@ final class HttpSession implements SessionInterface /** * Constructor. * - * @param int $liftetime Session life time + * @param int $lifetime Session life time * @param string $sid Session id * @param int $inactivityInterval Interval for session activity * * @since 1.0.0 */ - public function __construct(int $liftetime = 3600, string $sid = '', int $inactivityInterval = 0) + public function __construct(int $lifetime = 3600, string $sid = '', int $inactivityInterval = 0) { if (\session_id()) { \session_write_close(); // @codeCoverageIgnore @@ -86,8 +86,17 @@ final class HttpSession implements SessionInterface if (\session_status() !== \PHP_SESSION_ACTIVE && !\headers_sent()) { // @codeCoverageIgnoreStart + // samesite: Strict results in losing sessions in some situations when working with iframe + // This can happen when the iframe content uses relative links + // -> loads iframe page + // -> iframe page references relative resources (css, js, ...) + // -> client browser tries to load resources + // -> client browser loads current app based on relative link but without session cookie + // -> creates new session cookie for page (not authenticated yet) + // -> loses authentication on iframe parent + // samesite: None would solve that but is way too dangerous. \session_set_cookie_params([ - 'lifetime' => $liftetime, + 'lifetime' => $lifetime, 'path' => '/', 'domain' => '', 'secure' => false, @@ -97,10 +106,9 @@ final class HttpSession implements SessionInterface \session_start(); // @codeCoverageIgnoreEnd } else { - $logger = FileLogger::getInstance(); - $logger->error( + FileLogger::getInstance()->warning( FileLogger::MSG_FULL, [ - 'message' => 'Bad application flow.', + 'message' => 'Headers already sent.', 'line' => __LINE__, 'file' => self::class, ] diff --git a/Dispatcher/Dispatcher.php b/Dispatcher/Dispatcher.php index e80ce2c9e..c256b69ff 100755 --- a/Dispatcher/Dispatcher.php +++ b/Dispatcher/Dispatcher.php @@ -53,7 +53,7 @@ final class Dispatcher implements DispatcherInterface * * @since 1.0.0 */ - public function __construct(ApplicationAbstract $app = null) + public function __construct(?ApplicationAbstract $app = null) { $this->app = $app; } @@ -112,7 +112,7 @@ final class Dispatcher implements DispatcherInterface * * @since 1.0.0 */ - private function dispatchString(string $controller, array $data = null) : array + private function dispatchString(string $controller, ?array $data = null) : array { $views = []; $dispatch = \explode(':', $controller); @@ -152,7 +152,7 @@ final class Dispatcher implements DispatcherInterface * * @since 1.0.0 */ - private function dispatchArray(array $controller, array $data = null) : array + private function dispatchArray(array $controller, ?array $data = null) : array { $views = []; foreach ($controller as $controllerSingle) { @@ -172,7 +172,7 @@ final class Dispatcher implements DispatcherInterface * * @since 1.0.0 */ - private function dispatchClosure(callable $controller, array $data = null) : mixed + private function dispatchClosure(callable $controller, ?array $data = null) : mixed { return $data === null ? $controller($this->app) : $controller($this->app, ...$data); } diff --git a/Event/EventManager.php b/Event/EventManager.php index bba4b3d73..c2ef3977a 100755 --- a/Event/EventManager.php +++ b/Event/EventManager.php @@ -21,7 +21,7 @@ use phpOMS\Dispatcher\DispatcherInterface; * EventManager class. * * The event manager allows to define events which can be triggered/executed in an application. - * This implementation allows to create sub-conditions which need to be met (triggered in advance) bevore the actual + * This implementation allows to create sub-conditions which need to be met (triggered in advance) before the actual * callback is getting executed. * * What happens after triggering an event (removing the callback, resetting the sub-conditions etc.) depends on the setup. @@ -64,7 +64,7 @@ final class EventManager implements \Countable * * @since 1.0.0 */ - public function __construct(Dispatcher $dispatcher = null) + public function __construct(?Dispatcher $dispatcher = null) { $this->dispatcher = $dispatcher ?? new class() implements DispatcherInterface { /** @@ -90,7 +90,7 @@ final class EventManager implements \Countable * return [ * '{EVENT_ID}' => [ * 'callback' => [ - * '{DESTINATION_NAMESPACE:method}', // can also be static by using :: between namespace and functio name + * '{DESTINATION_NAMESPACE:method}', // can also be static by using :: between namespace and function name * // more callbacks here * ], * ], @@ -159,7 +159,7 @@ final class EventManager implements \Countable /** * Trigger event based on regex for group and/or id. * - * This tigger function allows the group to be a regex in either this function call or in the definition of the group. + * This trigger function allows the group to be a regex in either this function call or in the definition of the group. * * @param string $group Name of the event (can be regex) * @param string $id Sub-requirement for event (can be regex) @@ -257,7 +257,7 @@ final class EventManager implements \Countable foreach ($this->callbacks[$group]['callbacks'] as $func) { if (\is_array($data)) { $data['@triggerGroup'] ??= $group; - $data['@triggerId'] = $id; + $data['@triggerId'] = $id; } else { $data = [ $data, diff --git a/Image/Kernel.php b/Image/Kernel.php index 5a50da54d..37da68f93 100755 --- a/Image/Kernel.php +++ b/Image/Kernel.php @@ -26,54 +26,116 @@ use phpOMS\Utils\NumericUtils; */ final class Kernel { + /** + * Kernel matrix for ridge + * + * @var array + * @since 1.0.0 + */ public const KERNEL_RIDGE_1 = [ [0, -1, 0], [-1, 4, -1], [0, -1, 0], ]; + /** + * Kernel matrix for ridge + * + * @var array + * @since 1.0.0 + */ public const KERNEL_RIDGE_2 = [ [-1, -1, -1], [-1, 8, -1], [-1, -1, -1], ]; + /** + * Kernel matrix for sharpening + * + * @var array + * @since 1.0.0 + */ public const KERNEL_SHARPEN = [ [0, -1, 0], [-1, 5, -1], [0, -1, 0], ]; + /** + * Kernel matrix for blurring + * + * @var array + * @since 1.0.0 + */ public const KERNEL_BOX_BLUR = [ [1 / 9, 1 / 9, 1 / 9], [1 / 9, 1 / 9, 1 / 9], [1 / 9, 1 / 9, 1 / 9], ]; + /** + * Kernel matrix for gaussian blurring + * + * @var array + * @since 1.0.0 + */ public const KERNEL_GAUSSUAN_BLUR_3 = [ [1 / 16, 2 / 16, 1 / 16], [2 / 16, 4 / 16, 2 / 16], [1 / 16, 2 / 16, 1 / 16], ]; + /** + * Kernel matrix for embossing + * + * @var array + * @since 1.0.0 + */ public const KERNEL_EMBOSS = [ [-2, -1, 0], [-1, 1, 1], [0, 1, 2], ]; + /** + * Kernel matrix for unsharpening + * + * @var array + * @since 1.0.0 + */ public const KERNEL_UNSHARP_MASKING = [ - [-1 / 256, -4 / 256, -6 / 256, -4 / 256, -1 / 256], - [-4 / 256, -16 / 256, -24 / 256, -16 / 256, -4 / 256], - [-6 / 256, -24 / 256, 476 / 256, -24 / 256, -6 / 256], - [-4 / 256, -16 / 256, -24 / 256, -16 / 256, -4 / 256], - [-1 / 256, -4 / 256, -6 / 256, -4 / 256, -1 / 256], + [-1 / 256, -4 / 256, -6 / 256, -4 / 256, -1 / 256], + [-4 / 256, -16 / 256, -24 / 256, -16 / 256, -4 / 256], + [-6 / 256, -24 / 256, 476 / 256, -24 / 256, -6 / 256], + [-4 / 256, -16 / 256, -24 / 256, -16 / 256, -4 / 256], + [-1 / 256, -4 / 256, -6 / 256, -4 / 256, -1 / 256], ]; /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /** + * Apply kernel matrix + * + * @param string $inPath Image file path + * @param string $outPath Image output path + * @param array $kernel Kernel matrix + * + * @return void + * * @see https://en.wikipedia.org/wiki/Kernel_(image_processing) * @see https://towardsdatascience.com/image-processing-with-python-blurring-and-sharpening-for-beginners-3bcebec0583a * @see https://web.eecs.umich.edu/~jjcorso/t/598F14/files/lecture_0924_filtering.pdf + * + * @since 1.0.0 */ public static function convolve(string $inPath, string $outPath, array $kernel) : void { @@ -107,7 +169,7 @@ final class Kernel for ($ky = 0; $ky < $kDim[0]; ++$ky) { for ($kx = 0; $kx < $kDim[1]; ++$kx) { - $pixel = \imagecolorat($im, + $pixel = \imagecolorat($im, \min(\max($x + $kx - $kWidthRadius, 0), $dim[0] - 1), \min(\max($y + $ky - $kHeightRadius, 0), $dim[1] - 1) ); diff --git a/Image/Skew.php b/Image/Skew.php index 22736f9b3..6f4936ebc 100755 --- a/Image/Skew.php +++ b/Image/Skew.php @@ -24,6 +24,16 @@ namespace phpOMS\Image; */ final class Skew { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Automatically rotate image based on projection profile * @@ -62,7 +72,7 @@ final class Skew for ($i = $start[0]; $i < $end[0]; ++$i) { for ($j = $start[1]; $j < $end[1]; ++$j) { $imMatrix[$j - $start[1]][$i - $start[0]] = \imagecolorat($im, $i, $j) < 0.5 ? 1 : 0; - $avg += $imMatrix[$j - $start[1]][$i - $start[0]]; + $avg += $imMatrix[$j - $start[1]][$i - $start[0]]; } } diff --git a/Image/Thresholding.php b/Image/Thresholding.php index 935f55e1a..4759c381e 100755 --- a/Image/Thresholding.php +++ b/Image/Thresholding.php @@ -26,6 +26,16 @@ use phpOMS\Utils\ImageUtils; */ final class Thresholding { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Perform integral thresholding * diff --git a/LICENSE.txt b/LICENSE.txt index 18d430e7b..4ba0161ba 100755 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -27,7 +27,7 @@ Version 2.0 Subject to the terms and conditions of this License, each Contributor grants to You after purchase a perpetual, worldwide, non-exclusive, irrevocable copyright license to prepare Derivative Works of, publicly display, publicly perform the Work and such Derivative Works in Source or Object form. You are not allowed to sublicense, reproduce, or distribute the Work and such Derivative Works in Source or Object form. -3. Redistribution. +3. Redistribution You may not reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form. diff --git a/Localization/BaseStringL11n.php b/Localization/BaseStringL11n.php index 78952c75c..326a633b3 100755 --- a/Localization/BaseStringL11n.php +++ b/Localization/BaseStringL11n.php @@ -40,11 +40,12 @@ class BaseStringL11n implements \JsonSerializable */ public string $name = ''; - // @todo: this feels like $name and $type accomplish the same thing - // maybe we can always use $type and remove $name. - // This would require some smart mapper adjustment where the name is part of the l11n model, - // maybe use the path definition in the mapper which is used by arrays (e.g. type/name) - // More maybe: $name might have been intended as internal value? -> Makes no sense because only string + // @question Check if $name and $type accomplish the same thing + // Maybe we can always use $type and remove $name. + // This would require some smart mapper adjustment where the name is part of the l11n model, + // Maybe use the path definition in the mapper which is used by arrays (e.g. type/name) + // More maybe: $name might have been intended as internal value? -> Makes no sense because only string + // https://github.com/Karaka-Management/phpOMS/issues/357 public ?BaseStringL11nType $type = null; /** diff --git a/Localization/BaseStringL11nType.php b/Localization/BaseStringL11nType.php index 551a4628b..3600e26c1 100644 --- a/Localization/BaseStringL11nType.php +++ b/Localization/BaseStringL11nType.php @@ -95,12 +95,12 @@ class BaseStringL11nType implements \JsonSerializable if ($l11n instanceof BaseStringL11n) { $this->l11n = $l11n; } elseif (isset($this->l11n) && $this->l11n instanceof BaseStringL11n) { - $this->l11n->content = $l11n; - $this->l11n->setLanguage($lang); + $this->l11n->content = $l11n; + $this->l11n->language = $lang; } else { - $this->l11n = new BaseStringL11n(); - $this->l11n->content = $l11n; - $this->l11n->setLanguage($lang); + $this->l11n = new BaseStringL11n(); + $this->l11n->content = $l11n; + $this->l11n->language = $lang; } } diff --git a/Localization/Defaults/NullCity.php b/Localization/Defaults/NullCity.php index 4f74176ee..ce735c0d6 100755 --- a/Localization/Defaults/NullCity.php +++ b/Localization/Defaults/NullCity.php @@ -24,4 +24,15 @@ namespace phpOMS\Localization\Defaults; */ final class NullCity extends City { + /** + * Constructor + * + * @param int $id Model id + * + * @since 1.0.0 + */ + public function __construct(int $id = 0) + { + $this->id = $id; + } } diff --git a/Localization/Defaults/NullCountry.php b/Localization/Defaults/NullCountry.php index 2705484cc..918a7a44e 100755 --- a/Localization/Defaults/NullCountry.php +++ b/Localization/Defaults/NullCountry.php @@ -24,4 +24,15 @@ namespace phpOMS\Localization\Defaults; */ final class NullCountry extends Country { + /** + * Constructor + * + * @param int $id Model id + * + * @since 1.0.0 + */ + public function __construct(int $id = 0) + { + $this->id = $id; + } } diff --git a/Localization/Defaults/NullCurrency.php b/Localization/Defaults/NullCurrency.php index 13102f208..63e54fb33 100755 --- a/Localization/Defaults/NullCurrency.php +++ b/Localization/Defaults/NullCurrency.php @@ -24,4 +24,15 @@ namespace phpOMS\Localization\Defaults; */ final class NullCurrency extends Currency { + /** + * Constructor + * + * @param int $id Model id + * + * @since 1.0.0 + */ + public function __construct(int $id = 0) + { + $this->id = $id; + } } diff --git a/Localization/Defaults/NullIban.php b/Localization/Defaults/NullIban.php index 5a16e6916..29edc5d5d 100755 --- a/Localization/Defaults/NullIban.php +++ b/Localization/Defaults/NullIban.php @@ -24,4 +24,15 @@ namespace phpOMS\Localization\Defaults; */ final class NullIban extends Iban { + /** + * Constructor + * + * @param int $id Model id + * + * @since 1.0.0 + */ + public function __construct(int $id = 0) + { + $this->id = $id; + } } diff --git a/Localization/Defaults/NullLanguage.php b/Localization/Defaults/NullLanguage.php index 497c8a2c5..9fb9c5965 100755 --- a/Localization/Defaults/NullLanguage.php +++ b/Localization/Defaults/NullLanguage.php @@ -24,4 +24,15 @@ namespace phpOMS\Localization\Defaults; */ final class NullLanguage extends Language { + /** + * Constructor + * + * @param int $id Model id + * + * @since 1.0.0 + */ + public function __construct(int $id = 0) + { + $this->id = $id; + } } diff --git a/Localization/ISO3166Trait.php b/Localization/ISO3166Trait.php index 92c42706a..3e050a500 100644 --- a/Localization/ISO3166Trait.php +++ b/Localization/ISO3166Trait.php @@ -44,6 +44,479 @@ trait ISO3166Trait return self::getByName($code3); } + /** + * Get country from language. + * + * @param string $language Language 2 code + * + * @return array + * + * @since 1.0.0 + */ + public static function countryFromLanguage(string $language) : array + { + switch (\strtolower($language)) { + case ISO639x1Enum::_PS: + return [ + self::_AFG, + ]; + case ISO639x1Enum::_UZ: + return [ + self::_AFG, self::_UZB, + ]; + case ISO639x1Enum::_TK: + return [ + self::_AFG, self::_TKM, + ]; + case ISO639x1Enum::_SV: + return [ + self::_ALA, self::_FIN, self::_SWE, + ]; + case ISO639x1Enum::_SQ: + return [ + self::_ALB, self::_MNE, + ]; + case ISO639x1Enum::_AR: + return [ + self::_DZA, self::_BHR, self::_TCD, self::_COM, self::_DJI, self::_EGY, self::_ERI, self::_ETH, self::_IRQ, self::_ISR, self::_JOR, self::_KWT, self::_LBN, self::_LBY, self::_MRT, self::_MAR, self::_OMN, self::_PSE, self::_QAT, self::_SAU, self::_SOM, self::_SDN, self::_SYR, self::_TUN, self::_ARE, self::_ESH, self::_YEM, + ]; + case ISO639x1Enum::_EN: + return [ + self::_USA, self::_ASM, self::_AIA, self::_ATA, self::_ATG, self::_AUS, self::_BHS, self::_BRB, self::_BLZ, self::_BMU, self::_BES, self::_BWA, self::_IOT, self::_CMR, self::_CAN, self::_CYM, self::_CXR, self::_CCK, self::_COK, self::_CUW, self::_DMA, self::_ERI, self::_FLK, self::_FJI, self::_GMB, self::_GHA, self::_GIB, self::_GRD, self::_GUM, self::_GGY, self::_GUY, self::_HMD, self::_HKG, self::_IND, self::_IRL, self::_IMN, self::_JAM, self::_JEY, self::_KEN, self::_KIR, self::_LSO, self::_LBR, self::_MWI, self::_MLT, self::_MHL, self::_MUS, self::_FSM, self::_MSR, self::_NAM, self::_NRU, self::_NZL, self::_NGA, self::_NIU, self::_NFK, self::_MNP, self::_PAK, self::_PLW, self::_PNG, self::_PHL, self::_PCN, self::_PRI, self::_RWA, self::_SHN, self::_KNA, self::_LCA, self::_MAF, self::_VCT, self::_WSM, self::_SYC, self::_SLE, self::_SGP, self::_SXM, self::_SLB, self::_SOM, self::_ZAF, self::_SGS, self::_SSD, self::_LKA, self::_SDN, self::_SWZ, self::_TZA, self::_TKL, self::_TON, self::_TTO, self::_TCA, self::_TUV, self::_UGA, self::_GBR, self::_UMI, self::_VUT, self::_VGB, self::_VIR, self::_ZMB, self::_ZWE, + ]; + case ISO639x1Enum::_SM: + return [ + self::_ASM, self::_WSM, + ]; + case ISO639x1Enum::_CA: + return [ + self::_AND, + ]; + case ISO639x1Enum::_PT: + return [ + self::_AGO, self::_BRA, self::_CPV, self::_GNQ, self::_GNB, self::_MAC, self::_MOZ, self::_PRT, self::_STP, self::_TLS, + ]; + case ISO639x1Enum::_RU: + return [ + self::_ATA, self::_ARM, self::_AZE, self::_BLR, self::_KAZ, self::_KGZ, self::_RUS, self::_TJK, self::_TKM, self::_UZB, + ]; + case ISO639x1Enum::_ES: + return [ + self::_ARG, self::_BOL, self::_CHL, self::_COL, self::_CRI, self::_CUB, self::_DOM, self::_ECU, self::_SLV, self::_GNQ, self::_GUM, self::_GTM, self::_HND, self::_MEX, self::_NIC, self::_PAN, self::_PRY, self::_PER, self::_PRI, self::_ESP, self::_USA, self::_URY, self::_VEN, self::_ESH, + ]; + case ISO639x1Enum::_GN: + return [ + self::_ARG, self::_PRY, + ]; + case ISO639x1Enum::_HY: + return [ + self::_ARM, self::_AZE, + ]; + case ISO639x1Enum::_NL: + return [ + self::_ABW, self::_BEL, self::_BES, self::_CUW, self::_NLD, self::_MAF, self::_SXM, self::_SUR, + ]; + case ISO639x1Enum::_DE: + return [ + self::_DEU, self::_AUT, self::_BEL, self::_LIE, self::_LUX, self::_CHE, + ]; + case ISO639x1Enum::_AZ: + return [ + self::_AZE, + ]; + case ISO639x1Enum::_BN: + return [ + self::_BGD, + ]; + case ISO639x1Enum::_BE: + return [ + self::_BLR, + ]; + case ISO639x1Enum::_FR: + return [ + self::_BEL, self::_BEN, self::_BFA, self::_BDI, self::_CMR, self::_CAN, self::_CAF, self::_TCD, self::_COM, self::_COG, self::_COD, self::_CIV, self::_DJI, self::_GNQ, self::_FRA, self::_GUF, self::_PYF, self::_ATF, self::_GAB, self::_GLP, self::_GGY, self::_GIN, self::_HTI, self::_JEY, self::_LBN, self::_LUX, self::_MDG, self::_MLI, self::_MTQ, self::_MRT, self::_MUS, self::_MYT, self::_MCO, self::_NCL, self::_NER, self::_REU, self::_RWA, self::_BLM, self::_MAF, self::_SPM, self::_SEN, self::_SYC, self::_CHE, self::_TGO, self::_VUT, self::_WLF, + ]; + case ISO639x1Enum::_DZ: + return [ + self::_BTN, + ]; + case ISO639x1Enum::_QU: + return [ + self::_BOL, self::_PER, + ]; + case ISO639x1Enum::_AY: + return [ + self::_BOL, self::_PER, + ]; + case ISO639x1Enum::_BS: + return [ + self::_BIH, self::_MNE, self::_XXK, + ]; + case ISO639x1Enum::_HR: + return [ + self::_BIH, self::_HRV, self::_MNE, + ]; + case ISO639x1Enum::_SR: + return [ + self::_BIH, self::_MNE, self::_SRB, + ]; + case ISO639x1Enum::_TN: + return [ + self::_BWA, self::_ZAF, + ]; + case ISO639x1Enum::_NO: + return [ + self::_BVT, self::_NOR, self::_SJM, + ]; + case ISO639x1Enum::_MS: + return [ + self::_BRN, self::_MYS, self::_SGP, + ]; + case ISO639x1Enum::_BG: + return [ + self::_BGR, + ]; + case ISO639x1Enum::_RN: + return [ + self::_BDI, + ]; + case ISO639x1Enum::_KM: + return [ + self::_KHM, + ]; + case ISO639x1Enum::_SG: + return [ + self::_CAF, + ]; + case ISO639x1Enum::_ZH: + return [ + self::_CHN, self::_HKG, self::_MAC, self::_PLW, self::_SGP, self::_TWN, + ]; + case ISO639x1Enum::_LN: + return [ + self::_COG, self::_COD, + ]; + case ISO639x1Enum::_KG: + return [ + self::_COG, self::_COD, + ]; + case ISO639x1Enum::_SW: + return [ + self::_COG, self::_COD, self::_KEN, self::_TZA, self::_UGA, + ]; + case ISO639x1Enum::_PA: + return [ + self::_CUW, + ]; + case ISO639x1Enum::_EL: + return [ + self::_CYP, self::_GRC, + ]; + case ISO639x1Enum::_TR: + return [ + self::_CYP, self::_TUR, self::_XXK, + ]; + case ISO639x1Enum::_CS: + return [ + self::_CZE, + ]; + case ISO639x1Enum::_SK: + return [ + self::_CZE, self::_SVK, + ]; + case ISO639x1Enum::_DA: + return [ + self::_DNK, + ]; + case ISO639x1Enum::_SO: + return [ + self::_DJI, self::_ETH, self::_SOM, + ]; + case ISO639x1Enum::_TI: + return [ + self::_ERI, self::_ETH, + ]; + case ISO639x1Enum::_ET: + return [ + self::_EST, + ]; + case ISO639x1Enum::_AM: + return [ + self::_ETH, + ]; + case ISO639x1Enum::_OM: + return [ + self::_ETH, + ]; + case ISO639x1Enum::_FO: + return [ + self::_FRO, + ]; + case ISO639x1Enum::_FJ: + return [ + self::_FJI, + ]; + case ISO639x1Enum::_HI: + return [ + self::_FJI, self::_IND, + ]; + case ISO639x1Enum::_UR: + return [ + self::_FJI, self::_PAK, + ]; + case ISO639x1Enum::_FI: + return [ + self::_FIN, + ]; + case ISO639x1Enum::_TY: + return [ + self::_PYF, + ]; + case ISO639x1Enum::_KA: + return [ + self::_GEO, + ]; + case ISO639x1Enum::_KL: + return [ + self::_GRL, + ]; + case ISO639x1Enum::_CH: + return [ + self::_GUM, self::_MNP, + ]; + case ISO639x1Enum::_HT: + return [ + self::_HTI, + ]; + case ISO639x1Enum::_IT: + return [ + self::_VAT, self::_ITA, self::_SMR, self::_SOM, self::_CHE, + ]; + case ISO639x1Enum::_LA: + return [ + self::_VAT, + ]; + case ISO639x1Enum::_HU: + return [ + self::_HUN, + ]; + case ISO639x1Enum::_IS: + return [ + self::_ISL, + ]; + case ISO639x1Enum::_ID: + return [ + self::_IDN, + ]; + case ISO639x1Enum::_FA: + return [ + self::_IRN, + ]; + case ISO639x1Enum::_KU: + return [ + self::_IRQ, + ]; + case ISO639x1Enum::_GA: + return [ + self::_IRL, self::_GBR, + ]; + case ISO639x1Enum::_GV: + return [ + self::_IMN, + ]; + case ISO639x1Enum::_HE: + return [ + self::_ISR, + ]; + case ISO639x1Enum::_JA: + return [ + self::_JPN, self::_PLW, + ]; + case ISO639x1Enum::_KK: + return [ + self::_KAZ, + ]; + case ISO639x1Enum::_KO: + return [ + self::_PRK, self::_KOR, + ]; + case ISO639x1Enum::_KY: + return [ + self::_KGZ, + ]; + case ISO639x1Enum::_LO: + return [ + self::_LAO, + ]; + case ISO639x1Enum::_LV: + return [ + self::_LVA, + ]; + case ISO639x1Enum::_ST: + return [ + self::_LSO, self::_ZAF, + ]; + case ISO639x1Enum::_LT: + return [ + self::_LTU, + ]; + case ISO639x1Enum::_LB: + return [ + self::_LUX, + ]; + case ISO639x1Enum::_MK: + return [ + self::_MKD, + ]; + case ISO639x1Enum::_MG: + return [ + self::_MDG, + ]; + case ISO639x1Enum::_NY: + return [ + self::_MWI, + ]; + case ISO639x1Enum::_DV: + return [ + self::_MDV, + ]; + case ISO639x1Enum::_MT: + return [ + self::_MLT, + ]; + case ISO639x1Enum::_MH: + return [ + self::_MHL, + ]; + case ISO639x1Enum::_RO: + return [ + self::_MDA, self::_ROU, + ]; + case ISO639x1Enum::_MN: + return [ + self::_MNG, + ]; + case ISO639x1Enum::_MY: + return [ + self::_MMR, + ]; + case ISO639x1Enum::_AF: + return [ + self::_NAM, self::_ZAF, + ]; + case ISO639x1Enum::_NA: + return [ + self::_NRU, + ]; + case ISO639x1Enum::_NE: + return [ + self::_NPL, + ]; + case ISO639x1Enum::_MI: + return [ + self::_NZL, + ]; + case ISO639x1Enum::_NB: + return [ + self::_NOR, + ]; + case ISO639x1Enum::_NN: + return [ + self::_NOR, + ]; + case ISO639x1Enum::_HO: + return [ + self::_PNG, + ]; + case ISO639x1Enum::_PL: + return [ + self::_POL, + ]; + case ISO639x1Enum::_RW: + return [ + self::_RWA, + ]; + case ISO639x1Enum::_WO: + return [ + self::_SEN, + ]; + case ISO639x1Enum::_TA: + return [ + self::_SGP, self::_LKA, + ]; + case ISO639x1Enum::_SL: + return [ + self::_SVN, + ]; + case ISO639x1Enum::_ZU: + return [ + self::_ZAF, + ]; + case ISO639x1Enum::_XH: + return [ + self::_ZAF, + ]; + case ISO639x1Enum::_TS: + return [ + self::_ZAF, + ]; + case ISO639x1Enum::_SS: + return [ + self::_ZAF, self::_SWZ, + ]; + case ISO639x1Enum::_VE: + return [ + self::_ZAF, + ]; + case ISO639x1Enum::_SI: + return [ + self::_LKA, + ]; + case ISO639x1Enum::_TG: + return [ + self::_TJK, + ]; + case ISO639x1Enum::_TH: + return [ + self::_THA, + ]; + case ISO639x1Enum::_TO: + return [ + self::_TON, + ]; + case ISO639x1Enum::_UK: + return [ + self::_UKR, + ]; + case ISO639x1Enum::_CY: + return [ + self::_GBR, + ]; + case ISO639x1Enum::_GD: + return [ + self::_GBR, + ]; + case ISO639x1Enum::_BI: + return [ + self::_VUT, + ]; + case ISO639x1Enum::_VI: + return [ + self::_VNM, + ]; + case ISO639x1Enum::_SN: + return [ + self::_ZWE, + ]; + case ISO639x1Enum::_ND: + return [ + self::_ZWE, + ]; + default: + return []; + } + } + /** * Get countries in a region * @@ -135,6 +608,26 @@ trait ISO3166Trait self::getRegion('west-africa'), self::getRegion('central-africa') ); + case 'nato': + return [ + self::_ALB, self::_BEL, self::_BGR, self::_CAN, self::_HRV, + self::_CZE, self::_DNK, self::_EST, self::_FRA, self::_DEU, + self::_GRC, self::_HUN, self::_ISL, self::_ITA, self::_LVA, + self::_LTU, self::_LUX, self::_MNE, self::_NLD, self::_MKD, + self::_NOR, self::_POL, self::_PRT, self::_ROU, self::_SVK, + self::_SVN, self::_ESP, self::_TUR, self::_GBR, self::_USA, + self::_SWE, + ]; + case 'oecd': + return [ + self::_AUS, self::_NZL, self::_AUT, self::_NOR, self::_BEL, + self::_POL, self::_CAN, self::_PRT, self::_CHL, self::_SVK, + self::_DNK, self::_ESP, self::_EST, self::_SWE, self::_FIN, + self::_CHE, self::_FRA, self::_TUR, self::_DEU, self::_GBR, + self::_GRC, self::_USA, self::_HUN, self::_ISL, self::_IRL, + self::_ISR, self::_ITA, self::_JPN, self::_KOR, self::_LUX, + self::_MEX, self::_NLD, self::_CZE, self::_SVN, + ]; case 'eu': return [ self::_AUT, self::_BEL, self::_BGR, self::_HRV, self::_CYP, @@ -151,6 +644,16 @@ trait ISO3166Trait self::_ITA, self::_LVA, self::_LTU, self::_LUX, self::_MLT, self::_NLD, self::_PRT, self::_SVK, self::_SVN, self::_ESP, ]; + case 'schengen': + return [ + self::_AUT, self::_BEL, self::_HRV, + self::_CZE, self::_DNK, self::_EST, self::_FIN, self::_FRA, + self::_DEU, self::_GRC, self::_HUN, self::_ITA, + self::_LVA, self::_LTU, self::_LUX, self::_MLT, self::_NLD, + self::_POL, self::_PRT, self::_SVK, self::_SVN, + self::_ESP, self::_SWE, + self::_ISL, self::_NOR, self::_CHE, self::_LIE, + ]; case 'north-europe': return [ self::_ALA, self::_DNK, self::_EST, self::_FRO, self::_FIN, @@ -285,6 +788,19 @@ trait ISO3166Trait return [ self::_ATA, self::_ATF, ]; + case 'dach': + return [ + self::_DEU, self::_AUT, self::_CHE, + ]; + case 'g8': + return [ + self::_USA, self::_GBR, self::_FRA, self::_DEU, self::_ITA, + self::_CAN, self::_RUS, self::_JPN, + ]; + case 'p5': + return [ + self::_USA, self::_GBR, self::_FRA, self::_RUS, self::_CHN, + ]; default: return []; } diff --git a/Localization/ISO4217CharEnum.php b/Localization/ISO4217CharEnum.php index 108571a1d..f35a9a2e0 100755 --- a/Localization/ISO4217CharEnum.php +++ b/Localization/ISO4217CharEnum.php @@ -75,6 +75,8 @@ class ISO4217CharEnum extends Enum public const _CLP = 'CLP'; public const _CNY = 'CNY'; + public const _CNH = 'CNH'; + public const _RMB = 'RMB'; public const _COP = 'COP'; @@ -403,4 +405,6 @@ class ISO4217CharEnum extends Enum public const _XUA = 'XUA'; public const _ZMW = 'ZMW'; + + use ISO4217Trait; } diff --git a/Localization/ISO4217DecimalEnum.php b/Localization/ISO4217DecimalEnum.php index a5dc42125..23f758dfd 100755 --- a/Localization/ISO4217DecimalEnum.php +++ b/Localization/ISO4217DecimalEnum.php @@ -93,6 +93,8 @@ class ISO4217DecimalEnum extends Enum public const _CLP = 0; public const _CNY = 2; + public const _CNH = 2; + public const _RMB = 2; public const _COP = 2; diff --git a/Localization/ISO4217Enum.php b/Localization/ISO4217Enum.php index 1ae94b490..458458b1a 100755 --- a/Localization/ISO4217Enum.php +++ b/Localization/ISO4217Enum.php @@ -75,6 +75,8 @@ class ISO4217Enum extends Enum public const _CLP = 'Pesos, Chile'; public const _CNY = 'Yuan Renminbi, China'; + public const _CNH = 'Yuan Renminbi, China'; + public const _RMB = 'Yuan Renminbi, China'; public const _COP = 'Pesos, Colombia'; @@ -403,4 +405,6 @@ class ISO4217Enum extends Enum public const _XUA = 'ADB Unit of Account'; public const _ZMW = 'kwacha, Zambian'; + + use ISO4217Trait; } diff --git a/Localization/ISO4217NumEnum.php b/Localization/ISO4217NumEnum.php index 29bf818ce..f75c56176 100755 --- a/Localization/ISO4217NumEnum.php +++ b/Localization/ISO4217NumEnum.php @@ -93,6 +93,8 @@ class ISO4217NumEnum extends Enum public const _CLP = '152'; public const _CNY = '156'; + public const _CNH = '156'; + public const _RMB = '156'; public const _COP = '170'; @@ -393,4 +395,6 @@ class ISO4217NumEnum extends Enum public const _ZMK = '894'; public const _ZWL = '932'; + + use ISO4217Trait; } diff --git a/Localization/ISO4217SubUnitEnum.php b/Localization/ISO4217SubUnitEnum.php index 06d7d4911..0bcbe52d7 100755 --- a/Localization/ISO4217SubUnitEnum.php +++ b/Localization/ISO4217SubUnitEnum.php @@ -83,6 +83,8 @@ class ISO4217SubUnitEnum extends Enum public const _CLP = 100; public const _CNY = 100; + public const _CNH = 100; + public const _RMB = 100; public const _COP = 100; diff --git a/Localization/ISO4217SymbolEnum.php b/Localization/ISO4217SymbolEnum.php index 7f223b7d1..aeff629e7 100755 --- a/Localization/ISO4217SymbolEnum.php +++ b/Localization/ISO4217SymbolEnum.php @@ -75,6 +75,8 @@ class ISO4217SymbolEnum extends Enum public const _CLP = '$'; public const _CNY = '¥'; + public const _CNH = '¥'; + public const _RMB = '¥'; public const _COP = '$'; @@ -401,4 +403,6 @@ class ISO4217SymbolEnum extends Enum public const _ZMW = 'ZK'; public const _KES = 'KSh'; + + use ISO4217Trait; } diff --git a/Localization/ISO4217Trait.php b/Localization/ISO4217Trait.php new file mode 100644 index 000000000..65cf44fc2 --- /dev/null +++ b/Localization/ISO4217Trait.php @@ -0,0 +1,483 @@ + currency trait. + * + * @package phpOMS\Localization + * @license OMS License 2.0 + * @link https://jingga.app + * @since 1.0.0 + */ +trait ISO4217Trait +{ + /** + * Get currency from country. + * + * @param string $country Country 2 code + * + * @return array + * + * @since 1.0.0 + */ + public static function currencyFromCountry(string $country) : string + { + switch (\strtoupper($country)) { + case ISO3166TwoEnum::_AFG: + return self::_USD; + case ISO3166TwoEnum::_ALA: + return self::_EUR; + case ISO3166TwoEnum::_ALB: + return self::_EUR; + case ISO3166TwoEnum::_DZA: + return self::_DZD; + case ISO3166TwoEnum::_ASM: + return self::_USD; + case ISO3166TwoEnum::_AND: + return self::_EUR; + case ISO3166TwoEnum::_AGO: + return self::_AOA; + case ISO3166TwoEnum::_AIA: + return self::_XCD; + case ISO3166TwoEnum::_ATG: + return self::_XCD; + case ISO3166TwoEnum::_ARG: + return self::_ARS; + case ISO3166TwoEnum::_ARM: + return self::_AMD; + case ISO3166TwoEnum::_ABW: + return self::_AWG; + case ISO3166TwoEnum::_AUS: + return self::_AUD; + case ISO3166TwoEnum::_AUT: + return self::_EUR; + case ISO3166TwoEnum::_AZE: + return self::_AZM; + case ISO3166TwoEnum::_PRT: + return self::_EUR; + case ISO3166TwoEnum::_BHS: + return self::_BSD; + case ISO3166TwoEnum::_BHR: + return self::_BHD; + case ISO3166TwoEnum::_BGD: + return self::_BDT; + case ISO3166TwoEnum::_BRB: + return self::_BBD; + case ISO3166TwoEnum::_BLR: + return self::_BYR; + case ISO3166TwoEnum::_BEL: + return self::_EUR; + case ISO3166TwoEnum::_BLZ: + return self::_BZD; + case ISO3166TwoEnum::_BEN: + return self::_XOF; + case ISO3166TwoEnum::_BMU: + return self::_BMD; + case ISO3166TwoEnum::_BTN: + return self::_BTN; + case ISO3166TwoEnum::_BOL: + return self::_BOB; + case ISO3166TwoEnum::_BES: + return self::_ANG; + case ISO3166TwoEnum::_BIH: + return self::_BAM; + case ISO3166TwoEnum::_BWA: + return self::_BWP; + case ISO3166TwoEnum::_BRA: + return self::_BRL; + case ISO3166TwoEnum::_VGB: + return self::_USD; + case ISO3166TwoEnum::_BRN: + return self::_BND; + case ISO3166TwoEnum::_BGR: + return self::_EUR; + case ISO3166TwoEnum::_BFA: + return self::_XOF; + case ISO3166TwoEnum::_BDI: + return self::_BIF; + case ISO3166TwoEnum::_KHM: + return self::_KHR; + case ISO3166TwoEnum::_CMR: + return self::_XAF; + case ISO3166TwoEnum::_CAN: + return self::_CAD; + case ISO3166TwoEnum::_ESP: + return self::_EUR; + case ISO3166TwoEnum::_CPV: + return self::_CVE; + case ISO3166TwoEnum::_CYM: + return self::_KYD; + case ISO3166TwoEnum::_CAF: + return self::_XAF; + case ISO3166TwoEnum::_TCD: + return self::_XAF; + case ISO3166TwoEnum::_CHL: + return self::_CLP; + case ISO3166TwoEnum::_CHN: + return self::_CNY; + case ISO3166TwoEnum::_COL: + return self::_COP; + case ISO3166TwoEnum::_COM: + return self::_USD; + case ISO3166TwoEnum::_COG: + return self::_XAF; + case ISO3166TwoEnum::_COD: + return self::_CDF; + case ISO3166TwoEnum::_COK: + return self::_NZD; + case ISO3166TwoEnum::_CRI: + return self::_CRC; + case ISO3166TwoEnum::_HRV: + return self::_EUR; + case ISO3166TwoEnum::_CUW: + return self::_USD; + case ISO3166TwoEnum::_CYP: + return self::_EUR; + case ISO3166TwoEnum::_CZE: + return self::_CZK; + case ISO3166TwoEnum::_DNK: + return self::_DKK; + case ISO3166TwoEnum::_DJI: + return self::_DJF; + case ISO3166TwoEnum::_DMA: + return self::_XCD; + case ISO3166TwoEnum::_DOM: + return self::_DOP; + case ISO3166TwoEnum::_TLS: + return self::_USD; + case ISO3166TwoEnum::_ECU: + return self::_USD; + case ISO3166TwoEnum::_EGY: + return self::_EGP; + case ISO3166TwoEnum::_SLV: + return self::_USD; + case ISO3166TwoEnum::_GBR: + return self::_GBP; + case ISO3166TwoEnum::_GNQ: + return self::_XAF; + case ISO3166TwoEnum::_ERI: + return self::_ERN; + case ISO3166TwoEnum::_EST: + return self::_EUR; + case ISO3166TwoEnum::_ETH: + return self::_ETB; + case ISO3166TwoEnum::_FRO: + return self::_DKK; + case ISO3166TwoEnum::_FJI: + return self::_FJD; + case ISO3166TwoEnum::_FIN: + return self::_EUR; + case ISO3166TwoEnum::_FRA: + return self::_EUR; + case ISO3166TwoEnum::_GUF: + return self::_EUR; + case ISO3166TwoEnum::_PYF: + return self::_XPF; + case ISO3166TwoEnum::_GAB: + return self::_XAF; + case ISO3166TwoEnum::_GMB: + return self::_GMD; + case ISO3166TwoEnum::_GEO: + return self::_GEL; + case ISO3166TwoEnum::_DEU: + return self::_EUR; + case ISO3166TwoEnum::_GHA: + return self::_GHS; + case ISO3166TwoEnum::_GIB: + return self::_GIP; + case ISO3166TwoEnum::_GRC: + return self::_EUR; + case ISO3166TwoEnum::_GRL: + return self::_DKK; + case ISO3166TwoEnum::_GRD: + return self::_XCD; + case ISO3166TwoEnum::_GLP: + return self::_EUR; + case ISO3166TwoEnum::_GUM: + return self::_USD; + case ISO3166TwoEnum::_GTM: + return self::_GTQ; + case ISO3166TwoEnum::_GGY: + return self::_GBP; + case ISO3166TwoEnum::_GIN: + return self::_GNF; + case ISO3166TwoEnum::_GNB: + return self::_XOF; + case ISO3166TwoEnum::_GUY: + return self::_GYD; + case ISO3166TwoEnum::_HTI: + return self::_HTG; + case ISO3166TwoEnum::_NLD: + return self::_EUR; + case ISO3166TwoEnum::_HND: + return self::_HNL; + case ISO3166TwoEnum::_HKG: + return self::_HKD; + case ISO3166TwoEnum::_HUN: + return self::_HUF; + case ISO3166TwoEnum::_ISL: + return self::_ISK; + case ISO3166TwoEnum::_IND: + return self::_INR; + case ISO3166TwoEnum::_IDN: + return self::_IDR; + case ISO3166TwoEnum::_IRQ: + return self::_NID; + case ISO3166TwoEnum::_IRL: + return self::_EUR; + case ISO3166TwoEnum::_ISR: + return self::_ILS; + case ISO3166TwoEnum::_ITA: + return self::_EUR; + case ISO3166TwoEnum::_CIV: + return self::_XOF; + case ISO3166TwoEnum::_JAM: + return self::_JMD; + case ISO3166TwoEnum::_JPN: + return self::_JPY; + case ISO3166TwoEnum::_JEY: + return self::_GBP; + case ISO3166TwoEnum::_JOR: + return self::_JOD; + case ISO3166TwoEnum::_KAZ: + return self::_KZT; + case ISO3166TwoEnum::_KEN: + return self::_KES; + case ISO3166TwoEnum::_KIR: + return self::_AUD; + case ISO3166TwoEnum::_KOR: + return self::_KRW; + case ISO3166TwoEnum::_FSM: + return self::_USD; + case ISO3166TwoEnum::_KWT: + return self::_KWD; + case ISO3166TwoEnum::_KGZ: + return self::_KGS; + case ISO3166TwoEnum::_LAO: + return self::_LAK; + case ISO3166TwoEnum::_LVA: + return self::_EUR; + case ISO3166TwoEnum::_LBN: + return self::_LBP; + case ISO3166TwoEnum::_LSO: + return self::_LSL; + case ISO3166TwoEnum::_LBR: + return self::_LRD; + case ISO3166TwoEnum::_LBY: + return self::_LYD; + case ISO3166TwoEnum::_LIE: + return self::_CHF; + case ISO3166TwoEnum::_LTU: + return self::_EUR; + case ISO3166TwoEnum::_LUX: + return self::_EUR; + case ISO3166TwoEnum::_MAC: + return self::_MOP; + case ISO3166TwoEnum::_MKD: + return self::_EUR; + case ISO3166TwoEnum::_MDG: + return self::_MGA; + case ISO3166TwoEnum::_MWI: + return self::_MWK; + case ISO3166TwoEnum::_MYS: + return self::_MYR; + case ISO3166TwoEnum::_MDV: + return self::_MVR; + case ISO3166TwoEnum::_MLI: + return self::_XOF; + case ISO3166TwoEnum::_MLT: + return self::_EUR; + case ISO3166TwoEnum::_MHL: + return self::_USD; + case ISO3166TwoEnum::_MTQ: + return self::_EUR; + case ISO3166TwoEnum::_MRT: + return self::_MRO; + case ISO3166TwoEnum::_MUS: + return self::_MUR; + case ISO3166TwoEnum::_MYT: + return self::_EUR; + case ISO3166TwoEnum::_MEX: + return self::_MXN; + case ISO3166TwoEnum::_MDA: + return self::_MDL; + case ISO3166TwoEnum::_MCO: + return self::_EUR; + case ISO3166TwoEnum::_MNG: + return self::_MNT; + case ISO3166TwoEnum::_MNE: + return self::_EUR; + case ISO3166TwoEnum::_MSR: + return self::_XCD; + case ISO3166TwoEnum::_MAR: + return self::_MAD; + case ISO3166TwoEnum::_MOZ: + return self::_MZM; + case ISO3166TwoEnum::_NAM: + return self::_NAD; + case ISO3166TwoEnum::_NPL: + return self::_NPR; + case ISO3166TwoEnum::_NCL: + return self::_XPF; + case ISO3166TwoEnum::_NZL: + return self::_NZD; + case ISO3166TwoEnum::_NIC: + return self::_NIO; + case ISO3166TwoEnum::_NER: + return self::_XOF; + case ISO3166TwoEnum::_NGA: + return self::_NGN; + case ISO3166TwoEnum::_NFK: + return self::_AUD; + case ISO3166TwoEnum::_MNP: + return self::_USD; + case ISO3166TwoEnum::_NOR: + return self::_NOK; + case ISO3166TwoEnum::_OMN: + return self::_OMR; + case ISO3166TwoEnum::_PAK: + return self::_PKR; + case ISO3166TwoEnum::_PLW: + return self::_USD; + case ISO3166TwoEnum::_PAN: + return self::_PAB; + case ISO3166TwoEnum::_PNG: + return self::_PGK; + case ISO3166TwoEnum::_PRY: + return self::_PYG; + case ISO3166TwoEnum::_PER: + return self::_PEN; + case ISO3166TwoEnum::_PHL: + return self::_PHP; + case ISO3166TwoEnum::_POL: + return self::_PLN; + case ISO3166TwoEnum::_PRI: + return self::_USD; + case ISO3166TwoEnum::_QAT: + return self::_QAR; + case ISO3166TwoEnum::_REU: + return self::_EUR; + case ISO3166TwoEnum::_ROU: + return self::_ROL; + case ISO3166TwoEnum::_RUS: + return self::_RUB; + case ISO3166TwoEnum::_RWA: + return self::_RWF; + case ISO3166TwoEnum::_WSM: + return self::_WST; + case ISO3166TwoEnum::_SMR: + return self::_EUR; + case ISO3166TwoEnum::_STP: + return self::_STD; + case ISO3166TwoEnum::_SAU: + return self::_SAR; + case ISO3166TwoEnum::_SEN: + return self::_XOF; + case ISO3166TwoEnum::_SRB: + return self::_EUR; + case ISO3166TwoEnum::_SYC: + return self::_SCR; + case ISO3166TwoEnum::_SLE: + return self::_SLL; + case ISO3166TwoEnum::_SGP: + return self::_SGD; + case ISO3166TwoEnum::_SVK: + return self::_EUR; + case ISO3166TwoEnum::_SVN: + return self::_EUR; + case ISO3166TwoEnum::_SLB: + return self::_SBD; + case ISO3166TwoEnum::_ZAF: + return self::_ZAR; + case ISO3166TwoEnum::_LKA: + return self::_LKR; + case ISO3166TwoEnum::_BLM: + return self::_EUR; + case ISO3166TwoEnum::_KNA: + return self::_XCD; + case ISO3166TwoEnum::_VIR: + return self::_USD; + case ISO3166TwoEnum::_LCA: + return self::_XCD; + case ISO3166TwoEnum::_SXM: + return self::_USD; + case ISO3166TwoEnum::_VCT: + return self::_XCD; + case ISO3166TwoEnum::_SUR: + return self::_SRG; + case ISO3166TwoEnum::_SWZ: + return self::_SZL; + case ISO3166TwoEnum::_SWE: + return self::_SEK; + case ISO3166TwoEnum::_CHE: + return self::_CHF; + case ISO3166TwoEnum::_TWN: + return self::_TWD; + case ISO3166TwoEnum::_TJK: + return self::_TJS; + case ISO3166TwoEnum::_TZA: + return self::_TZS; + case ISO3166TwoEnum::_THA: + return self::_THB; + case ISO3166TwoEnum::_TGO: + return self::_XOF; + case ISO3166TwoEnum::_TON: + return self::_TOP; + case ISO3166TwoEnum::_TTO: + return self::_TTD; + case ISO3166TwoEnum::_TUN: + return self::_TND; + case ISO3166TwoEnum::_TUR: + return self::_TRY; + case ISO3166TwoEnum::_TKM: + return self::_TMT; + case ISO3166TwoEnum::_TCA: + return self::_USD; + case ISO3166TwoEnum::_TUV: + return self::_AUD; + case ISO3166TwoEnum::_UGA: + return self::_UGX; + case ISO3166TwoEnum::_UKR: + return self::_UAH; + case ISO3166TwoEnum::_ARE: + return self::_AED; + case ISO3166TwoEnum::_USA: + return self::_USD; + case ISO3166TwoEnum::_URY: + return self::_UYU; + case ISO3166TwoEnum::_UZB: + return self::_UZS; + case ISO3166TwoEnum::_VUT: + return self::_VUV; + case ISO3166TwoEnum::_VAT: + return self::_EUR; + case ISO3166TwoEnum::_VEN: + return self::_VEB; + case ISO3166TwoEnum::_VNM: + return self::_VND; + case ISO3166TwoEnum::_WLF: + return self::_XPF; + case ISO3166TwoEnum::_YEM: + return self::_YER; + case ISO3166TwoEnum::_ZMB: + return self::_ZMK; + case ISO3166TwoEnum::_ZWE: + return self::_ZWD; + default: + return ''; + } + } +} diff --git a/Localization/L11nManager.php b/Localization/L11nManager.php index 05f2dbb04..fd57eced6 100755 --- a/Localization/L11nManager.php +++ b/Localization/L11nManager.php @@ -135,7 +135,7 @@ final class L11nManager * * @since 1.0.0 */ - public function getModuleLanguage(string $language, string $module = null) : array + public function getModuleLanguage(string $language, ?string $module = null) : array { if ($module === null && isset($this->language[$language])) { return $this->language[$language]; @@ -213,7 +213,7 @@ final class L11nManager * * @since 1.0.0 */ - public function getNumeric(Localization $l11n, int | float | FloatInt $numeric, string $format = null) : string + public function getNumeric(Localization $l11n, int | float | FloatInt $numeric, ?string $format = null) : string { if (!($numeric instanceof FloatInt)) { return \number_format( @@ -235,18 +235,19 @@ final class L11nManager /** * Print a percentage value * - * @param Localization $l11n Localization - * @param float $percentage Percentage value to print - * @param null|string $format Format type to use + * @param Localization $l11n Localization + * @param float|FloatInt $percentage Percentage value to print + * @param null|string $format Format type to use * * @return string * * @since 1.0.0 */ - public function getPercentage(Localization $l11n, float $percentage, string $format = null) : string + public function getPercentage(Localization $l11n, float | FloatInt $percentage, ?string $format = null) : string { return \number_format( - $percentage, $l11n->getPrecision()[$format ?? 'medium'], + \is_float($percentage) ? $percentage : $percentage->value / (FloatInt::DIVISOR * 100), + $l11n->getPrecision()[$format ?? 'medium'], $l11n->getDecimal(), $l11n->getThousands() ) . '%'; @@ -268,16 +269,16 @@ final class L11nManager public function getCurrency( Localization $l11n, int | float | Money | FloatInt $currency, - string $symbol = null, - string $format = null, + ?string $symbol = null, + ?string $format = null, int $divide = 1 ) : string { $language = $l11n->language; - $symbol ??= $l11n->getCurrency(); + $symbol ??= $l11n->currency; if (\is_float($currency)) { - $currency = (int) ($currency * \pow(10, Money::MAX_DECIMALS)); + $currency = (int) ($currency * FloatInt::DIVISOR); } if ($divide > 1 && !empty($symbol)) { @@ -320,7 +321,7 @@ final class L11nManager * * @since 1.0.0 */ - public function getDateTime(Localization $l11n, \DateTimeInterface $datetime = null, string $format = null) : string + public function getDateTime(Localization $l11n, ?\DateTimeInterface $datetime = null, ?string $format = null) : string { return $datetime === null ? '' diff --git a/Localization/LanguageDetection/Language.php b/Localization/LanguageDetection/Language.php index f47d6286a..d9b253831 100755 --- a/Localization/LanguageDetection/Language.php +++ b/Localization/LanguageDetection/Language.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace phpOMS\Localization\LanguageDetection; /** - * Langauge detection class + * Language detection class * * @package phpOMS\Localization\LanguageDetection * @license https://opensource.org/licenses/mit-license.html MIT @@ -49,7 +49,7 @@ class Language extends NgramParser } elseif (!\is_dir($dirname) || !\is_readable($dirname)) { throw new \InvalidArgumentException('Provided directory could not be found or is not readable'); } else { - $dirname = \rtrim($dirname, '/'); + $dirname = \rtrim($dirname, '/'); $dirname .= '/*/*.php'; } @@ -93,8 +93,8 @@ class Language extends NgramParser foreach ($samples as $v) { if (isset($value[$v])) { - $x = $index++ - $value[$v]; - $y = $x >> (\PHP_INT_SIZE * 8); + $x = $index++ - $value[$v]; + $y = $x >> (\PHP_INT_SIZE * 8); $sum += ($x + $y) ^ $y; continue; diff --git a/Localization/LanguageDetection/LanguageResult.php b/Localization/LanguageDetection/LanguageResult.php index dc54b8c20..643b76884 100755 --- a/Localization/LanguageDetection/LanguageResult.php +++ b/Localization/LanguageDetection/LanguageResult.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace phpOMS\Localization\LanguageDetection; /** - * Langauge match result + * Language match result * * @package phpOMS\Localization\LanguageDetection * @license https://opensource.org/licenses/mit-license.html MIT @@ -36,14 +36,14 @@ class LanguageResult implements \ArrayAccess, \IteratorAggregate, \JsonSerializa * Match values per language * * @var array - * @sicne 1.0.0 + * @since 1.0.0 */ private array $result = []; /** * Constructor. * - * @param array $result Langauge match results + * @param array $result Language match results * * @since 1.0.0 */ @@ -188,7 +188,7 @@ class LanguageResult implements \ArrayAccess, \IteratorAggregate, \JsonSerializa * * @since 1.0.0 */ - public function limit(int $offset, int $length = null) : self + public function limit(int $offset, ?int $length = null) : self { return new self(\array_slice($this->result, $offset, $length)); } diff --git a/Localization/LanguageDetection/NgramParser.php b/Localization/LanguageDetection/NgramParser.php index 06e2cfc6b..20568df8d 100755 --- a/Localization/LanguageDetection/NgramParser.php +++ b/Localization/LanguageDetection/NgramParser.php @@ -69,9 +69,7 @@ abstract class NgramParser */ private function tokenize(string $str) : array { - if ($this->tokenizer === null) { - $this->tokenizer = new WhitespaceTokenizer(); - } + $this->tokenizer ??= new WhitespaceTokenizer(); return $this->tokenizer->tokenize($str); } diff --git a/Localization/LanguageDetection/Trainer.php b/Localization/LanguageDetection/Trainer.php index 9fbbc445a..ba480fa39 100755 --- a/Localization/LanguageDetection/Trainer.php +++ b/Localization/LanguageDetection/Trainer.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace phpOMS\Localization\LanguageDetection; /** - * Langauge training class + * Language training class * * @package phpOMS\Localization\LanguageDetection * @license https://opensource.org/licenses/mit-license.html MIT @@ -42,7 +42,7 @@ class Trainer extends NgramParser } elseif (!\is_dir($dirname) || !\is_readable($dirname)) { throw new \InvalidArgumentException('Provided directory could not be found or is not readable'); } else { - $dirname = \rtrim($dirname, '/'); + $dirname = \rtrim($dirname, '/'); $dirname .= '/*/*.txt'; } diff --git a/Localization/LanguageDetection/resources/ab/ab.php b/Localization/LanguageDetection/resources/ab/ab.php index 9900d161d..e7acb1b27 100755 --- a/Localization/LanguageDetection/resources/ab/ab.php +++ b/Localization/LanguageDetection/resources/ab/ab.php @@ -1,8 +1,7 @@ - [ + 'ab' => [ 0 => 'а', 1 => 'и', 2 => 'р', diff --git a/Localization/LanguageDetection/resources/af/af.php b/Localization/LanguageDetection/resources/af/af.php index 0b1c8f348..099ccc68a 100755 --- a/Localization/LanguageDetection/resources/af/af.php +++ b/Localization/LanguageDetection/resources/af/af.php @@ -1,8 +1,7 @@ - [ + 'af' => [ 0 => 'e', 1 => 'n', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/am/am.php b/Localization/LanguageDetection/resources/am/am.php index 77f9e8673..d21128b58 100755 --- a/Localization/LanguageDetection/resources/am/am.php +++ b/Localization/LanguageDetection/resources/am/am.php @@ -1,8 +1,7 @@ - [ + 'am' => [ 0 => 'ን', 1 => 'ት', 2 => 'ት_', diff --git a/Localization/LanguageDetection/resources/ar/ar.php b/Localization/LanguageDetection/resources/ar/ar.php index 61035f1e7..f14b47f85 100755 --- a/Localization/LanguageDetection/resources/ar/ar.php +++ b/Localization/LanguageDetection/resources/ar/ar.php @@ -1,8 +1,7 @@ - [ + 'ar' => [ 0 => 'ا', 1 => 'ل', 2 => 'ال', diff --git a/Localization/LanguageDetection/resources/ay/ay.php b/Localization/LanguageDetection/resources/ay/ay.php index e9dc659ae..4b338e279 100755 --- a/Localization/LanguageDetection/resources/ay/ay.php +++ b/Localization/LanguageDetection/resources/ay/ay.php @@ -1,8 +1,7 @@ - [ + 'ay' => [ 0 => 'a', 1 => 'i', 2 => 'k', diff --git a/Localization/LanguageDetection/resources/az-Cyrl/az-Cyrl.php b/Localization/LanguageDetection/resources/az-Cyrl/az-Cyrl.php index eaae9a271..847d83d7a 100755 --- a/Localization/LanguageDetection/resources/az-Cyrl/az-Cyrl.php +++ b/Localization/LanguageDetection/resources/az-Cyrl/az-Cyrl.php @@ -1,8 +1,7 @@ - [ + 'az-Cyrl' => [ 0 => 'ә', 1 => 'а', 2 => 'и', diff --git a/Localization/LanguageDetection/resources/az-Latn/az-Latn.php b/Localization/LanguageDetection/resources/az-Latn/az-Latn.php index cca7252de..273fc151a 100755 --- a/Localization/LanguageDetection/resources/az-Latn/az-Latn.php +++ b/Localization/LanguageDetection/resources/az-Latn/az-Latn.php @@ -1,8 +1,7 @@ - [ + 'az-Latn' => [ 0 => 'ə', 1 => 'a', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/be/be.php b/Localization/LanguageDetection/resources/be/be.php index f11b43194..c2acbca1e 100755 --- a/Localization/LanguageDetection/resources/be/be.php +++ b/Localization/LanguageDetection/resources/be/be.php @@ -1,8 +1,7 @@ - [ + 'be' => [ 0 => 'а', 1 => 'н', 2 => 'ы', diff --git a/Localization/LanguageDetection/resources/bg/bg.php b/Localization/LanguageDetection/resources/bg/bg.php index 13493588c..fc3f140ea 100755 --- a/Localization/LanguageDetection/resources/bg/bg.php +++ b/Localization/LanguageDetection/resources/bg/bg.php @@ -1,8 +1,7 @@ - [ + 'bg' => [ 0 => 'а', 1 => 'о', 2 => 'и', diff --git a/Localization/LanguageDetection/resources/bi/bi.php b/Localization/LanguageDetection/resources/bi/bi.php index bd827a197..a426fa181 100755 --- a/Localization/LanguageDetection/resources/bi/bi.php +++ b/Localization/LanguageDetection/resources/bi/bi.php @@ -1,8 +1,7 @@ - [ + 'bi' => [ 0 => 'o', 1 => 'e', 2 => 'a', diff --git a/Localization/LanguageDetection/resources/bn/bn.php b/Localization/LanguageDetection/resources/bn/bn.php index 2c852f1e1..62d1af187 100755 --- a/Localization/LanguageDetection/resources/bn/bn.php +++ b/Localization/LanguageDetection/resources/bn/bn.php @@ -1,8 +1,7 @@ - [ + 'bn' => [ 0 => '_র_', 1 => '_র', 2 => 'র_', diff --git a/Localization/LanguageDetection/resources/bo/bo.php b/Localization/LanguageDetection/resources/bo/bo.php index bc200c5bc..f62123880 100755 --- a/Localization/LanguageDetection/resources/bo/bo.php +++ b/Localization/LanguageDetection/resources/bo/bo.php @@ -1,8 +1,7 @@ - [ + 'bo' => [ 0 => 'ས_', 1 => '_ས_', 2 => 'ས', diff --git a/Localization/LanguageDetection/resources/br/br.php b/Localization/LanguageDetection/resources/br/br.php index 2c1579843..14b8f6ef0 100755 --- a/Localization/LanguageDetection/resources/br/br.php +++ b/Localization/LanguageDetection/resources/br/br.php @@ -1,8 +1,7 @@ - [ + 'br' => [ 0 => 'e', 1 => 'a', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/bs-Cyrl/bs-Cyrl.php b/Localization/LanguageDetection/resources/bs-Cyrl/bs-Cyrl.php index 7cb0c28a7..b32fcfb54 100755 --- a/Localization/LanguageDetection/resources/bs-Cyrl/bs-Cyrl.php +++ b/Localization/LanguageDetection/resources/bs-Cyrl/bs-Cyrl.php @@ -1,8 +1,7 @@ - [ + 'bs-Cyrl' => [ 0 => 'а', 1 => 'и', 2 => 'о', diff --git a/Localization/LanguageDetection/resources/bs-Latn/bs-Latn.php b/Localization/LanguageDetection/resources/bs-Latn/bs-Latn.php index da584f07e..762105f9a 100755 --- a/Localization/LanguageDetection/resources/bs-Latn/bs-Latn.php +++ b/Localization/LanguageDetection/resources/bs-Latn/bs-Latn.php @@ -1,8 +1,7 @@ - [ + 'bs-Latn' => [ 0 => 'a', 1 => 'i', 2 => 'o', diff --git a/Localization/LanguageDetection/resources/ca/ca.php b/Localization/LanguageDetection/resources/ca/ca.php index 7701459eb..74fa39b14 100755 --- a/Localization/LanguageDetection/resources/ca/ca.php +++ b/Localization/LanguageDetection/resources/ca/ca.php @@ -1,8 +1,7 @@ - [ + 'ca' => [ 0 => 'e', 1 => 'a', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/ch/ch.php b/Localization/LanguageDetection/resources/ch/ch.php index 88fae9f85..6fcf112fe 100755 --- a/Localization/LanguageDetection/resources/ch/ch.php +++ b/Localization/LanguageDetection/resources/ch/ch.php @@ -1,8 +1,7 @@ - [ + 'ch' => [ 0 => 'a', 1 => 'i', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/co/co.php b/Localization/LanguageDetection/resources/co/co.php index 9bea91ec6..10e1ec17e 100755 --- a/Localization/LanguageDetection/resources/co/co.php +++ b/Localization/LanguageDetection/resources/co/co.php @@ -1,8 +1,7 @@ - [ + 'co' => [ 0 => 'i', 1 => 'a', 2 => 'u', diff --git a/Localization/LanguageDetection/resources/cr/cr.php b/Localization/LanguageDetection/resources/cr/cr.php index acf54e2af..82547afce 100755 --- a/Localization/LanguageDetection/resources/cr/cr.php +++ b/Localization/LanguageDetection/resources/cr/cr.php @@ -1,8 +1,7 @@ - [ + 'cr' => [ 0 => 'ᓂ', 1 => 'ᑕ', 2 => 'ᑭ', diff --git a/Localization/LanguageDetection/resources/cs/cs.php b/Localization/LanguageDetection/resources/cs/cs.php index acde29a90..b84892e67 100755 --- a/Localization/LanguageDetection/resources/cs/cs.php +++ b/Localization/LanguageDetection/resources/cs/cs.php @@ -1,8 +1,7 @@ - [ + 'cs' => [ 0 => 'o', 1 => 'n', 2 => 'e', diff --git a/Localization/LanguageDetection/resources/cy/cy.php b/Localization/LanguageDetection/resources/cy/cy.php index aade5a244..1a32754df 100755 --- a/Localization/LanguageDetection/resources/cy/cy.php +++ b/Localization/LanguageDetection/resources/cy/cy.php @@ -1,8 +1,7 @@ - [ + 'cy' => [ 0 => 'a', 1 => 'd', 2 => 'y', diff --git a/Localization/LanguageDetection/resources/da/da.php b/Localization/LanguageDetection/resources/da/da.php index 39c7544dd..e73a4d5c2 100755 --- a/Localization/LanguageDetection/resources/da/da.php +++ b/Localization/LanguageDetection/resources/da/da.php @@ -1,8 +1,7 @@ - [ + 'da' => [ 0 => 'e', 1 => 'r', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/de/de.php b/Localization/LanguageDetection/resources/de/de.php index 66d8a808d..3e6fbd3fc 100755 --- a/Localization/LanguageDetection/resources/de/de.php +++ b/Localization/LanguageDetection/resources/de/de.php @@ -1,8 +1,7 @@ - [ + 'de' => [ 0 => 'e', 1 => 'n', 2 => 'r', diff --git a/Localization/LanguageDetection/resources/dz/dz.php b/Localization/LanguageDetection/resources/dz/dz.php index dc7d3cd76..f8349afb2 100755 --- a/Localization/LanguageDetection/resources/dz/dz.php +++ b/Localization/LanguageDetection/resources/dz/dz.php @@ -1,8 +1,7 @@ - [ + 'dz' => [ 0 => 'ས_', 1 => '_ད', 2 => '_ག', diff --git a/Localization/LanguageDetection/resources/el-monoton/el-monoton.php b/Localization/LanguageDetection/resources/el-monoton/el-monoton.php index 614798d08..7d2e52179 100755 --- a/Localization/LanguageDetection/resources/el-monoton/el-monoton.php +++ b/Localization/LanguageDetection/resources/el-monoton/el-monoton.php @@ -1,8 +1,7 @@ - [ + 'el-monoton' => [ 0 => 'α', 1 => 'ι', 2 => 'ε', diff --git a/Localization/LanguageDetection/resources/el-polyton/el-polyton.php b/Localization/LanguageDetection/resources/el-polyton/el-polyton.php index cc09972b6..87357c376 100755 --- a/Localization/LanguageDetection/resources/el-polyton/el-polyton.php +++ b/Localization/LanguageDetection/resources/el-polyton/el-polyton.php @@ -1,8 +1,7 @@ - [ + 'el-polyton' => [ 0 => 'α', 1 => 'τ', 2 => 'ι', diff --git a/Localization/LanguageDetection/resources/en/en.php b/Localization/LanguageDetection/resources/en/en.php index 0771e1924..a5be3e423 100755 --- a/Localization/LanguageDetection/resources/en/en.php +++ b/Localization/LanguageDetection/resources/en/en.php @@ -1,8 +1,7 @@ - [ + 'en' => [ 0 => 'e', 1 => 't', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/eo/eo.php b/Localization/LanguageDetection/resources/eo/eo.php index 6b93bed48..e4d23e3f7 100755 --- a/Localization/LanguageDetection/resources/eo/eo.php +++ b/Localization/LanguageDetection/resources/eo/eo.php @@ -1,8 +1,7 @@ - [ + 'eo' => [ 0 => 'a', 1 => 'e', 2 => 'o', diff --git a/Localization/LanguageDetection/resources/es/es.php b/Localization/LanguageDetection/resources/es/es.php index a63eb4983..e14dbc05f 100755 --- a/Localization/LanguageDetection/resources/es/es.php +++ b/Localization/LanguageDetection/resources/es/es.php @@ -1,8 +1,7 @@ - [ + 'es' => [ 0 => 'e', 1 => 'a', 2 => 'o', diff --git a/Localization/LanguageDetection/resources/et/et.php b/Localization/LanguageDetection/resources/et/et.php index 3928a8c77..667c48517 100755 --- a/Localization/LanguageDetection/resources/et/et.php +++ b/Localization/LanguageDetection/resources/et/et.php @@ -1,8 +1,7 @@ - [ + 'et' => [ 0 => 'a', 1 => 'e', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/eu/eu.php b/Localization/LanguageDetection/resources/eu/eu.php index 67ec09dab..8bd892015 100755 --- a/Localization/LanguageDetection/resources/eu/eu.php +++ b/Localization/LanguageDetection/resources/eu/eu.php @@ -1,8 +1,7 @@ - [ + 'eu' => [ 0 => 'a', 1 => 'e', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/fa/fa.php b/Localization/LanguageDetection/resources/fa/fa.php index 271a93e80..7ec1855b1 100755 --- a/Localization/LanguageDetection/resources/fa/fa.php +++ b/Localization/LanguageDetection/resources/fa/fa.php @@ -1,8 +1,7 @@ - [ + 'fa' => [ 0 => 'ا', 1 => 'ی', 2 => 'د', diff --git a/Localization/LanguageDetection/resources/fi/fi.php b/Localization/LanguageDetection/resources/fi/fi.php index 0efdcd4c4..f3358b645 100755 --- a/Localization/LanguageDetection/resources/fi/fi.php +++ b/Localization/LanguageDetection/resources/fi/fi.php @@ -1,8 +1,7 @@ - [ + 'fi' => [ 0 => 'a', 1 => 'i', 2 => 't', diff --git a/Localization/LanguageDetection/resources/fj/fj.php b/Localization/LanguageDetection/resources/fj/fj.php index 27f106019..92720806c 100755 --- a/Localization/LanguageDetection/resources/fj/fj.php +++ b/Localization/LanguageDetection/resources/fj/fj.php @@ -1,8 +1,7 @@ - [ + 'fj' => [ 0 => 'a', 1 => 'a_', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/fo/fo.php b/Localization/LanguageDetection/resources/fo/fo.php index 906ed60bb..e583b6a9d 100755 --- a/Localization/LanguageDetection/resources/fo/fo.php +++ b/Localization/LanguageDetection/resources/fo/fo.php @@ -1,8 +1,7 @@ - [ + 'fo' => [ 0 => 'a', 1 => 'i', 2 => 'r', diff --git a/Localization/LanguageDetection/resources/fr/fr.php b/Localization/LanguageDetection/resources/fr/fr.php index aacc5a6b6..07d782d8c 100755 --- a/Localization/LanguageDetection/resources/fr/fr.php +++ b/Localization/LanguageDetection/resources/fr/fr.php @@ -1,8 +1,7 @@ - [ + 'fr' => [ 0 => 'e', 1 => 't', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/fy/fy.php b/Localization/LanguageDetection/resources/fy/fy.php index 91f3b5c52..0c18b3abe 100755 --- a/Localization/LanguageDetection/resources/fy/fy.php +++ b/Localization/LanguageDetection/resources/fy/fy.php @@ -1,8 +1,7 @@ - [ + 'fy' => [ 0 => 'e', 1 => 'n', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/ga/ga.php b/Localization/LanguageDetection/resources/ga/ga.php index 22346226f..ef46ac8b2 100755 --- a/Localization/LanguageDetection/resources/ga/ga.php +++ b/Localization/LanguageDetection/resources/ga/ga.php @@ -1,8 +1,7 @@ - [ + 'ga' => [ 0 => 'a', 1 => 'i', 2 => 'h', diff --git a/Localization/LanguageDetection/resources/gd/gd.php b/Localization/LanguageDetection/resources/gd/gd.php index 5d5812af8..571dc7d75 100755 --- a/Localization/LanguageDetection/resources/gd/gd.php +++ b/Localization/LanguageDetection/resources/gd/gd.php @@ -1,8 +1,7 @@ - [ + 'gd' => [ 0 => 'a', 1 => 'h', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/gl/gl.php b/Localization/LanguageDetection/resources/gl/gl.php index c797f999b..ce0d43bef 100755 --- a/Localization/LanguageDetection/resources/gl/gl.php +++ b/Localization/LanguageDetection/resources/gl/gl.php @@ -1,8 +1,7 @@ - [ + 'gl' => [ 0 => 'e', 1 => 'a', 2 => 'o', diff --git a/Localization/LanguageDetection/resources/gn/gn.php b/Localization/LanguageDetection/resources/gn/gn.php index 60f0892f0..dbd4060be 100755 --- a/Localization/LanguageDetection/resources/gn/gn.php +++ b/Localization/LanguageDetection/resources/gn/gn.php @@ -1,8 +1,7 @@ - [ + 'gn' => [ 0 => 'a', 1 => 'e', 2 => 'o', diff --git a/Localization/LanguageDetection/resources/gu/gu.php b/Localization/LanguageDetection/resources/gu/gu.php index 13d22356f..72d74deb0 100755 --- a/Localization/LanguageDetection/resources/gu/gu.php +++ b/Localization/LanguageDetection/resources/gu/gu.php @@ -1,8 +1,7 @@ - [ + 'gu' => [ 0 => 'ન_', 1 => '_ન_', 2 => '_ક_', diff --git a/Localization/LanguageDetection/resources/ha/ha.php b/Localization/LanguageDetection/resources/ha/ha.php index 850ec877c..ab3d1fd8c 100755 --- a/Localization/LanguageDetection/resources/ha/ha.php +++ b/Localization/LanguageDetection/resources/ha/ha.php @@ -1,8 +1,7 @@ - [ + 'ha' => [ 0 => 'a', 1 => 'ɛ', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/he/he.php b/Localization/LanguageDetection/resources/he/he.php index 1c486d840..58cb45a52 100755 --- a/Localization/LanguageDetection/resources/he/he.php +++ b/Localization/LanguageDetection/resources/he/he.php @@ -1,8 +1,7 @@ - [ + 'he' => [ 0 => 'ו', 1 => 'י', 2 => 'ה', diff --git a/Localization/LanguageDetection/resources/hi/hi.php b/Localization/LanguageDetection/resources/hi/hi.php index 88b4473b4..050de2c20 100755 --- a/Localization/LanguageDetection/resources/hi/hi.php +++ b/Localization/LanguageDetection/resources/hi/hi.php @@ -1,8 +1,7 @@ - [ + 'hi' => [ 0 => '_क_', 1 => 'क_', 2 => '_क', diff --git a/Localization/LanguageDetection/resources/hr/hr.php b/Localization/LanguageDetection/resources/hr/hr.php index 7942f90cd..cef271ab8 100755 --- a/Localization/LanguageDetection/resources/hr/hr.php +++ b/Localization/LanguageDetection/resources/hr/hr.php @@ -1,8 +1,7 @@ - [ + 'hr' => [ 0 => 'a', 1 => 'o', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/hu/hu.php b/Localization/LanguageDetection/resources/hu/hu.php index e2fd9bfd4..abd76690f 100755 --- a/Localization/LanguageDetection/resources/hu/hu.php +++ b/Localization/LanguageDetection/resources/hu/hu.php @@ -1,8 +1,7 @@ - [ + 'hu' => [ 0 => 'e', 1 => 'a', 2 => 's', diff --git a/Localization/LanguageDetection/resources/hy/hy.php b/Localization/LanguageDetection/resources/hy/hy.php index 3cf4da028..1da809cd8 100755 --- a/Localization/LanguageDetection/resources/hy/hy.php +++ b/Localization/LanguageDetection/resources/hy/hy.php @@ -1,8 +1,7 @@ - [ + 'hy' => [ 0 => 'ա', 1 => 'ն', 2 => 'ո', diff --git a/Localization/LanguageDetection/resources/ia/ia.php b/Localization/LanguageDetection/resources/ia/ia.php index a107d44cc..610f768aa 100755 --- a/Localization/LanguageDetection/resources/ia/ia.php +++ b/Localization/LanguageDetection/resources/ia/ia.php @@ -1,8 +1,7 @@ - [ + 'ia' => [ 0 => 'e', 1 => 't', 2 => 'a', diff --git a/Localization/LanguageDetection/resources/id/id.php b/Localization/LanguageDetection/resources/id/id.php index 5e125adee..ccff76e46 100755 --- a/Localization/LanguageDetection/resources/id/id.php +++ b/Localization/LanguageDetection/resources/id/id.php @@ -1,8 +1,7 @@ - [ + 'id' => [ 0 => 'a', 1 => 'n', 2 => 'e', diff --git a/Localization/LanguageDetection/resources/ig/ig.php b/Localization/LanguageDetection/resources/ig/ig.php index 806d7c1f0..5d708854e 100755 --- a/Localization/LanguageDetection/resources/ig/ig.php +++ b/Localization/LanguageDetection/resources/ig/ig.php @@ -1,8 +1,7 @@ - [ + 'ig' => [ 0 => 'a', 1 => 'e', 2 => 'a_', diff --git a/Localization/LanguageDetection/resources/io/io.php b/Localization/LanguageDetection/resources/io/io.php index bd4a48715..2b6a8baf9 100755 --- a/Localization/LanguageDetection/resources/io/io.php +++ b/Localization/LanguageDetection/resources/io/io.php @@ -1,8 +1,7 @@ - [ + 'io' => [ 0 => 'a', 1 => 'e', 2 => 'o', diff --git a/Localization/LanguageDetection/resources/is/is.php b/Localization/LanguageDetection/resources/is/is.php index 888d8c573..ea11e28b6 100755 --- a/Localization/LanguageDetection/resources/is/is.php +++ b/Localization/LanguageDetection/resources/is/is.php @@ -1,8 +1,7 @@ - [ + 'is' => [ 0 => 'a', 1 => 'r', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/it/it.php b/Localization/LanguageDetection/resources/it/it.php index c4fafd0d3..b8dc05043 100755 --- a/Localization/LanguageDetection/resources/it/it.php +++ b/Localization/LanguageDetection/resources/it/it.php @@ -1,8 +1,7 @@ - [ + 'it' => [ 0 => 'i', 1 => 'e', 2 => 'a', diff --git a/Localization/LanguageDetection/resources/iu/iu.php b/Localization/LanguageDetection/resources/iu/iu.php index 5c945a981..6f91434d4 100755 --- a/Localization/LanguageDetection/resources/iu/iu.php +++ b/Localization/LanguageDetection/resources/iu/iu.php @@ -1,8 +1,7 @@ - [ + 'iu' => [ 0 => 'ᑦ', 1 => 'ᑦ_', 2 => 'ᑎ', diff --git a/Localization/LanguageDetection/resources/ja/ja.php b/Localization/LanguageDetection/resources/ja/ja.php index e72b4b417..38a841370 100755 --- a/Localization/LanguageDetection/resources/ja/ja.php +++ b/Localization/LanguageDetection/resources/ja/ja.php @@ -1,8 +1,7 @@ - [ + 'ja' => [ 0 => 'の', 1 => 'る', 2 => 'に', diff --git a/Localization/LanguageDetection/resources/jv/jv.php b/Localization/LanguageDetection/resources/jv/jv.php index 435dd58b5..d5f0657ea 100755 --- a/Localization/LanguageDetection/resources/jv/jv.php +++ b/Localization/LanguageDetection/resources/jv/jv.php @@ -1,8 +1,7 @@ - [ + 'jv' => [ 0 => 'a', 1 => 'n', 2 => 'an', diff --git a/Localization/LanguageDetection/resources/ka/ka.php b/Localization/LanguageDetection/resources/ka/ka.php index c5c3ccf91..73c193101 100755 --- a/Localization/LanguageDetection/resources/ka/ka.php +++ b/Localization/LanguageDetection/resources/ka/ka.php @@ -1,8 +1,7 @@ - [ + 'ka' => [ 0 => 'ა', 1 => 'ი', 2 => 'ე', diff --git a/Localization/LanguageDetection/resources/km/km.php b/Localization/LanguageDetection/resources/km/km.php index 5cc2326b2..dc325d17e 100755 --- a/Localization/LanguageDetection/resources/km/km.php +++ b/Localization/LanguageDetection/resources/km/km.php @@ -1,8 +1,7 @@ - [ + 'km' => [ 0 => '_រ', 1 => 'រ', 2 => '_ន_', diff --git a/Localization/LanguageDetection/resources/ko/ko.php b/Localization/LanguageDetection/resources/ko/ko.php index 2732be778..161bf1b9d 100755 --- a/Localization/LanguageDetection/resources/ko/ko.php +++ b/Localization/LanguageDetection/resources/ko/ko.php @@ -1,8 +1,7 @@ - [ + 'ko' => [ 0 => '의', 1 => '한', 2 => '의_', diff --git a/Localization/LanguageDetection/resources/kr/kr.php b/Localization/LanguageDetection/resources/kr/kr.php index aa0dfc84a..d4aa4cf37 100755 --- a/Localization/LanguageDetection/resources/kr/kr.php +++ b/Localization/LanguageDetection/resources/kr/kr.php @@ -1,8 +1,7 @@ - [ + 'kr' => [ 0 => 'a', 1 => 'n', 2 => 'ə', diff --git a/Localization/LanguageDetection/resources/ku/ku.php b/Localization/LanguageDetection/resources/ku/ku.php index 321b0b164..b1178d8f6 100755 --- a/Localization/LanguageDetection/resources/ku/ku.php +++ b/Localization/LanguageDetection/resources/ku/ku.php @@ -1,8 +1,7 @@ - [ + 'ku' => [ 0 => 'e', 1 => 'a', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/la/la.php b/Localization/LanguageDetection/resources/la/la.php index 3fcd391d2..5bb368ea7 100755 --- a/Localization/LanguageDetection/resources/la/la.php +++ b/Localization/LanguageDetection/resources/la/la.php @@ -1,8 +1,7 @@ - [ + 'la' => [ 0 => 'i', 1 => 'e', 2 => 't', diff --git a/Localization/LanguageDetection/resources/lg/lg.php b/Localization/LanguageDetection/resources/lg/lg.php index c462e273d..38760ca1d 100755 --- a/Localization/LanguageDetection/resources/lg/lg.php +++ b/Localization/LanguageDetection/resources/lg/lg.php @@ -1,8 +1,7 @@ - [ + 'lg' => [ 0 => 'a', 1 => 'u', 2 => 'e', diff --git a/Localization/LanguageDetection/resources/ln/ln.php b/Localization/LanguageDetection/resources/ln/ln.php index 68b2d3406..46b28c977 100755 --- a/Localization/LanguageDetection/resources/ln/ln.php +++ b/Localization/LanguageDetection/resources/ln/ln.php @@ -1,8 +1,7 @@ - [ + 'ln' => [ 0 => 'a', 1 => 'o', 2 => 'a_', diff --git a/Localization/LanguageDetection/resources/lo/lo.php b/Localization/LanguageDetection/resources/lo/lo.php index 215e7d27f..8fd83f1a2 100755 --- a/Localization/LanguageDetection/resources/lo/lo.php +++ b/Localization/LanguageDetection/resources/lo/lo.php @@ -1,8 +1,7 @@ - [ + 'lo' => [ 0 => 'ນ', 1 => 'າ', 2 => 'ດ', diff --git a/Localization/LanguageDetection/resources/lt/lt.php b/Localization/LanguageDetection/resources/lt/lt.php index a8f6885e6..6c8ca625c 100755 --- a/Localization/LanguageDetection/resources/lt/lt.php +++ b/Localization/LanguageDetection/resources/lt/lt.php @@ -1,8 +1,7 @@ - [ + 'lt' => [ 0 => 'i', 1 => 'a', 2 => 's', diff --git a/Localization/LanguageDetection/resources/lv/lv.php b/Localization/LanguageDetection/resources/lv/lv.php index 86eedef6c..97a04675f 100755 --- a/Localization/LanguageDetection/resources/lv/lv.php +++ b/Localization/LanguageDetection/resources/lv/lv.php @@ -1,8 +1,7 @@ - [ + 'lv' => [ 0 => 'a', 1 => 'i', 2 => 's', diff --git a/Localization/LanguageDetection/resources/mh/mh.php b/Localization/LanguageDetection/resources/mh/mh.php index 0aa5f7f63..92f4c1515 100755 --- a/Localization/LanguageDetection/resources/mh/mh.php +++ b/Localization/LanguageDetection/resources/mh/mh.php @@ -1,8 +1,7 @@ - [ + 'mh' => [ 0 => 'o', 1 => 'e', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/mn-Cyrl/mn-Cyrl.php b/Localization/LanguageDetection/resources/mn-Cyrl/mn-Cyrl.php index 3d14c9fbc..3f0e14525 100755 --- a/Localization/LanguageDetection/resources/mn-Cyrl/mn-Cyrl.php +++ b/Localization/LanguageDetection/resources/mn-Cyrl/mn-Cyrl.php @@ -1,8 +1,7 @@ - [ + 'mn-Cyrl' => [ 0 => 'а', 1 => 'э', 2 => 'л', diff --git a/Localization/LanguageDetection/resources/ms-Arab/ms-Arab.php b/Localization/LanguageDetection/resources/ms-Arab/ms-Arab.php index 4b3a00903..e892cf915 100755 --- a/Localization/LanguageDetection/resources/ms-Arab/ms-Arab.php +++ b/Localization/LanguageDetection/resources/ms-Arab/ms-Arab.php @@ -1,8 +1,7 @@ - [ + 'ms-Arab' => [ 0 => 'ا', 1 => 'ن', 2 => 'ي', diff --git a/Localization/LanguageDetection/resources/ms-Latn/ms-Latn.php b/Localization/LanguageDetection/resources/ms-Latn/ms-Latn.php index 42bc87daa..63a9c43e2 100755 --- a/Localization/LanguageDetection/resources/ms-Latn/ms-Latn.php +++ b/Localization/LanguageDetection/resources/ms-Latn/ms-Latn.php @@ -1,8 +1,7 @@ - [ + 'ms-Latn' => [ 0 => 'a', 1 => 'n', 2 => 'e', diff --git a/Localization/LanguageDetection/resources/mt/mt.php b/Localization/LanguageDetection/resources/mt/mt.php index 8af896b94..ce1e7de22 100755 --- a/Localization/LanguageDetection/resources/mt/mt.php +++ b/Localization/LanguageDetection/resources/mt/mt.php @@ -1,8 +1,7 @@ - [ + 'mt' => [ 0 => 'i', 1 => 'a', 2 => 'l', diff --git a/Localization/LanguageDetection/resources/nb/nb.php b/Localization/LanguageDetection/resources/nb/nb.php index 56877d1bf..29cca937c 100755 --- a/Localization/LanguageDetection/resources/nb/nb.php +++ b/Localization/LanguageDetection/resources/nb/nb.php @@ -1,8 +1,7 @@ - [ + 'nb' => [ 0 => 'e', 1 => 'r', 2 => 't', diff --git a/Localization/LanguageDetection/resources/ng/ng.php b/Localization/LanguageDetection/resources/ng/ng.php index af07599b9..fdd925262 100755 --- a/Localization/LanguageDetection/resources/ng/ng.php +++ b/Localization/LanguageDetection/resources/ng/ng.php @@ -1,8 +1,7 @@ - [ + 'ng' => [ 0 => 'a', 1 => 'o', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/nl/nl.php b/Localization/LanguageDetection/resources/nl/nl.php index c80d3de44..f0e69f6cd 100755 --- a/Localization/LanguageDetection/resources/nl/nl.php +++ b/Localization/LanguageDetection/resources/nl/nl.php @@ -1,8 +1,7 @@ - [ + 'nl' => [ 0 => 'e', 1 => 'n', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/nn/nn.php b/Localization/LanguageDetection/resources/nn/nn.php index b3c04cd7a..91fa9222e 100755 --- a/Localization/LanguageDetection/resources/nn/nn.php +++ b/Localization/LanguageDetection/resources/nn/nn.php @@ -1,8 +1,7 @@ - [ + 'nn' => [ 0 => 'e', 1 => 'a', 2 => 'r', diff --git a/Localization/LanguageDetection/resources/nv/nv.php b/Localization/LanguageDetection/resources/nv/nv.php index e68a119ac..5c36f7855 100755 --- a/Localization/LanguageDetection/resources/nv/nv.php +++ b/Localization/LanguageDetection/resources/nv/nv.php @@ -1,8 +1,7 @@ - [ + 'nv' => [ 0 => 'a', 1 => 'i', 2 => 'h', diff --git a/Localization/LanguageDetection/resources/oc/oc.php b/Localization/LanguageDetection/resources/oc/oc.php index 04b4a1ccf..9cf3622ba 100755 --- a/Localization/LanguageDetection/resources/oc/oc.php +++ b/Localization/LanguageDetection/resources/oc/oc.php @@ -1,8 +1,7 @@ - [ + 'oc' => [ 0 => 'a', 1 => 'e', 2 => 's', diff --git a/Localization/LanguageDetection/resources/om/om.php b/Localization/LanguageDetection/resources/om/om.php index 6fbe522cc..06d3758f6 100755 --- a/Localization/LanguageDetection/resources/om/om.php +++ b/Localization/LanguageDetection/resources/om/om.php @@ -1,8 +1,7 @@ - [ + 'om' => [ 0 => 'a', 1 => 'i', 2 => 'u', diff --git a/Localization/LanguageDetection/resources/pl/pl.php b/Localization/LanguageDetection/resources/pl/pl.php index 48b5110ed..230fbaefa 100755 --- a/Localization/LanguageDetection/resources/pl/pl.php +++ b/Localization/LanguageDetection/resources/pl/pl.php @@ -1,8 +1,7 @@ - [ + 'pl' => [ 0 => 'a', 1 => 'o', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/pt-BR/pt-BR.php b/Localization/LanguageDetection/resources/pt-BR/pt-BR.php index 4e2dcfdd7..0cc805f3c 100755 --- a/Localization/LanguageDetection/resources/pt-BR/pt-BR.php +++ b/Localization/LanguageDetection/resources/pt-BR/pt-BR.php @@ -1,8 +1,7 @@ - [ + 'pt-BR' => [ 0 => 'e', 1 => 'o', 2 => 'a', diff --git a/Localization/LanguageDetection/resources/pt-PT/pt-PT.php b/Localization/LanguageDetection/resources/pt-PT/pt-PT.php index 6dbadb628..bce0d0b2a 100755 --- a/Localization/LanguageDetection/resources/pt-PT/pt-PT.php +++ b/Localization/LanguageDetection/resources/pt-PT/pt-PT.php @@ -1,8 +1,7 @@ - [ + 'pt-PT' => [ 0 => 'e', 1 => 'a', 2 => 'o', diff --git a/Localization/LanguageDetection/resources/ro/ro.php b/Localization/LanguageDetection/resources/ro/ro.php index d89d599a9..5b54547bb 100755 --- a/Localization/LanguageDetection/resources/ro/ro.php +++ b/Localization/LanguageDetection/resources/ro/ro.php @@ -1,8 +1,7 @@ - [ + 'ro' => [ 0 => 'e', 1 => 'i', 2 => 'a', diff --git a/Localization/LanguageDetection/resources/ru/ru.php b/Localization/LanguageDetection/resources/ru/ru.php index c6a52b0ad..637f902e9 100755 --- a/Localization/LanguageDetection/resources/ru/ru.php +++ b/Localization/LanguageDetection/resources/ru/ru.php @@ -1,8 +1,7 @@ - [ + 'ru' => [ 0 => 'о', 1 => 'е', 2 => 'и', diff --git a/Localization/LanguageDetection/resources/sa/sa.php b/Localization/LanguageDetection/resources/sa/sa.php index 8f1331ab9..af5d7ab0f 100755 --- a/Localization/LanguageDetection/resources/sa/sa.php +++ b/Localization/LanguageDetection/resources/sa/sa.php @@ -1,8 +1,7 @@ - [ + 'sa' => [ 0 => '_य_', 1 => '_त_', 2 => 'त_', diff --git a/Localization/LanguageDetection/resources/sk/sk.php b/Localization/LanguageDetection/resources/sk/sk.php index e70aab01a..f3df5c008 100755 --- a/Localization/LanguageDetection/resources/sk/sk.php +++ b/Localization/LanguageDetection/resources/sk/sk.php @@ -1,8 +1,7 @@ - [ + 'sk' => [ 0 => 'o', 1 => 'a', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/sl/sl.php b/Localization/LanguageDetection/resources/sl/sl.php index 616b69025..c4ab9c7a2 100755 --- a/Localization/LanguageDetection/resources/sl/sl.php +++ b/Localization/LanguageDetection/resources/sl/sl.php @@ -1,8 +1,7 @@ - [ + 'sl' => [ 0 => 'o', 1 => 'a', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/so/so.php b/Localization/LanguageDetection/resources/so/so.php index eaff5602a..46c11594c 100755 --- a/Localization/LanguageDetection/resources/so/so.php +++ b/Localization/LanguageDetection/resources/so/so.php @@ -1,8 +1,7 @@ - [ + 'so' => [ 0 => 'a', 1 => 'i', 2 => 'o', diff --git a/Localization/LanguageDetection/resources/sq/sq.php b/Localization/LanguageDetection/resources/sq/sq.php index 4d784e48a..13a417b88 100755 --- a/Localization/LanguageDetection/resources/sq/sq.php +++ b/Localization/LanguageDetection/resources/sq/sq.php @@ -1,8 +1,7 @@ - [ + 'sq' => [ 0 => 'e', 1 => 't', 2 => 'ë', diff --git a/Localization/LanguageDetection/resources/sr-Cyrl/sr-Cyrl.php b/Localization/LanguageDetection/resources/sr-Cyrl/sr-Cyrl.php index 76d741df6..98ef3b691 100755 --- a/Localization/LanguageDetection/resources/sr-Cyrl/sr-Cyrl.php +++ b/Localization/LanguageDetection/resources/sr-Cyrl/sr-Cyrl.php @@ -1,8 +1,7 @@ - [ + 'sr-Cyrl' => [ 0 => 'а', 1 => 'о', 2 => 'и', diff --git a/Localization/LanguageDetection/resources/sr-Latn/sr-Latn.php b/Localization/LanguageDetection/resources/sr-Latn/sr-Latn.php index a51d62e8f..7e4640866 100755 --- a/Localization/LanguageDetection/resources/sr-Latn/sr-Latn.php +++ b/Localization/LanguageDetection/resources/sr-Latn/sr-Latn.php @@ -1,8 +1,7 @@ - [ + 'sr-Latn' => [ 0 => 'a', 1 => 'o', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/ss/ss.php b/Localization/LanguageDetection/resources/ss/ss.php index 469c5af3b..878e157e9 100755 --- a/Localization/LanguageDetection/resources/ss/ss.php +++ b/Localization/LanguageDetection/resources/ss/ss.php @@ -1,8 +1,7 @@ - [ + 'ss' => [ 0 => 'e', 1 => 'a', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/sv/sv.php b/Localization/LanguageDetection/resources/sv/sv.php index ff0694d2a..7d23fd18c 100755 --- a/Localization/LanguageDetection/resources/sv/sv.php +++ b/Localization/LanguageDetection/resources/sv/sv.php @@ -1,8 +1,7 @@ - [ + 'sv' => [ 0 => 'a', 1 => 'e', 2 => 't', diff --git a/Localization/LanguageDetection/resources/sw/sw.php b/Localization/LanguageDetection/resources/sw/sw.php index ec3661596..a9aa94947 100755 --- a/Localization/LanguageDetection/resources/sw/sw.php +++ b/Localization/LanguageDetection/resources/sw/sw.php @@ -1,8 +1,7 @@ - [ + 'sw' => [ 0 => 'a', 1 => 'a_', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/ta/ta.php b/Localization/LanguageDetection/resources/ta/ta.php index abb719a8c..f0a5bfd48 100755 --- a/Localization/LanguageDetection/resources/ta/ta.php +++ b/Localization/LanguageDetection/resources/ta/ta.php @@ -1,8 +1,7 @@ - [ + 'ta' => [ 0 => '_க_', 1 => '_ம_', 2 => '_க', diff --git a/Localization/LanguageDetection/resources/th/th.php b/Localization/LanguageDetection/resources/th/th.php index dcd8aa250..03188f4d6 100755 --- a/Localization/LanguageDetection/resources/th/th.php +++ b/Localization/LanguageDetection/resources/th/th.php @@ -1,8 +1,7 @@ - [ + 'th' => [ 0 => 'า', 1 => 'ร', 2 => 'น', diff --git a/Localization/LanguageDetection/resources/tl/tl.php b/Localization/LanguageDetection/resources/tl/tl.php index 8aaf5fa4f..ce055e56b 100755 --- a/Localization/LanguageDetection/resources/tl/tl.php +++ b/Localization/LanguageDetection/resources/tl/tl.php @@ -1,8 +1,7 @@ - [ + 'tl' => [ 0 => 'a', 1 => 'n', 2 => 'g', diff --git a/Localization/LanguageDetection/resources/to/to.php b/Localization/LanguageDetection/resources/to/to.php index bd206c4d0..75ac601ea 100755 --- a/Localization/LanguageDetection/resources/to/to.php +++ b/Localization/LanguageDetection/resources/to/to.php @@ -1,8 +1,7 @@ - [ + 'to' => [ 0 => 'a', 1 => 'u', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/tr/tr.php b/Localization/LanguageDetection/resources/tr/tr.php index 8b5d50a1a..d279caccc 100755 --- a/Localization/LanguageDetection/resources/tr/tr.php +++ b/Localization/LanguageDetection/resources/tr/tr.php @@ -1,8 +1,7 @@ - [ + 'tr' => [ 0 => 'e', 1 => 'a', 2 => 'i', diff --git a/Localization/LanguageDetection/resources/tt/tt.php b/Localization/LanguageDetection/resources/tt/tt.php index 764d4a5e5..2be63ba06 100755 --- a/Localization/LanguageDetection/resources/tt/tt.php +++ b/Localization/LanguageDetection/resources/tt/tt.php @@ -1,8 +1,7 @@ - [ + 'tt' => [ 0 => 'а', 1 => 'е', 2 => 'ә', diff --git a/Localization/LanguageDetection/resources/ty/ty.php b/Localization/LanguageDetection/resources/ty/ty.php index 975ea2036..4bd07c22e 100755 --- a/Localization/LanguageDetection/resources/ty/ty.php +++ b/Localization/LanguageDetection/resources/ty/ty.php @@ -1,8 +1,7 @@ - [ + 'ty' => [ 0 => 'a', 1 => '\'', 2 => 't', diff --git a/Localization/LanguageDetection/resources/ug-Arab/ug-Arab.php b/Localization/LanguageDetection/resources/ug-Arab/ug-Arab.php index 46749b894..768c4cf4c 100755 --- a/Localization/LanguageDetection/resources/ug-Arab/ug-Arab.php +++ b/Localization/LanguageDetection/resources/ug-Arab/ug-Arab.php @@ -1,8 +1,7 @@ - [ + 'ug-Arab' => [ 0 => 'ى', 1 => 'ا', 2 => 'ە', diff --git a/Localization/LanguageDetection/resources/ug-Latn/ug-Latn.php b/Localization/LanguageDetection/resources/ug-Latn/ug-Latn.php index f2de960d9..5c9bae4f5 100755 --- a/Localization/LanguageDetection/resources/ug-Latn/ug-Latn.php +++ b/Localization/LanguageDetection/resources/ug-Latn/ug-Latn.php @@ -1,8 +1,7 @@ - [ + 'ug-Latn' => [ 0 => 'i', 1 => 'a', 2 => 'e', diff --git a/Localization/LanguageDetection/resources/uk/uk.php b/Localization/LanguageDetection/resources/uk/uk.php index 7318f7cd1..ed6c461f2 100755 --- a/Localization/LanguageDetection/resources/uk/uk.php +++ b/Localization/LanguageDetection/resources/uk/uk.php @@ -1,8 +1,7 @@ - [ + 'uk' => [ 0 => 'о', 1 => 'а', 2 => 'н', diff --git a/Localization/LanguageDetection/resources/ur/ur.php b/Localization/LanguageDetection/resources/ur/ur.php index 07671c152..6caf25449 100755 --- a/Localization/LanguageDetection/resources/ur/ur.php +++ b/Localization/LanguageDetection/resources/ur/ur.php @@ -1,8 +1,7 @@ - [ + 'ur' => [ 0 => 'ا', 1 => 'ی', 2 => 'و', diff --git a/Localization/LanguageDetection/resources/uz/uz.php b/Localization/LanguageDetection/resources/uz/uz.php index 6188fe539..b257c9537 100755 --- a/Localization/LanguageDetection/resources/uz/uz.php +++ b/Localization/LanguageDetection/resources/uz/uz.php @@ -1,8 +1,7 @@ - [ + 'uz' => [ 0 => 'а', 1 => 'и', 2 => 'л', diff --git a/Localization/LanguageDetection/resources/ve/ve.php b/Localization/LanguageDetection/resources/ve/ve.php index f95a3d866..3715b33eb 100755 --- a/Localization/LanguageDetection/resources/ve/ve.php +++ b/Localization/LanguageDetection/resources/ve/ve.php @@ -1,8 +1,7 @@ - [ + 've' => [ 0 => 'a', 1 => 'a_', 2 => 'h', diff --git a/Localization/LanguageDetection/resources/vi/vi.php b/Localization/LanguageDetection/resources/vi/vi.php index bd20de2c4..7de503775 100755 --- a/Localization/LanguageDetection/resources/vi/vi.php +++ b/Localization/LanguageDetection/resources/vi/vi.php @@ -1,8 +1,7 @@ - [ + 'vi' => [ 0 => 'n', 1 => '_n', 2 => '_c', diff --git a/Localization/LanguageDetection/resources/wa/wa.php b/Localization/LanguageDetection/resources/wa/wa.php index 09fb454d0..ea6f8f24b 100755 --- a/Localization/LanguageDetection/resources/wa/wa.php +++ b/Localization/LanguageDetection/resources/wa/wa.php @@ -1,8 +1,7 @@ - [ + 'wa' => [ 0 => 's', 1 => 'e', 2 => 't', diff --git a/Localization/LanguageDetection/resources/wo/wo.php b/Localization/LanguageDetection/resources/wo/wo.php index 0b96bbc8d..e9a46d046 100755 --- a/Localization/LanguageDetection/resources/wo/wo.php +++ b/Localization/LanguageDetection/resources/wo/wo.php @@ -1,8 +1,7 @@ - [ + 'wo' => [ 0 => 'a', 1 => 'e', 2 => 'u', diff --git a/Localization/LanguageDetection/resources/xh/xh.php b/Localization/LanguageDetection/resources/xh/xh.php index 93f3a9f18..81a5f9b8e 100755 --- a/Localization/LanguageDetection/resources/xh/xh.php +++ b/Localization/LanguageDetection/resources/xh/xh.php @@ -1,8 +1,7 @@ - [ + 'xh' => [ 0 => 'e', 1 => 'a', 2 => 'n', diff --git a/Localization/LanguageDetection/resources/yo/yo.php b/Localization/LanguageDetection/resources/yo/yo.php index fffa29b75..897077110 100755 --- a/Localization/LanguageDetection/resources/yo/yo.php +++ b/Localization/LanguageDetection/resources/yo/yo.php @@ -1,8 +1,7 @@ - [ + 'yo' => [ 0 => 'n', 1 => 'í', 2 => 'à', diff --git a/Localization/LanguageDetection/resources/zh-Hans/zh-Hans.php b/Localization/LanguageDetection/resources/zh-Hans/zh-Hans.php index 0bf8287f3..980d59ced 100755 --- a/Localization/LanguageDetection/resources/zh-Hans/zh-Hans.php +++ b/Localization/LanguageDetection/resources/zh-Hans/zh-Hans.php @@ -1,8 +1,7 @@ - [ + 'zh-Hans' => [ 0 => '的', 1 => '人', 2 => '和', diff --git a/Localization/LanguageDetection/resources/zh-Hant/zh-Hant.php b/Localization/LanguageDetection/resources/zh-Hant/zh-Hant.php index 9591cdf35..1a2294aa8 100755 --- a/Localization/LanguageDetection/resources/zh-Hant/zh-Hant.php +++ b/Localization/LanguageDetection/resources/zh-Hant/zh-Hant.php @@ -1,8 +1,7 @@ - [ + 'zh-Hant' => [ 0 => '的', 1 => '人', 2 => '和', diff --git a/Localization/Localization.php b/Localization/Localization.php index 1f540ac84..eb1b0a528 100755 --- a/Localization/Localization.php +++ b/Localization/Localization.php @@ -216,8 +216,8 @@ class Localization implements \JsonSerializable $l11n = new self(); $l11n->setCountry($json['country']); $l11n->setTimezone($json['timezone'] ?? 'America/New_York'); - $l11n->setLanguage($json['language']); - $l11n->setCurrency(\is_string($json['currency']) ? $json['currency'] : ($json['currency']['code'] ?? ISO4217Enum::_USD)); + $l11n->language = $json['language']; + $l11n->currency = \is_string($json['currency']) ? $json['currency'] : ($json['currency']['code'] ?? ISO4217Enum::_USD); $l11n->setCurrencyFormat(isset($json['currencyformat']) && \is_string($json['currencyformat']) ? $json['currencyformat'] : ($json['currency']['format'] ?? '1')); $l11n->setDecimal($json['decimal']); $l11n->setThousands($json['thousand']); @@ -322,7 +322,7 @@ class Localization implements \JsonSerializable /** * Load localization from locale * - * @param array{language?:string, country?:string, currency?:array{code?:string}, thousand?:string, angle?:string, temperatur?:string, weight?:array, speed?:array, length?:array, area?:array, volume?:array, precision?:array, timezone?:string, datetime?:array} $locale Locale data + * @param array{language?:string, country?:string, currency?:array{code?:string}, thousand?:string, angle?:string, temperature?:string, weight?:array, speed?:array, length?:array, area?:array, volume?:array, precision?:array, timezone?:string, datetime?:array} $locale Locale data * * @return void * @@ -330,9 +330,9 @@ class Localization implements \JsonSerializable */ public function importLocale(array $locale) : void { - $this->setLanguage($locale['language'] ?? 'en'); + $this->language = $locale['language'] ?? 'en'; $this->setCountry($locale['country'] ?? 'US'); - $this->setCurrency($locale['currency']['code'] ?? ISO4217Enum::_USD); + $this->currency = $locale['currency']['code'] ?? ISO4217Enum::_USD; $this->setThousands($locale['thousand'] ?? ','); $this->setDecimal($locale['decimal'] ?? '.'); $this->setAngle($locale['angle'] ?? AngleType::DEGREE); @@ -350,7 +350,7 @@ class Localization implements \JsonSerializable /** * Set country name * - * @param string $country Contry name + * @param string $country Country name * * @return void * diff --git a/Localization/Money.php b/Localization/Money.php index 17f995ced..d70c19f77 100755 --- a/Localization/Money.php +++ b/Localization/Money.php @@ -73,7 +73,7 @@ final class Money extends FloatInt * * @since 1.0.0 */ - public function getCurrency(?int $decimals = 2, int $position = null, string $symbol = null) : string + public function getCurrency(?int $decimals = 2, ?int $position = null, ?string $symbol = null) : string { return (($position ?? $this->position) === 0 && !empty($symbol ?? $this->symbol) ? ($symbol ?? $this->symbol) . ' ' : '') . $this->getAmount($decimals) diff --git a/Localization/RegionEnum.php b/Localization/RegionEnum.php index 24b6e4001..65b1133dc 100644 --- a/Localization/RegionEnum.php +++ b/Localization/RegionEnum.php @@ -32,6 +32,16 @@ class RegionEnum extends Enum public const EURO = 'Euro'; + public const OECD = 'OECD'; + + public const NATO = 'NATO'; + + public const SCHENGEN = 'Schengen'; + + public const P5 = 'P5'; + + public const G8 = 'G8'; + public const NORTH_EUROPE = 'North-Europe'; public const SOUTH_EUROPE = 'South-Europe'; @@ -89,4 +99,10 @@ class RegionEnum extends Enum public const ANTARCTICA = 'Antarctica'; public const CONTINENTS = 'Continents'; + + public const DOMESTIC = 'Domestic'; + + public const EXPORT = 'Export'; + + public const DACH = 'DACH'; } diff --git a/Localization/Subregion/ISO3166TwoATCharEnum.php b/Localization/Subregion/ISO3166TwoATCharEnum.php new file mode 100644 index 000000000..7dd9201b5 --- /dev/null +++ b/Localization/Subregion/ISO3166TwoATCharEnum.php @@ -0,0 +1,46 @@ + + * + * @since 1.0.0 + */ + public static function cross3(array $vector1, array $vector2) : array + { + return [ + $vector1[1] * $vector2[2] - $vector1[2] * $vector2[1], + $vector1[2] * $vector2[0] - $vector1[0] * $vector2[2], + $vector1[0] * $vector2[1] - $vector1[1] * $vector2[0], + ]; + } +} diff --git a/Math/Functions/Beta.php b/Math/Functions/Beta.php index a668b95ba..23c05fd4d 100755 --- a/Math/Functions/Beta.php +++ b/Math/Functions/Beta.php @@ -123,8 +123,8 @@ final class Beta } $frac *= $h * $c; - $d = -($p + $m) * ($pqSum + $m) * $x / (($p + $m2) * ($pPlus + $m2)); - $h = 1.0 + $d * $h; + $d = -($p + $m) * ($pqSum + $m) * $x / (($p + $m2) * ($pPlus + $m2)); + $h = 1.0 + $d * $h; if (\abs($h) < 1.18e-37) { $h = 1.18e-37; } diff --git a/Math/Functions/Functions.php b/Math/Functions/Functions.php index e2eed8673..153a9e3a7 100755 --- a/Math/Functions/Functions.php +++ b/Math/Functions/Functions.php @@ -196,6 +196,11 @@ final class Functions return $a % $b; } + public static function modFloat(float $a, float $b) : float + { + return $a - ((int) ($a / $b)) * $b; + } + /** * Check if value is odd. * @@ -348,7 +353,7 @@ final class Functions for ($j = 0; $j < 2; ++$j) { $err = self::getErfc($x) - $pp; - $x += $err / (1.12837916709551257 * \exp(-($x * $x)) - $x * $err); + $x += $err / (1.12837916709551257 * \exp(-($x * $x)) - $x * $err); } return ($p < 1.0? $x : -$x); diff --git a/Math/Functions/Gamma.php b/Math/Functions/Gamma.php index 913558ab1..9858af4d2 100755 --- a/Math/Functions/Gamma.php +++ b/Math/Functions/Gamma.php @@ -114,7 +114,7 @@ final class Gamma $c = [\sqrt(2.0 * \M_PI)]; for ($k = 1; $k < 12; ++$k) { - $c[$k] = \exp(12 - $k) * \pow(12 - $k, $k - 0.5) / $k1_fact; + $c[$k] = \exp(12 - $k) * \pow(12 - $k, $k - 0.5) / $k1_fact; $k1_fact *= -$k; } @@ -283,8 +283,8 @@ final class Gamma for ($i = 1; $i < 150 && \abs($del - 1.0) > 2.22e-16; ++$i) { $an = - $i * ($i - $a); $b += 2.0; - $d = $an * $d + $b; - $c = $b + $an / $c; + $d = $an * $d + $b; + $c = $b + $an / $c; if (\abs($c) < 1.18e-37) { $c = 1.18e-37; @@ -296,7 +296,7 @@ final class Gamma $d = 1.0 / $d; $del = $d * $c; - $h *= $del; + $h *= $del; } return \exp(-$x + $a * \log($x) - self::logGamma($a)) * $h; diff --git a/Math/Geometry/ConvexHull/GrahamScan.php b/Math/Geometry/ConvexHull/GrahamScan.php index 440d5cc60..24dbab119 100644 --- a/Math/Geometry/ConvexHull/GrahamScan.php +++ b/Math/Geometry/ConvexHull/GrahamScan.php @@ -45,17 +45,17 @@ final class GrahamScan */ public static function createConvexHull(array $points) : array { - $count = \count($points); - - if ($count < 3) { - return []; + if (($n = \count($points)) < 3) { + return $points; } $min = 1; $points = \array_merge([null], $points); - for ($i = 2; $i < $count; ++$i) { - if ($points[$i]['y'] < $points[$min]['y'] || ($points[$i]['y'] == $points[$min]['y'] && $points[$i]['x'] < $points[$min]['x'])) { + for ($i = 2; $i < $n; ++$i) { + if ($points[$i]['y'] < $points[$min]['y'] + || ($points[$i]['y'] === $points[$min]['y'] && $points[$i]['x'] < $points[$min]['x']) + ) { $min = $i; } } @@ -67,22 +67,21 @@ final class GrahamScan $c = $points[1]; /** @var array $subpoints */ - $subpoints = \array_slice($points, 2, $count); + $subpoints = \array_slice($points, 2, $n); \usort($subpoints, function (array $a, array $b) use ($c) : int { - // @todo: Might be wrong order of comparison - return \atan2($a['y'] - $c['y'], $a['x'] - $c['x']) <=> \atan2( $b['y'] - $c['y'], $b['x'] - $c['x']); + return \atan2($a['y'] - $c['y'], $a['x'] - $c['x']) <=> \atan2($b['y'] - $c['y'], $b['x'] - $c['x']); }); /** @var array $points */ $points = \array_merge([$points[0], $points[1]], $subpoints); - $points[0] = $points[$count]; + $points[0] = $points[$n]; $size = 1; - for ($i = 2; $i <= $count; ++$i) { + for ($i = 2; $i <= $n; ++$i) { while (self::ccw($points[$size - 1], $points[$size], $points[$i]) <= 0) { if ($size > 1) { --$size; - } elseif ($i === $count) { + } elseif ($i === $n) { break; } else { ++$i; @@ -106,9 +105,9 @@ final class GrahamScan /** * Counterclockwise rotation * - * @param array $a Vector - * @param array $b Vector - * @param array $c Vector + * @param array{x:int|float, y:int|float} $a Vector + * @param array{x:int|float, y:int|float} $b Vector + * @param array{x:int|float, y:int|float} $c Vector * * @return int|float * diff --git a/Math/Geometry/ConvexHull/MonotoneChain.php b/Math/Geometry/ConvexHull/MonotoneChain.php index 9599031af..2cc2c4a1d 100755 --- a/Math/Geometry/ConvexHull/MonotoneChain.php +++ b/Math/Geometry/ConvexHull/MonotoneChain.php @@ -45,7 +45,7 @@ final class MonotoneChain */ public static function createConvexHull(array $points) : array { - if (($n = \count($points)) < 2) { + if (($n = \count($points)) < 3) { return $points; } diff --git a/Math/Geometry/Shape/D2/Circle.php b/Math/Geometry/Shape/D2/Circle.php index d3d67ec38..1b77b76a1 100755 --- a/Math/Geometry/Shape/D2/Circle.php +++ b/Math/Geometry/Shape/D2/Circle.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D2; */ final class Circle implements D2ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Area * diff --git a/Math/Geometry/Shape/D2/Ellipse.php b/Math/Geometry/Shape/D2/Ellipse.php index 8bbc8eb9f..5e13daf80 100755 --- a/Math/Geometry/Shape/D2/Ellipse.php +++ b/Math/Geometry/Shape/D2/Ellipse.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D2; */ final class Ellipse implements D2ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Area * diff --git a/Math/Geometry/Shape/D2/Polygon.php b/Math/Geometry/Shape/D2/Polygon.php index 5fbfdeaa3..74a4065c0 100755 --- a/Math/Geometry/Shape/D2/Polygon.php +++ b/Math/Geometry/Shape/D2/Polygon.php @@ -248,12 +248,12 @@ final class Polygon implements D2ShapeInterface $count = \count($this->coord); for ($i = 0; $i < $count - 1; ++$i) { - $mult = ($this->coord[$i]['x'] * $this->coord[$i + 1]['y'] - $this->coord[$i + 1]['x'] * $this->coord[$i]['y']); + $mult = ($this->coord[$i]['x'] * $this->coord[$i + 1]['y'] - $this->coord[$i + 1]['x'] * $this->coord[$i]['y']); $barycenter['x'] += ($this->coord[$i]['x'] + $this->coord[$i + 1]['x']) * $mult; $barycenter['y'] += ($this->coord[$i]['y'] + $this->coord[$i + 1]['y']) * $mult; } - $mult = ($this->coord[$count - 1]['x'] * $this->coord[0]['y'] - $this->coord[0]['x'] * $this->coord[$count - 1]['y']); + $mult = ($this->coord[$count - 1]['x'] * $this->coord[0]['y'] - $this->coord[0]['x'] * $this->coord[$count - 1]['y']); $barycenter['x'] += ($this->coord[$count - 1]['x'] + $this->coord[0]['x']) * $mult; $barycenter['y'] += ($this->coord[$count - 1]['y'] + $this->coord[0]['y']) * $mult; diff --git a/Math/Geometry/Shape/D2/Quadrilateral.php b/Math/Geometry/Shape/D2/Quadrilateral.php index a7bd4f0cb..a0f8ce40c 100755 --- a/Math/Geometry/Shape/D2/Quadrilateral.php +++ b/Math/Geometry/Shape/D2/Quadrilateral.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D2; */ final class Quadrilateral implements D2ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Calculate the surface area from the length of all sides and the angle between a and b * diff --git a/Math/Geometry/Shape/D2/Rectangle.php b/Math/Geometry/Shape/D2/Rectangle.php index 91ea83eb0..b51dd3dbc 100755 --- a/Math/Geometry/Shape/D2/Rectangle.php +++ b/Math/Geometry/Shape/D2/Rectangle.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D2; */ final class Rectangle implements D2ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Area * diff --git a/Math/Geometry/Shape/D2/Trapezoid.php b/Math/Geometry/Shape/D2/Trapezoid.php index 7edb13496..1ec6d866b 100755 --- a/Math/Geometry/Shape/D2/Trapezoid.php +++ b/Math/Geometry/Shape/D2/Trapezoid.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D2; */ final class Trapezoid implements D2ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Area * diff --git a/Math/Geometry/Shape/D2/Triangle.php b/Math/Geometry/Shape/D2/Triangle.php index 84f1efdf5..112934944 100755 --- a/Math/Geometry/Shape/D2/Triangle.php +++ b/Math/Geometry/Shape/D2/Triangle.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D2; */ final class Triangle implements D2ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Area * diff --git a/Math/Geometry/Shape/D3/Cone.php b/Math/Geometry/Shape/D3/Cone.php index 5c46b079d..935bd1ac8 100755 --- a/Math/Geometry/Shape/D3/Cone.php +++ b/Math/Geometry/Shape/D3/Cone.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D3; */ final class Cone implements D3ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Volume * diff --git a/Math/Geometry/Shape/D3/Cuboid.php b/Math/Geometry/Shape/D3/Cuboid.php index 7767ef45c..2361d9195 100755 --- a/Math/Geometry/Shape/D3/Cuboid.php +++ b/Math/Geometry/Shape/D3/Cuboid.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D3; */ final class Cuboid implements D3ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Volume * diff --git a/Math/Geometry/Shape/D3/Cylinder.php b/Math/Geometry/Shape/D3/Cylinder.php index bef7d0812..4ea37ef6e 100755 --- a/Math/Geometry/Shape/D3/Cylinder.php +++ b/Math/Geometry/Shape/D3/Cylinder.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D3; */ final class Cylinder implements D3ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Volume * diff --git a/Math/Geometry/Shape/D3/Prism.php b/Math/Geometry/Shape/D3/Prism.php index 5b0319e06..c90864593 100755 --- a/Math/Geometry/Shape/D3/Prism.php +++ b/Math/Geometry/Shape/D3/Prism.php @@ -26,6 +26,16 @@ use phpOMS\Math\Geometry\Shape\D2\Polygon; */ final class Prism implements D3ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get volume of regular polygon prism by side length * diff --git a/Math/Geometry/Shape/D3/RectangularPyramid.php b/Math/Geometry/Shape/D3/RectangularPyramid.php index 0b71157cf..3016d50a9 100755 --- a/Math/Geometry/Shape/D3/RectangularPyramid.php +++ b/Math/Geometry/Shape/D3/RectangularPyramid.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D3; */ final class RectangularPyramid implements D3ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Volume * diff --git a/Math/Geometry/Shape/D3/Tetrahedron.php b/Math/Geometry/Shape/D3/Tetrahedron.php index 80632e629..a546fa268 100755 --- a/Math/Geometry/Shape/D3/Tetrahedron.php +++ b/Math/Geometry/Shape/D3/Tetrahedron.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Geometry\Shape\D3; */ final class Tetrahedron implements D3ShapeInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Volume * diff --git a/Math/Matrix/EigenvalueDecomposition.php b/Math/Matrix/EigenvalueDecomposition.php index c648e21e7..c12b9187e 100755 --- a/Math/Matrix/EigenvalueDecomposition.php +++ b/Math/Matrix/EigenvalueDecomposition.php @@ -182,14 +182,14 @@ final class EigenvalueDecomposition } else { for ($k = 0; $k < $i; ++$k) { $this->D[$k] /= $scale; - $h += $this->D[$k] * $this->D[$k]; + $h += $this->D[$k] * $this->D[$k]; } $f = $this->D[$i - 1]; $g = $f > 0 ? -\sqrt($h) : \sqrt($h); - $this->E[$i] = $scale * $g; - $h -= $f * $g; + $this->E[$i] = $scale * $g; + $h -= $f * $g; $this->D[$i - 1] = $f - $g; for ($j = 0; $j < $i; ++$j) { @@ -212,7 +212,7 @@ final class EigenvalueDecomposition $f = 0.0; for ($j = 0; $j < $i; ++$j) { $this->E[$j] /= $h; - $f += $this->E[$j] * $this->D[$j]; + $f += $this->E[$j] * $this->D[$j]; } $hh = $f / ($h + $h); @@ -321,7 +321,7 @@ final class EigenvalueDecomposition $this->D[$i] -= $h; } - $f += $h; + $f += $h; $p = $this->D[$m]; $c = 1.0; $c2 = 1.0; @@ -357,7 +357,7 @@ final class EigenvalueDecomposition } $this->D[$l] += $f; - $this->E[$l] = 0.0; + $this->E[$l] = 0.0; } for ($i = 0; $i < $this->m - 1; ++$i) { @@ -407,10 +407,10 @@ final class EigenvalueDecomposition $h = 0.0; for ($i = $high; $i >= $m; --$i) { $this->ort[$i] = $this->H[$i][$m - 1] / $scale; - $h += $this->ort[$i] * $this->ort[$i]; + $h += $this->ort[$i] * $this->ort[$i]; } - $g = $this->ort[$m] > 0 ? -\sqrt($h) : \sqrt($h); + $g = $this->ort[$m] > 0 ? -\sqrt($h) : \sqrt($h); $h -= $this->ort[$m] * $g; $this->ort[$m] -= $g; @@ -438,7 +438,7 @@ final class EigenvalueDecomposition } } - $this->ort[$m] *= $scale; + $this->ort[$m] *= $scale; $this->H[$m][$m - 1] = $scale * $g; } } @@ -553,9 +553,9 @@ final class EigenvalueDecomposition if ($l === $n) { $this->H[$n][$n] += $exshift; - $this->D[$n] = $this->H[$n][$n]; - $this->E[$n] = 0.0; - $iter = 0; + $this->D[$n] = $this->H[$n][$n]; + $this->E[$n] = 0.0; + $iter = 0; --$n; } elseif ($l === $n - 1) { @@ -576,11 +576,11 @@ final class EigenvalueDecomposition $this->E[$n - 1] = 0.0; $this->E[$n] = 0.0; - $x = $this->H[$n][$n - 1]; - $s = \abs($x) + \abs($z); - $p = $x / $s; - $q = $z / $s; - $r = \sqrt($p * $p + $q * $q); + $x = $this->H[$n][$n - 1]; + $s = \abs($x) + \abs($z); + $p = $x / $s; + $q = $z / $s; + $r = \sqrt($p * $p + $q * $q); $p /= $r; $q /= $r; @@ -608,7 +608,7 @@ final class EigenvalueDecomposition $this->E[$n] = -$z; } - $n -= 2; + $n -= 2; $iter = 0; } else { $x = $this->H[$n][$n]; @@ -645,7 +645,7 @@ final class EigenvalueDecomposition } $exshift += $s; - $x = $y = $w = 0.964; + $x = $y = $w = 0.964; } } @@ -653,13 +653,13 @@ final class EigenvalueDecomposition $m = $n - 2; while ($m >= $l) { - $z = $this->H[$m][$m]; - $r = $x - $z; - $s = $y - $z; - $p = ($r * $s - $w) / $this->H[$m + 1][$m] + $this->H[$m][$m + 1]; - $q = $this->H[$m + 1][$m + 1] - $z - $r - $s; - $r = $this->H[$m + 2][$m + 1]; - $s = \abs($p) + \abs($q) + \abs($r); + $z = $this->H[$m][$m]; + $r = $x - $z; + $s = $y - $z; + $p = ($r * $s - $w) / $this->H[$m + 1][$m] + $this->H[$m][$m + 1]; + $q = $this->H[$m + 1][$m + 1] - $z - $r - $s; + $r = $this->H[$m + 2][$m + 1]; + $s = \abs($p) + \abs($q) + \abs($r); $p /= $s; $q /= $s; $r /= $s; @@ -712,16 +712,16 @@ final class EigenvalueDecomposition } $p += $s; - $x = $p / $s; - $y = $q / $s; - $z = $r / $s; + $x = $p / $s; + $y = $q / $s; + $z = $r / $s; $q /= $p; $r /= $p; for ($j = $k; $j < $nn; ++$j) { $p = $this->H[$k][$j] + $q * $this->H[$k + 1][$j]; if ($notlast) { - $p += $r * $this->H[$k + 2][$j]; + $p += $r * $this->H[$k + 2][$j]; $this->H[$k + 2][$j] -= $p * $z; } @@ -734,7 +734,7 @@ final class EigenvalueDecomposition $p = $x * $this->H[$i][$k] + $y * $this->H[$i][$k + 1]; if ($notlast) { - $p += $z * $this->H[$i][$k + 2]; + $p += $z * $this->H[$i][$k + 2]; $this->H[$i][$k + 2] -= $p * $r; } @@ -746,7 +746,7 @@ final class EigenvalueDecomposition $p = $x * $this->V[$i][$k] + $y * $this->V[$i][$k + 1]; if ($notlast) { - $p += $z * $this->V[$i][$k + 2]; + $p += $z * $this->V[$i][$k + 2]; $this->V[$i][$k + 2] -= $p * $r; } $this->V[$i][$k] -= $p; diff --git a/Math/Matrix/Exception/InvalidDimensionException.php b/Math/Matrix/Exception/InvalidDimensionException.php index e423c2a66..69006f043 100755 --- a/Math/Matrix/Exception/InvalidDimensionException.php +++ b/Math/Matrix/Exception/InvalidDimensionException.php @@ -33,7 +33,7 @@ final class InvalidDimensionException extends \UnexpectedValueException * * @since 1.0.0 */ - public function __construct($message, int $code = 0, \Exception $previous = null) + public function __construct($message, int $code = 0, ?\Exception $previous = null) { parent::__construct('Dimension "' . $message . '" is not valid.', $code, $previous); } diff --git a/Math/Matrix/LUDecomposition.php b/Math/Matrix/LUDecomposition.php index 568b7a4e6..c336d3e90 100755 --- a/Math/Matrix/LUDecomposition.php +++ b/Math/Matrix/LUDecomposition.php @@ -118,9 +118,9 @@ final class LUDecomposition $this->LU[$j][$k] = $t; } - $k = $this->piv[$p]; - $this->piv[$p] = $this->piv[$j]; - $this->piv[$j] = $k; + $k = $this->piv[$p]; + $this->piv[$p] = $this->piv[$j]; + $this->piv[$j] = $k; $this->pivSign *= -1; } diff --git a/Math/Matrix/Matrix.php b/Math/Matrix/Matrix.php index 116ad38e1..a19d82d6f 100755 --- a/Math/Matrix/Matrix.php +++ b/Math/Matrix/Matrix.php @@ -82,9 +82,7 @@ class Matrix implements \ArrayAccess, \Iterator $this->n = $n; $this->m = $m; - for ($i = 0; $i < $m; ++$i) { - $this->matrix[$i] = \array_fill(0, $n, 0); - } + $this->matrix = \array_fill(0, $m, \array_fill(0, $n, 0)); } /** @@ -162,18 +160,6 @@ class Matrix implements \ArrayAccess, \Iterator return $matrix; } - /** - * Get matrix array. - * - * @return array> - * - * @since 1.0.0 - */ - public function getMatrix() : array - { - return $this->matrix; - } - /** * Get sub matrix array. * @@ -297,6 +283,25 @@ class Matrix implements \ArrayAccess, \Iterator return $this->matrix; } + /** + * Get matrix as 1D array. + * + * @return array + * + * @since 1.0.0 + */ + public function toVectorArray() : array + { + $result = []; + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $result[] = $this->matrix[$i][$j]; + } + } + + return $result; + } + /** * Is symmetric. * @@ -309,7 +314,7 @@ class Matrix implements \ArrayAccess, \Iterator $isSymmetric = true; for ($j = 0; ($j < $this->m) & $isSymmetric; ++$j) { for ($i = 0; ($i < $this->n) & $isSymmetric; ++$i) { - $isSymmetric = ($this->matrix[$i][$j] === $this->matrix[$j][$i]); + $isSymmetric = \abs($this->matrix[$i][$j] - $this->matrix[$j][$i]) < self::EPSILON; } } @@ -432,7 +437,7 @@ class Matrix implements \ArrayAccess, \Iterator throw new InvalidDimensionException($matrix->getM() . 'x' . $matrix->getN()); } - $matrixArr = $matrix->getMatrix(); + $matrixArr = $matrix->toArray(); $newMatrixArr = $this->matrix; foreach ($newMatrixArr as $i => $vector) { @@ -485,7 +490,7 @@ class Matrix implements \ArrayAccess, \Iterator $newMatrixArr = $this->matrix; foreach ($newMatrixArr as $i => $vector) { - foreach ($vector as $j => $value) { + foreach ($vector as $j => $_) { $newMatrixArr[$i][$j] += $scalar; } } @@ -534,25 +539,45 @@ class Matrix implements \ArrayAccess, \Iterator throw new InvalidDimensionException($mDim . 'x' . $nDim); } - $matrixArr = $matrix->getMatrix(); - $newMatrix = new self($this->m, $nDim); - $newMatrixArr = $newMatrix->getMatrix(); + $matrixArr = $matrix->toArray(); + $newMatrixArr = \array_fill(0, $this->m, \array_fill(0, $nDim, 0)); - for ($i = 0; $i < $this->m; ++$i) { // Row of $this - for ($c = 0; $c < $nDim; ++$c) { // Column of $matrix - $temp = 0; - - for ($j = 0; $j < $mDim; ++$j) { // Row of $matrix - $temp += ($this->matrix[$i][$j] ?? 0) * ($matrixArr[$j][$c] ?? 0); + if ($mDim > 10 || $nDim > 10) { + // Standard transposed for iteration over rows -> higher cache hit + $transposedMatrixArr = []; + for ($k = 0; $k < $mDim; ++$k) { + for ($j = 0; $j < $nDim; ++$j) { + $transposedMatrixArr[$k][$j] = $matrixArr[$j][$k]; } + } - $newMatrixArr[$i][$c] = $temp; + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $temp = 0; + + for ($k = 0; $k < $mDim; ++$k) { + $temp += $this->matrix[$i][$k] * $transposedMatrixArr[$i][$k]; + } + + $newMatrixArr[$i][$j] = $temp; + } + } + } else { + // Standard + for ($i = 0; $i < $this->m; ++$i) { + for ($j = 0; $j < $nDim; ++$j) { + $temp = 0; + + for ($k = 0; $k < $mDim; ++$k) { + $temp += $this->matrix[$i][$k] * $matrixArr[$k][$j]; + } + + $newMatrixArr[$i][$j] = $temp; + } } } - $newMatrix->setMatrix($newMatrixArr); /* @phpstan-ignore-line */ - - return $newMatrix; + return self::fromArray($newMatrixArr); } /** @@ -569,7 +594,7 @@ class Matrix implements \ArrayAccess, \Iterator $newMatrixArr = $this->matrix; foreach ($newMatrixArr as $i => $vector) { - foreach ($vector as $j => $value) { + foreach ($vector as $j => $_) { $newMatrixArr[$i][$j] *= $scalar; } } @@ -693,80 +718,7 @@ class Matrix implements \ArrayAccess, \Iterator } /** - * Dot product - * - * @param self $B Matrix - * - * @return self - * - * @since 1.0.0 - */ - public function dot(self $B) : self - { - $value1 = $this->matrix; - $value2 = $B->getMatrix(); - - $m1 = \count($value1); - $n1 = ($isMatrix1 = \is_array($value1[0])) ? \count($value1[0]) : 1; - - $m2 = \count($value2); - $n2 = ($isMatrix2 = \is_array($value2[0])) ? \count($value2[0]) : 1; - - $result = null; - - if ($isMatrix1 && $isMatrix2) { - if ($m2 !== $n1) { - throw new InvalidDimensionException($m2 . 'x' . $n2 . ' not compatible with ' . $m1 . 'x' . $n1); - } - - $result = [[]]; - for ($i = 0; $i < $m1; ++$i) { // Row of 1 - for ($c = 0; $c < $n2; ++$c) { // Column of 2 - $temp = 0; - - for ($j = 0; $j < $m2; ++$j) { // Row of 2 - $temp += $value1[$i][$j] * $value2[$j][$c]; - } - - $result[$i][$c] = $temp; - } - } - - return self::fromArray($result); - } elseif (!$isMatrix1 && !$isMatrix2) { - if ($m1 !== $m2) { - throw new InvalidDimensionException($m1 . 'x' . $m2); - } - - $result = 0; - for ($i = 0; $i < $m1; ++$i) { - /** @var array $value1 */ - /** @var array $value2 */ - $result += $value1[$i] * $value2[$i]; - } - - return self::fromArray([[$result]]); - } elseif ($isMatrix1 && !$isMatrix2) { - $result = []; - for ($i = 0; $i < $m1; ++$i) { // Row of 1 - $temp = 0; - - for ($c = 0; $c < $m2; ++$c) { // Row of 2 - /** @var array $value2 */ - $temp += $value1[$i][$c] * $value2[$c]; - } - - $result[$i] = $temp; - } - - return self::fromArray($result); - } - - throw new \InvalidArgumentException(); - } - - /** - * Sum the elements in the matrix + * Sum the elements in the matrix. * * @param int $axis Axis (-1 -> all dimensions, 0 -> columns, 1 -> rows) * @@ -796,14 +748,14 @@ class Matrix implements \ArrayAccess, \Iterator } } - return self::fromArray($sum); + return Vector::fromArray($sum); } elseif ($axis === 1) { $sum = []; foreach ($this->matrix as $idx => $row) { $sum[$idx] = \array_sum($row); } - return self::fromArray($sum); + return Vector::fromArray($sum); } return new self(); @@ -870,15 +822,25 @@ class Matrix implements \ArrayAccess, \Iterator } return $matrix; - } else { - // @todo: implement - throw new \Exception('Not yet implemented'); } + + $eig = new EigenvalueDecomposition($this); + + $d = $eig->getD(); + $m = $d->getM(); + + for ($i = 0; $i < $m; ++$i) { + $d->matrix[$i][$i] = \pow($d->matrix[$i][$i], $exponent); + } + + return $eig->getV()->mult($d)->mult($eig->getV()->inverse()); } /** * Calculate e^M * + * The algorithm uses a taylor series. + * * @param int $iterations Iterations for approximation * * @return self @@ -891,22 +853,20 @@ class Matrix implements \ArrayAccess, \Iterator throw new InvalidDimensionException($this->m . 'x' . $this->n); } - $identity = new IdentityMatrix($this->m); - $matrix = $identity; + $sum = new IdentityMatrix($this->m); $factorial = 1; - $pow = $matrix; + $pow = clone $sum; for ($i = 1; $i <= $iterations; ++$i) { $factorial *= $i; - $coeff = 1 / $factorial; + $coeff = 1 / $factorial; - $term = $pow->mult($coeff); - $matrix = $matrix->add($term); - $pow = $pow->mult($matrix); // @todo: maybe wrong order? + $pow = $pow->mult($this); + $sum = $sum->add($pow->mult($coeff)); } - return $matrix; + return $sum; } /** diff --git a/Math/Matrix/Vector.php b/Math/Matrix/Vector.php index 38e01d164..2ee9e3cf0 100755 --- a/Math/Matrix/Vector.php +++ b/Math/Matrix/Vector.php @@ -53,7 +53,7 @@ final class Vector extends Matrix */ public function setV(int $m, int | float $value) : void { - parent::set($m , 0, $value); + $this->matrix[$m][0] = $value; } /** @@ -67,7 +67,7 @@ final class Vector extends Matrix */ public function getV(int $m) : int | float { - return parent::get($m, 0); + return $this->matrix[$m][0]; } /** @@ -82,7 +82,7 @@ final class Vector extends Matrix public function setMatrixV(array $vector) : self { foreach ($vector as $key => $value) { - $this->setV($key, $value); + $this->matrix[$key][0] = $value; } return $this; @@ -123,6 +123,64 @@ final class Vector extends Matrix return $dotProduct / ($magnitude1 * $magnitude2); } + /** + * Calculate the euclidean dot product + * + * @param self $vector Vector + * + * @return float + * + * @since 1.0.0 + */ + public function dot(self $vector) : float + { + $length = $this->m; + $m1 = 0; + $m2 = 0; + $prod = 0; + + for ($i = 0; $i < $length; ++$i) { + $m1 += $this->matrix[$i][0] * $this->matrix[$i][0]; + $m2 += $vector->matrix[$i][0] * $vector->matrix[$i][0]; + $prod += $this->matrix[$i][0] * $vector->matrix[$i][0]; + } + + $m1 = \sqrt($m1); + $m2 = \sqrt($m2); + + $cos = $prod / ($m1 * $m2); + + return $m1 * $m2 * $cos; + } + + /** + * Calculate the angle between two vectors + * + * @param self $vector Vector + * + * @return float + * + * @since 1.0.0 + */ + public function angle(self $vector) : float + { + $length = $this->m; + $m1 = 0; + $m2 = 0; + $prod = 0; + + for ($i = 0; $i < $length; ++$i) { + $m1 += $this->matrix[$i][0] * $this->matrix[$i][0]; + $m2 += $vector->matrix[$i][0] * $vector->matrix[$i][0]; + $prod += $this->matrix[$i][0] * $vector->matrix[$i][0]; + } + + $m1 = \sqrt($m1); + $m2 = \sqrt($m2); + + return \acos($prod / ($m1 * $m2)); + } + /** * Calculate the cross product * @@ -135,11 +193,32 @@ final class Vector extends Matrix public function cross3(self $vector) : self { $crossArray = [ - $this->getV(1) * $vector->getV(2) - $this->getV(2) * $vector->getV(1), - $this->getV(2) * $vector->getV(0) - $this->getV(0) * $vector->getV(2), - $this->getV(0) * $vector->getV(1) - $this->getV(1) * $vector->getV(0), + $this->matrix[1][0] * $vector->matrix[2][0] - $this->matrix[2][0] * $vector->matrix[1][0], + $this->matrix[2][0] * $vector->matrix[0][0] - $this->matrix[0][0] * $vector->matrix[2][0], + $this->matrix[0][0] * $vector->matrix[1][0] - $this->matrix[1][0] * $vector->matrix[0][0], ]; return self::fromArray($crossArray); } + + /* + public function cross(self $vector) : float + { + $mat = []; + for ($i = 0; $i < $this->n; ++$i) { + for ($j = 0; $j < $this->n; ++$j) { + $mat[$i][$j] = ($i === 0) + ? $this->matrix[$j][0] + : (($i === 1) + ? $vector->matrix[$j][0] + : 0 + ); + } + } + + $matrix = Matrix::fromArray($mat); + + return $matrix->det(); + } + */ } diff --git a/Math/Number/Integer.php b/Math/Number/Integer.php index f1952e588..3aef486f3 100755 --- a/Math/Number/Integer.php +++ b/Math/Number/Integer.php @@ -73,7 +73,7 @@ final class Integer while ($value % $prime === 0) { $factors[] = $prime; - $value /= $prime; + $value /= $prime; } } @@ -108,7 +108,7 @@ final class Integer } $cycleSize *= 2; - $y = $x; + $y = $x; } return $factor; diff --git a/Math/Number/Numbers.php b/Math/Number/Numbers.php index be331c4bf..bcb1e3338 100755 --- a/Math/Number/Numbers.php +++ b/Math/Number/Numbers.php @@ -102,7 +102,7 @@ final class Numbers } /** - * Count trailling zeros + * Count trailing zeros * * @param int $n Number to test * diff --git a/Math/Numerics/Integration.php b/Math/Numerics/Integration.php index 4ddeb8345..352387d44 100755 --- a/Math/Numerics/Integration.php +++ b/Math/Numerics/Integration.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Numerics; */ final class Integration { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Integrate function by using rectangles from the left side * diff --git a/Math/Numerics/Interpolation/LagrangeInterpolation.php b/Math/Numerics/Interpolation/LagrangeInterpolation.php index 58f18e319..70b9a31cd 100755 --- a/Math/Numerics/Interpolation/LagrangeInterpolation.php +++ b/Math/Numerics/Interpolation/LagrangeInterpolation.php @@ -39,7 +39,8 @@ final class LagrangeInterpolation implements InterpolationInterface * * @since 1.0.0 */ - public function __construct(array $points) { + public function __construct(array $points) + { $this->points = $points; } diff --git a/Math/Numerics/Interpolation/LinearInterpolation.php b/Math/Numerics/Interpolation/LinearInterpolation.php index 26e5da4e8..0ec184965 100755 --- a/Math/Numerics/Interpolation/LinearInterpolation.php +++ b/Math/Numerics/Interpolation/LinearInterpolation.php @@ -65,7 +65,8 @@ final class LinearInterpolation implements InterpolationInterface * * @since 1.0.0 */ - public function __construct(array $points) { + public function __construct(array $points) + { $this->points = $points; $n = \count($this->points); diff --git a/Math/Optimization/Simplex.php b/Math/Optimization/Simplex.php index 190f28a48..88d06a93c 100644 --- a/Math/Optimization/Simplex.php +++ b/Math/Optimization/Simplex.php @@ -17,30 +17,113 @@ namespace phpOMS\Math\Optimization; /** * Simplex class. * + * The Simplex algorithm aims to solve a linear program - optimizing a linear function subject + * to linear constraints. As such it is useful for a very wide range of applications. + * + * N.B. The linear program has to be given in *slack form*, which is as follows: + * maximize + * c_1 * x_1 + c_2 * x_2 + ... + c_n * x_n + v + * subj. to + * a_11 * x_1 + a_12 * x_2 + ... + a_1n * x_n + b_1 = s_1 + * a_21 * x_1 + a_22 * x_2 + ... + a_2n * x_n + b_2 = s_2 + * ... + * a_m1 * x_1 + a_m2 * x_2 + ... + a_mn * x_n + b_m = s_m + * and + * x_1, x_2, ..., x_n, s_1, s_2, ..., s_m >= 0 + * + * Every linear program can be translated into slack form; the parameters to specify are: + * - the number of variables, n, and the number of constraints, m; + * - the matrix A = [[A_11, A_12, ..., A_1n], ..., [A_m1, A_m2, ..., A_mn]]; + * - the vector b = [b_1, b_2, ..., b_m]; + * - the vector c = [c_1, c_2, ..., c_n] and the constant v. + * + * Complexity: O(m^(n/2)) worst case + * O(n + m) average case (common) + * * @package phpOMS\Math\Optimization + * @license Copyright (c) 2015 Petar Veličković * @license OMS License 2.0 * @link https://jingga.app * @link https://github.com/PetarV-/Algorithms/blob/master/Mathematical%20Algorithms/Simplex%20Algorithm.cpp * @since 1.0.0 */ -class Simplex +final class Simplex { + /** + * Bounding equations + * + * @var int + * @since 1.0.0 + */ private int $m = 0; + + /** + * Bounding variables + * + * @var int + * @since 1.0.0 + */ private int $n = 0; + /** + * Bounding equations + * + * @var array> + * @since 1.0.0 + */ private array $A = []; + /** + * Bounds for bounding equations + * + * @var array + * @since 1.0.0 + */ private array $b = []; + /** + * Maximize vector + * + * @var array + * @since 1.0.0 + */ private array $c = []; - private int $v = 0; + /** + * Maximized value + * + * @var float + * @since 1.0.0 + */ + private float $v = 0.0; - private array $Basic = []; + /** + * Basic solutions + * + * @var array + * @since 1.0.0 + */ + private array $basic = []; - private array $Nonbasic = []; + /** + * Non-basic solutions + * + * @var array + * @since 1.0.0 + */ + private array $nonbasic = []; - private function pivot (int $x, int $y) + /** + * Pivot yth variable around xth constraint + * + * @param int $x Constraint index + * @param int $y Variable index + * + * @return void + * + * @since 1.0.0 + */ + private function pivot(int $x, int $y) : void { for ($j = 0; $j < $this->n; ++$j) { if ($j !== $y) { @@ -59,7 +142,7 @@ class Simplex } } - $this->b[$i] += $this->A[$i][$y] / $this->b[$x]; + $this->b[$i] += $this->A[$i][$y] * $this->b[$x]; $this->A[$i][$y] *= $this->A[$x][$y]; } } @@ -73,22 +156,29 @@ class Simplex $this->v += $this->c[$y] * $this->b[$x]; $this->c[$y] *= $this->A[$x][$y]; - $temp = $this->Basic[$x]; - $this->Basic[$x] = $this->Nonbasic[$y]; - $this->Nonbasic[$y] = $temp; + $temp = $this->basic[$x]; + $this->basic[$x] = $this->nonbasic[$y]; + $this->nonbasic[$y] = $temp; } + /** + * Perform simplex iteration step + * + * @return int 0 = OK, 1 = stop, -1 = unbound + * + * @since 1.0.0 + */ private function iterate() : int { - $ind = -1; + $ind = -1; $best = -1; for ($j = 0; $j < $this->n; ++$j) { - if ($this->c[$j] > 0) { - if ($best === -1 || $this->Nonbasic[$j] < $ind) { - $ind = $this->Nonbasic[$j]; - $best = $j; - } + if ($this->c[$j] > 0 + && ($best === -1 || $this->nonbasic[$j] < $ind) + ) { + $ind = $this->nonbasic[$j]; + $best = $j; } } @@ -96,14 +186,14 @@ class Simplex return 1; } - $maxConstraint = \INF; + $maxConstraint = \INF; $bestConstraint = -1; for ($i = 0; $i < $this->m; ++$i) { if ($this->A[$i][$best] < 0) { $currentConstraint = -$this->b[$i] / $this->A[$i][$best]; if ($currentConstraint < $maxConstraint) { - $maxConstraint = $currentConstraint; + $maxConstraint = $currentConstraint; $bestConstraint = $i; } } @@ -118,25 +208,35 @@ class Simplex return 0; } + /** + * Initialize simplex algorithm + * + * 1. possibly converts LP to slack form + * 2. find feasible basic solution + * + * @return int 0 = OK, 1 = stop, -1 = unbound + * + * @since 1.0.0 + */ private function initialize() : int { - $k = -1; + $k = -1; $minB = -1; for ($i = 0; $i < $this->m; ++$i) { if ($k === -1 || $this->b[$i] < $minB) { - $k = $i; + $k = $i; $minB = $this->b[$i]; } } if ($this->b[$k] >= 0) { for ($j = 0; $j < $this->n; ++$j) { - $this->Nonbasic[$j] = $j; + $this->nonbasic[$j] = $j; } for ($i = 0; $i < $this->m; ++$i) { - $this->Basic[$i] = $this->n + $i; + $this->basic[$i] = $this->n + $i; } return 0; @@ -144,11 +244,11 @@ class Simplex ++$this->n; for ($j = 0; $j < $this->n; ++$j) { - $this->Nonbasic[$j] = $j; + $this->nonbasic[$j] = $j; } for ($i = 0; $i < $this->m; ++$i) { - $this->Basic[$i] = $this->n + $i; + $this->basic[$i] = $this->n + $i; } $oldC = []; @@ -163,7 +263,7 @@ class Simplex $this->c[$j] = 0; } - $this->v = 0; + $this->v = 0.0; for ($i = 0; $i < $this->m; ++$i) { $this->A[$i][$this->n - 1] = 1; @@ -173,13 +273,13 @@ class Simplex while (!$this->iterate()); - if ($this->v !== 0) { + if ($this->v !== 0.0) { return -1; } $basicZ = -1; for ($i = 0; $i < $this->m; ++$i) { - if ($this->Basic[$i] === $this->n - 1) { + if ($this->basic[$i] === $this->n - 1) { $basicZ = $i; break; } @@ -191,7 +291,7 @@ class Simplex $nonbasicZ = -1; for ($j = 0; $j < $this->n; ++$j) { - if ($this->Nonbasic[$j] === $this->n - 1) { + if ($this->nonbasic[$j] === $this->n - 1) { $nonbasicZ = $j; break; } @@ -201,20 +301,20 @@ class Simplex $this->A[$i][$nonbasicZ] = $this->A[$i][$this->n - 1]; } - $temp = $this->Nonbasic[$nonbasicZ]; - $this->Nonbasic[$nonbasicZ] = $this->Nonbasic[$this->n - 1]; - $this->Nonbasic[$this->n - 1] = $temp; + $temp = $this->nonbasic[$nonbasicZ]; + $this->nonbasic[$nonbasicZ] = $this->nonbasic[$this->n - 1]; + $this->nonbasic[$this->n - 1] = $temp; --$this->n; for ($j = 0; $j < $this->n; ++$j) { - if ($this->Nonbasic[$j] > $this->n) { - --$this->Nonbasic[$j]; + if ($this->nonbasic[$j] > $this->n) { + --$this->nonbasic[$j]; } } for ($i = 0; $i < $this->m; ++$i) { - if ($this->Basic[$i] > $this->n) { - --$this->Basic[$i]; + if ($this->basic[$i] > $this->n) { + --$this->basic[$i]; } } @@ -227,7 +327,7 @@ class Simplex for ($j = 0; $j < $this->n; ++$j) { $ok = false; for ($k = 0; $k < $this->n; ++$k) { - if ($j = $this->Nonbasic[$k]) { + if ($j === $this->nonbasic[$k]) { $this->c[$k] += $oldC[$j]; $ok = true; break; @@ -239,7 +339,7 @@ class Simplex } for ($i = 0; $i < $this->m; ++$i) { - if ($j === $this->Basic[$i]) { + if ($j === $this->basic[$i]) { for ($k = 0; $k < $this->n; ++$k) { $this->c[$k] = $oldC[$j] * $this->A[$i][$k]; } @@ -253,15 +353,34 @@ class Simplex return 0; } - public function solve(array $A, array $b, array $c) + /** + * Solve simplex problem + * + * @param array> $A Bounding equations + * @param int[]|float[] $b Boundings for equations + * @param int[]|float[] $c Equation to maximize + * + * @return array|array{0:array, 1:float} + * + * @since 1.0.0 + */ + public function solve(array $A, array $b, array $c) : array { $this->A = $A; $this->b = $b; $this->c = $c; - // @todo: createSlackForm() required? + // @question Consider to generate slack form. It this required? + // https://github.com/Karaka-Management/phpOMS/issues/349 + + // @feature Handle minimize and maximize in Simplex + // https://github.com/Karaka-Management/phpOMS/issues/368 $this->m = \count($A); + if ($this->m < 1) { + return []; + } + $this->n = \count(\reset($A)); if ($this->initialize() === -1) { @@ -277,11 +396,11 @@ class Simplex $result = []; for ($j = 0; $j < $this->n; ++$j) { - $result[$this->Nonbasic[$j]] = 0; + $result[$this->nonbasic[$j]] = 0; } for ($i = 0; $i < $this->m; ++$i) { - $result[$this->Basic[$i]] = $this->b[$i]; + $result[$this->basic[$i]] = $this->b[$i]; } return [$result, $this->v]; diff --git a/Math/Parser/Evaluator.php b/Math/Parser/Evaluator.php index e9307952f..bdf1952ae 100755 --- a/Math/Parser/Evaluator.php +++ b/Math/Parser/Evaluator.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Parser; */ final class Evaluator { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Evaluate function. * @@ -105,7 +115,7 @@ final class Evaluator '+' => ['precedence' => 2, 'order' => -1], '-' => ['precedence' => 2, 'order' => -1], ]; - $output = []; + $output = []; $equation = \str_replace(' ', '', $equation); $equation = \preg_split('/([\+\-\*\/\^\(\)])/', $equation, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); @@ -118,7 +128,7 @@ final class Evaluator return $n !== ''; }); - foreach ($equation as $i => $token) { + foreach ($equation as $token) { if (\is_numeric($token)) { $output[] = $token; } elseif (\strpbrk($token, '^*/+-') !== false) { @@ -130,7 +140,7 @@ final class Evaluator /*|| ($operators[$o1]['order'] === 1 && $operators[$o1]['precedence'] < $operators[$o2]['precedence'])*/) ) { // The commented part above is always FALSE because this equation always compares 4 < 2|3|4. - // Only uncomment if the opperators array changes. + // Only uncomment if the operators array changes. $output[] = \array_pop($stack); $o2 = \end($stack); } diff --git a/Math/Solver/Root/Bisection.php b/Math/Solver/Root/Bisection.php index 23fd7eaa7..cb92dab64 100644 --- a/Math/Solver/Root/Bisection.php +++ b/Math/Solver/Root/Bisection.php @@ -30,14 +30,24 @@ final class Bisection * @var float * @since 1.0.0 */ - public const EPSILON = 1e-6; + public const EPSILON = 4.88e-04; + + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } /** * Perform bisection to find the root of a function * * Iteratively searches for root between two points on the x-axis * - * @param Callable $func Function defintion + * @param Callable $func Function definition * @param float $a Start value * @param float $b End value * @param int $maxIterations Maximum amount of iterations diff --git a/Math/Solver/Root/Illinois.php b/Math/Solver/Root/Illinois.php index 90b5d8091..1e7965762 100644 --- a/Math/Solver/Root/Illinois.php +++ b/Math/Solver/Root/Illinois.php @@ -30,14 +30,24 @@ final class Illinois * @var float * @since 1.0.0 */ - public const EPSILON = 1e-6; + public const EPSILON = 4.88e-04; + + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } /** * Perform bisection to find the root of a function * * Iteratively searches for root between two points on the x-axis * - * @param Callable $func Function defintion + * @param Callable $func Function definition * @param float $a Start value * @param float $b End value * @param int $maxIterations Maximum amount of iterations @@ -51,7 +61,7 @@ final class Illinois public static function root(callable $func, float $a, float $b, int $maxIterations = 100) : float { if ($func($a) * $func($b) >= 0) { - throw new \Exception("Function values at endpoints must have opposite signs."); + throw new \Exception('Function values at endpoints must have opposite signs.'); } $c = $b; @@ -66,9 +76,7 @@ final class Illinois return $c; } - // @todo: c might be wrong, could be that if and else must be switched - // @see https://en.wikipedia.org/wiki/Regula_falsi#The_Illinois_algorithm - if ($y * $fa < 0) { + if ($fa < 0) { $c = $sign === (int) ($y >= 0) ? (0.5 * $a * $fb - $b * $fa) / (0.5 * $fb - $fa) : ($a * $fb - $b * $fa) / ($fb - $fa); @@ -87,6 +95,6 @@ final class Illinois ++$iteration; } - return ($a + $b) / 2; + return $c; } } diff --git a/Math/Solver/Root/RegulaFalsi.php b/Math/Solver/Root/RegulaFalsi.php index fe9bb9210..361d18b23 100644 --- a/Math/Solver/Root/RegulaFalsi.php +++ b/Math/Solver/Root/RegulaFalsi.php @@ -30,14 +30,24 @@ final class RegulaFalsi * @var float * @since 1.0.0 */ - public const EPSILON = 1e-6; + public const EPSILON = 4.88e-04; + + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } /** * Perform bisection to find the root of a function * * Iteratively searches for root between two points on the x-axis * - * @param Callable $func Function defintion + * @param Callable $func Function definition * @param float $a Start value * @param float $b End value * @param int $maxIterations Maximum amount of iterations @@ -76,6 +86,6 @@ final class RegulaFalsi ++$iteration; } - return ($a + $b) / 2; + return $c; } } diff --git a/Math/Statistic/Average.php b/Math/Statistic/Average.php index 887cf2293..18d81bb0a 100755 --- a/Math/Statistic/Average.php +++ b/Math/Statistic/Average.php @@ -27,27 +27,69 @@ use phpOMS\Math\Matrix\Exception\InvalidDimensionException; */ final class Average { - public const MA3 = [1 / 3, 1 / 3]; + /** + * Moving average weights + * + * @var float[] + * @since 1.0.0 + */ + public const MA3 = [1 / 3, 1 / 3, 1 / 3]; - public const MA5 = [0.2, 0.2, 0.2]; + /** + * Moving average weights + * + * @var float[] + * @since 1.0.0 + */ + public const MA5 = [0.2, 0.2, 0.2, 0.2, 0.2]; - public const MA2X12 = [5 / 6, 5 / 6, 5 / 6, 5 / 6, 5 / 6, 5 / 6, 0.42]; + /** + * Moving average weights + * + * @var float[] + * @since 1.0.0 + */ + public const MAS15 = [-0.009, -0.019, -0.016, 0.009, 2 / 3, 0.144, 0.209, 0.231, 0.209, 0.144, 2 / 3, 0.009, -0.016, -0.019, -0.009]; - public const MA3X3 = [1 / 3, 2 / 9, 1 / 9]; + /** + * Moving average weights + * + * @var float[] + * @since 1.0.0 + */ + public const MAS21 = [-0.003, -0.009, -0.014, -0.014, -0.006, 0.017, 0.51, 0.37, 0.134, 0.163, 0.171, 0.163, 0.134, 0.37, 0.51, 0.017, -0.006, -0.014, -0.014, -0.009, -0.003]; - public const MA3X5 = [0.2, 0.2, 2 / 15, 4 / 6]; + /** + * Moving average weights + * + * @var float[] + * @since 1.0.0 + */ + public const MAH5 = [-0.73, 0.294, 0.558, 0.294, -0.73]; - public const MAS15 = [0.231, 0.209, 0.144, 2 / 3, 0.009, -0.016, -0.019, -0.009]; + /** + * Moving average weights + * + * @var float[] + * @since 1.0.0 + */ + public const MAH9 = [-0.041, -0.01, 0.119, 0.267, 0.330, 0.267, 0.119, -0.010, -0.041]; - public const MAS21 = [0.171, 0.163, 0.134, 0.37, 0.51, 0.017, -0.006, -0.014, -0.014, -0.009, -0.003]; + /** + * Moving average weights + * + * @var float[] + * @since 1.0.0 + */ + public const MAH13 = [-0.019, -0.028, 0, 0.66, 0.147, 0.214, 0.240, 0.214, 0.147, 0.66, 0, -0.028, -0.019]; - public const MAH5 = [0.558, 0.294, -0.73]; - - public const MAH9 = [0.330, 0.267, 0.119, -0.010, -0.041]; - - public const MAH13 = [0.240, 0.214, 0.147, 0.66, 0, -0.028, -0.019]; - - public const MAH23 = [0.148, 0.138, 0.122, 0.097, 0.068, 0.039, 0.013, -0.005, -0.015, -0.016, -0.011, -0.004]; + /** + * Moving average weights + * + * @var float[] + * @since 1.0.0 + */ + public const MAH23 = [-0.004, -0.011, -0.016, -0.015, -0.005, 0.013, 0.039, 0.068, 0.097, 0.122, 0.138, 0.148, 0.138, 0.122, 0.097, 0.068, 0.039, 0.013, -0.005, -0.015, -0.016, -0.011, -0.004]; /** * Constructor. @@ -90,7 +132,7 @@ final class Average * * @since 1.0.0 */ - public static function totalMovingAverage(array $x, int $order, array $weight = null, bool $symmetric = false) : array + public static function totalMovingAverage(array $x, int $order, ?array $weight = null, bool $symmetric = false) : array { $periods = (int) ($order / ($symmetric ? 2 : 1)); $count = \count($x) - ($symmetric ? $periods : 0); @@ -118,7 +160,7 @@ final class Average * * @since 1.0.0 */ - public static function movingAverage(array $x, int $t, int $order, array $weight = null, bool $symmetric = false) : float + public static function movingAverage(array $x, int $t, int $order, ?array $weight = null, bool $symmetric = false) : float { $periods = (int) ($order / ($symmetric ? 2 : 1)); $count = \count($x); @@ -127,7 +169,7 @@ final class Average throw new \Exception('Periods'); } - $t += 2; + $t += 2; $end = $symmetric ? $t + $periods - 1 : $t - 1; $start = $t - 1 - $periods; diff --git a/Math/Statistic/Basic.php b/Math/Statistic/Basic.php index c165e7fe5..9826b581b 100755 --- a/Math/Statistic/Basic.php +++ b/Math/Statistic/Basic.php @@ -47,17 +47,17 @@ final class Basic */ public static function frequency(array $values) : array { - $freaquency = []; - $sum = 1; + $frequency = []; + $sum = 1; if (!(\is_array(\reset($values)))) { $sum = \array_sum($values); } foreach ($values as $value) { - $freaquency[] = \is_array($value) ? self::frequency($value) : $value / $sum; + $frequency[] = \is_array($value) ? self::frequency($value) : $value / $sum; } - return $freaquency; + return $frequency; } } diff --git a/Math/Statistic/Correlation.php b/Math/Statistic/Correlation.php index a4b446b81..278d35932 100755 --- a/Math/Statistic/Correlation.php +++ b/Math/Statistic/Correlation.php @@ -35,7 +35,7 @@ final class Correlation } /** - * Calculage bravais person correlation coefficient. + * Calculate bravais person correlation coefficient. * * Example: ([4, 5, 9, 1, 3], [4, 5, 9, 1, 3]) * @@ -54,7 +54,7 @@ final class Correlation } /** - * Calculage bravais person correlation coefficient. + * Calculate bravais person correlation coefficient. * * Example: ([4, 5, 9, 1, 3], [4, 5, 9, 1, 3]) * diff --git a/Math/Statistic/Forecast/Forecasts.php b/Math/Statistic/Forecast/Forecasts.php index 0f7aeb8cf..6a2e0e9d9 100755 --- a/Math/Statistic/Forecast/Forecasts.php +++ b/Math/Statistic/Forecast/Forecasts.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Statistic\Forecast; */ final class Forecasts { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get forecast/prediction interval. * @@ -61,7 +71,7 @@ final class Forecasts $variance += \pow($sale - $avg, 2); } - $variance /= $size; + $variance /= $size; $stdDeviation = \sqrt($variance); // Calculate the seasonal index for each period diff --git a/Math/Statistic/Forecast/Regression/LevelLevelRegression.php b/Math/Statistic/Forecast/Regression/LevelLevelRegression.php index beb11f144..853659a9c 100755 --- a/Math/Statistic/Forecast/Regression/LevelLevelRegression.php +++ b/Math/Statistic/Forecast/Regression/LevelLevelRegression.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Statistic\Forecast\Regression; */ final class LevelLevelRegression extends RegressionAbstract { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * {@inheritdoc} */ diff --git a/Math/Statistic/Forecast/Regression/LevelLogRegression.php b/Math/Statistic/Forecast/Regression/LevelLogRegression.php index 51d84f96a..6635d881a 100755 --- a/Math/Statistic/Forecast/Regression/LevelLogRegression.php +++ b/Math/Statistic/Forecast/Regression/LevelLogRegression.php @@ -26,6 +26,16 @@ use phpOMS\Math\Matrix\Exception\InvalidDimensionException; */ final class LevelLogRegression extends RegressionAbstract { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * {@inheritdoc} * diff --git a/Math/Statistic/Forecast/Regression/LogLevelRegression.php b/Math/Statistic/Forecast/Regression/LogLevelRegression.php index d73d463c4..59a96427c 100755 --- a/Math/Statistic/Forecast/Regression/LogLevelRegression.php +++ b/Math/Statistic/Forecast/Regression/LogLevelRegression.php @@ -26,6 +26,16 @@ use phpOMS\Math\Matrix\Exception\InvalidDimensionException; */ final class LogLevelRegression extends RegressionAbstract { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * {@inheritdoc} * diff --git a/Math/Statistic/Forecast/Regression/LogLogRegression.php b/Math/Statistic/Forecast/Regression/LogLogRegression.php index 9ad23204d..1ce7a17df 100755 --- a/Math/Statistic/Forecast/Regression/LogLogRegression.php +++ b/Math/Statistic/Forecast/Regression/LogLogRegression.php @@ -26,6 +26,16 @@ use phpOMS\Math\Matrix\Exception\InvalidDimensionException; */ final class LogLogRegression extends RegressionAbstract { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * {@inheritdoc} * diff --git a/Math/Statistic/Forecast/Regression/MultipleLinearRegression.php b/Math/Statistic/Forecast/Regression/MultipleLinearRegression.php index d5303861a..57eb5ec52 100755 --- a/Math/Statistic/Forecast/Regression/MultipleLinearRegression.php +++ b/Math/Statistic/Forecast/Regression/MultipleLinearRegression.php @@ -26,6 +26,16 @@ use phpOMS\Math\Matrix\Matrix; */ final class MultipleLinearRegression extends RegressionAbstract { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get linear regression based on scatter plot. * @@ -47,7 +57,7 @@ final class MultipleLinearRegression extends RegressionAbstract $Y = new Matrix(\count($y)); $Y->setMatrix($y); - return $XT->mult($X)->inverse()->mult($XT)->mult($Y)->getMatrix(); + return $XT->mult($X)->inverse()->mult($XT)->mult($Y)->toArray(); } /** diff --git a/Math/Statistic/Forecast/Regression/PolynomialRegression.php b/Math/Statistic/Forecast/Regression/PolynomialRegression.php index 3bb3f90a7..bec61209a 100755 --- a/Math/Statistic/Forecast/Regression/PolynomialRegression.php +++ b/Math/Statistic/Forecast/Regression/PolynomialRegression.php @@ -27,6 +27,16 @@ use phpOMS\Math\Statistic\Average; */ final class PolynomialRegression { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get linear regression based on scatter plot. * diff --git a/Math/Statistic/Forecast/Regression/RegressionAbstract.php b/Math/Statistic/Forecast/Regression/RegressionAbstract.php index 79a780058..b08d87603 100755 --- a/Math/Statistic/Forecast/Regression/RegressionAbstract.php +++ b/Math/Statistic/Forecast/Regression/RegressionAbstract.php @@ -27,12 +27,22 @@ use phpOMS\Math\Statistic\Average; */ abstract class RegressionAbstract { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get linear regression based on scatter plot. * * @latex y = b_{0} + b_{1} \cdot x * - * @param array $x Obersved x values + * @param array $x Observed x values * @param array $y Observed y values * * @return array [b0 => ?, b1 => ?] @@ -103,13 +113,13 @@ abstract class RegressionAbstract } /** - * Get predictional interval for linear regression. + * Get prediction interval for linear regression. * * @latex * * @param float $fX Forecasted at x value * @param float $fY Forecasted y value - * @param array $x observex x values + * @param array $x Observed x values * @param float $mse Errors for y values (y - y_forecasted) * @param float $multiplier Multiplier for interval * @@ -137,7 +147,7 @@ abstract class RegressionAbstract * * @latex \beta_{1} = \frac{\sum_{i=1}^{N} \left(y_{i} - \bar{y}\right)\left(x_{i} - \bar{x}\right)}{\sum_{i=1}^{N} \left(x_{i} - \bar{x}\right)^{2}} * - * @param array $x Obersved x values + * @param array $x Observed x values * @param array $y Observed y values * * @return float @@ -166,7 +176,7 @@ abstract class RegressionAbstract * * @latex \beta_{0} = \bar{x} - b_{1} \cdot \bar{x} * - * @param array $x Obersved x values + * @param array $x Observed x values * @param array $y Observed y values * @param float $b1 Beta 1 * @@ -183,7 +193,7 @@ abstract class RegressionAbstract * Get slope * * @param float $b1 Beta 1 - * @param float $x Obersved x values + * @param float $x Observed x values * @param float $y Observed y values * * @return float @@ -196,7 +206,7 @@ abstract class RegressionAbstract * Get elasticity * * @param float $b1 Beta 1 - * @param float $x Obersved x values + * @param float $x Observed x values * @param float $y Observed y values * * @return float diff --git a/Math/Statistic/MeasureOfDispersion.php b/Math/Statistic/MeasureOfDispersion.php index e63b1c42f..f2b59da8d 100755 --- a/Math/Statistic/MeasureOfDispersion.php +++ b/Math/Statistic/MeasureOfDispersion.php @@ -58,7 +58,7 @@ final class MeasureOfDispersion } /** - * Calculage empirical variation coefficient. + * Calculate empirical variation coefficient. * * Example: ([4, 5, 9, 1, 3]) * @@ -71,7 +71,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function empiricalVariationCoefficient(array $values, float $mean = null) : float + public static function empiricalVariationCoefficient(array $values, ?float $mean = null) : float { $mean = $mean !== null ? $mean : Average::arithmeticMean($values); @@ -96,7 +96,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function standardDeviationSample(array $values, float $mean = null) : float + public static function standardDeviationSample(array $values, ?float $mean = null) : float { $mean = $mean !== null ? $mean : Average::arithmeticMean($values); $sum = 0.0; @@ -125,7 +125,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function standardDeviationPopulation(array $values, float $mean = null) : float + public static function standardDeviationPopulation(array $values, ?float $mean = null) : float { $mean = $mean !== null ? $mean : Average::arithmeticMean($values); $sum = 0.0; @@ -141,7 +141,7 @@ final class MeasureOfDispersion } /** - * Calculage sample variance. + * Calculate sample variance. * * Similar to `empiricalVariance`. * @@ -158,7 +158,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function sampleVariance(array $values, float $mean = null) : float + public static function sampleVariance(array $values, ?float $mean = null) : float { $count = \count($values); @@ -170,7 +170,7 @@ final class MeasureOfDispersion } /** - * Calculage empirical variance. + * Calculate empirical variance. * * Similar to `sampleVariance`. * @@ -188,7 +188,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function empiricalVariance(array $values, array $probabilities = [], float $mean = null) : float + public static function empiricalVariance(array $values, array $probabilities = [], ?float $mean = null) : float { $count = \count($values); $hasProbability = !empty($probabilities); @@ -208,7 +208,7 @@ final class MeasureOfDispersion } /** - * Calculage empirical covariance. + * Calculate empirical covariance. * * Example: ([4, 5, 9, 1, 3], [4, 5, 9, 1, 3]) * @@ -226,7 +226,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function empiricalCovariance(array $x, array $y, float $meanX = null, float $meanY = null) : float + public static function empiricalCovariance(array $x, array $y, ?float $meanX = null, ?float $meanY = null) : float { $count = \count($x); @@ -251,7 +251,7 @@ final class MeasureOfDispersion } /** - * Calculage empirical covariance on a sample + * Calculate empirical covariance on a sample * * Example: ([4, 5, 9, 1, 3], [4, 5, 9, 1, 3]) * @@ -268,7 +268,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function sampleCovariance(array $x, array $y, float $meanX = null, float $meanY = null) : float + public static function sampleCovariance(array $x, array $y, ?float $meanX = null, ?float $meanY = null) : float { $count = \count($x); @@ -318,7 +318,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function meanDeviation(array $x, float $mean = null, int $offset = 0) : float + public static function meanDeviation(array $x, ?float $mean = null, int $offset = 0) : float { $mean = $mean !== null ? $mean : Average::arithmeticMean($x); $sum = 0.0; @@ -339,7 +339,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function meanDeviationArray(array $x, float $mean = null) : array + public static function meanDeviationArray(array $x, ?float $mean = null) : array { $mean = $mean !== null ? $mean : Average::arithmeticMean($x); @@ -361,7 +361,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function meanAbsoluteDeviation(array $x, float $mean = null, int $offset = 0) : float + public static function meanAbsoluteDeviation(array $x, ?float $mean = null, int $offset = 0) : float { $mean = $mean !== null ? $mean : Average::arithmeticMean($x); $sum = 0.0; @@ -382,7 +382,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function meanAbsoluteDeviationArray(array $x, float $mean = null) : array + public static function meanAbsoluteDeviationArray(array $x, ?float $mean = null) : array { $mean = $mean !== null ? $mean : Average::arithmeticMean($x); @@ -404,7 +404,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function squaredMeanDeviation(array $x, float $mean = null, int $offset = 0) : float + public static function squaredMeanDeviation(array $x, ?float $mean = null, int $offset = 0) : float { $mean = $mean !== null ? $mean : Average::arithmeticMean($x); $sum = 0.0; @@ -425,7 +425,7 @@ final class MeasureOfDispersion * * @since 1.0.0 */ - public static function squaredMeanDeviationArray(array $x, float $mean = null) : array + public static function squaredMeanDeviationArray(array $x, ?float $mean = null) : array { $mean = $mean !== null ? $mean : Average::arithmeticMean($x); diff --git a/Math/Stochastic/Distribution/BernoulliDistribution.php b/Math/Stochastic/Distribution/BernoulliDistribution.php index 44966a9de..7ed1065b0 100755 --- a/Math/Stochastic/Distribution/BernoulliDistribution.php +++ b/Math/Stochastic/Distribution/BernoulliDistribution.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class BernoulliDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability mass function. * diff --git a/Math/Stochastic/Distribution/BetaDistribution.php b/Math/Stochastic/Distribution/BetaDistribution.php index 914802cfa..42d553acf 100755 --- a/Math/Stochastic/Distribution/BetaDistribution.php +++ b/Math/Stochastic/Distribution/BetaDistribution.php @@ -27,6 +27,16 @@ use phpOMS\Math\Functions\Functions; */ final class BetaDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get expected value. * @@ -153,7 +163,7 @@ final class BetaDistribution } /** - * Get prdobability distribution function. + * Get probability distribution function. * * @param float $x Value * @param float $alpha Alpha diff --git a/Math/Stochastic/Distribution/BinomialDistribution.php b/Math/Stochastic/Distribution/BinomialDistribution.php index 309c6b8e8..1cb483ec9 100755 --- a/Math/Stochastic/Distribution/BinomialDistribution.php +++ b/Math/Stochastic/Distribution/BinomialDistribution.php @@ -26,6 +26,16 @@ use phpOMS\Math\Functions\Functions; */ final class BinomialDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get mode. * diff --git a/Math/Stochastic/Distribution/CauchyDistribution.php b/Math/Stochastic/Distribution/CauchyDistribution.php index 4aeb80358..5bebb4bdd 100755 --- a/Math/Stochastic/Distribution/CauchyDistribution.php +++ b/Math/Stochastic/Distribution/CauchyDistribution.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class CauchyDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function. * diff --git a/Math/Stochastic/Distribution/ChiSquaredDistribution.php b/Math/Stochastic/Distribution/ChiSquaredDistribution.php index 0b0400c49..d8e582090 100755 --- a/Math/Stochastic/Distribution/ChiSquaredDistribution.php +++ b/Math/Stochastic/Distribution/ChiSquaredDistribution.php @@ -75,7 +75,17 @@ final class ChiSquaredDistribution ]; /** - * Test hypthesis. + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /** + * Test hypothesis. * * Goodness of fit test. * @@ -151,7 +161,7 @@ final class ChiSquaredDistribution * Get probability density function. * * @param float $x Value x - * @param int $df Degreegs of freedom + * @param int $df Degrees of freedom * * @return float * @@ -172,7 +182,7 @@ final class ChiSquaredDistribution * Get cumulative density function. * * @param float $x Value x - * @param int $df Degreegs of freedom + * @param int $df Degrees of freedom * * @return float * diff --git a/Math/Stochastic/Distribution/ExponentialDistribution.php b/Math/Stochastic/Distribution/ExponentialDistribution.php index e18bd9b23..7a7446ffd 100755 --- a/Math/Stochastic/Distribution/ExponentialDistribution.php +++ b/Math/Stochastic/Distribution/ExponentialDistribution.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class ExponentialDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function. * diff --git a/Math/Stochastic/Distribution/FDistribution.php b/Math/Stochastic/Distribution/FDistribution.php index 57d1dfffa..a36f6b0b8 100755 --- a/Math/Stochastic/Distribution/FDistribution.php +++ b/Math/Stochastic/Distribution/FDistribution.php @@ -26,12 +26,22 @@ use phpOMS\Math\Functions\Beta; */ final class FDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function. * * @param float $x Value x - * @param int $d1 Degreegs of freedom - * @param int $d2 Degreegs of freedom + * @param int $d1 Degrees of freedom + * @param int $d2 Degrees of freedom * * @return float * @@ -47,8 +57,8 @@ final class FDistribution * Get cumulative density function. * * @param float $x Value x - * @param int $d1 Degreegs of freedom - * @param int $d2 Degreegs of freedom + * @param int $d1 Degrees of freedom + * @param int $d2 Degrees of freedom * * @return float * diff --git a/Math/Stochastic/Distribution/GammaDistribution.php b/Math/Stochastic/Distribution/GammaDistribution.php index fe209ce07..9b100baf9 100755 --- a/Math/Stochastic/Distribution/GammaDistribution.php +++ b/Math/Stochastic/Distribution/GammaDistribution.php @@ -26,6 +26,16 @@ use phpOMS\Math\Functions\Gamma; */ final class GammaDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function for shape and scale. * diff --git a/Math/Stochastic/Distribution/GeometricDistribution.php b/Math/Stochastic/Distribution/GeometricDistribution.php index d0b81fdea..b58b4a4c1 100755 --- a/Math/Stochastic/Distribution/GeometricDistribution.php +++ b/Math/Stochastic/Distribution/GeometricDistribution.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class GeometricDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability mass function. * diff --git a/Math/Stochastic/Distribution/HypergeometricDistribution.php b/Math/Stochastic/Distribution/HypergeometricDistribution.php index 2c33ba7f8..03a598033 100755 --- a/Math/Stochastic/Distribution/HypergeometricDistribution.php +++ b/Math/Stochastic/Distribution/HypergeometricDistribution.php @@ -26,6 +26,16 @@ use phpOMS\Math\Functions\Functions; */ final class HypergeometricDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability mass function. * diff --git a/Math/Stochastic/Distribution/LaplaceDistribution.php b/Math/Stochastic/Distribution/LaplaceDistribution.php index b407a1566..fbf92f0a7 100755 --- a/Math/Stochastic/Distribution/LaplaceDistribution.php +++ b/Math/Stochastic/Distribution/LaplaceDistribution.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class LaplaceDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function. * @@ -129,7 +139,7 @@ final class LaplaceDistribution /** * Get moment generating function. * - * @param float $t Valute t + * @param float $t Value t * @param float $mu Mean * @param float $b Value b * diff --git a/Math/Stochastic/Distribution/LogDistribution.php b/Math/Stochastic/Distribution/LogDistribution.php index e65b3c3ca..043333726 100755 --- a/Math/Stochastic/Distribution/LogDistribution.php +++ b/Math/Stochastic/Distribution/LogDistribution.php @@ -34,6 +34,16 @@ final class LogDistribution */ public const EPSILON = 4.88e-04; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability mass function. * diff --git a/Math/Stochastic/Distribution/LogNormalDistribution.php b/Math/Stochastic/Distribution/LogNormalDistribution.php index 84056f43b..0644e0f20 100755 --- a/Math/Stochastic/Distribution/LogNormalDistribution.php +++ b/Math/Stochastic/Distribution/LogNormalDistribution.php @@ -26,6 +26,16 @@ use phpOMS\Math\Functions\Functions; */ final class LogNormalDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function. * diff --git a/Math/Stochastic/Distribution/LogisticDistribution.php b/Math/Stochastic/Distribution/LogisticDistribution.php index a2f4bf45f..632f85fb2 100755 --- a/Math/Stochastic/Distribution/LogisticDistribution.php +++ b/Math/Stochastic/Distribution/LogisticDistribution.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class LogisticDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function. * diff --git a/Math/Stochastic/Distribution/NormalDistribution.php b/Math/Stochastic/Distribution/NormalDistribution.php index 5e8549f7b..22724e598 100755 --- a/Math/Stochastic/Distribution/NormalDistribution.php +++ b/Math/Stochastic/Distribution/NormalDistribution.php @@ -39,6 +39,16 @@ final class NormalDistribution '0.85' => 1.44, '0.90' => 1.64, '0.95' => 1.96, '0.96' => 2.05, '0.97' => 2.17, '0.98' => 2.33, '0.99' => 2.58, ]; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Calculate the sample size * diff --git a/Math/Stochastic/Distribution/ParetoDistribution.php b/Math/Stochastic/Distribution/ParetoDistribution.php index ecf64624f..831cd1da5 100755 --- a/Math/Stochastic/Distribution/ParetoDistribution.php +++ b/Math/Stochastic/Distribution/ParetoDistribution.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class ParetoDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function. * diff --git a/Math/Stochastic/Distribution/PoissonDistribution.php b/Math/Stochastic/Distribution/PoissonDistribution.php index 7d8579cc8..ca419760e 100755 --- a/Math/Stochastic/Distribution/PoissonDistribution.php +++ b/Math/Stochastic/Distribution/PoissonDistribution.php @@ -27,6 +27,16 @@ use phpOMS\Math\Functions\Gamma; */ final class PoissonDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get density. * diff --git a/Math/Stochastic/Distribution/TDistribution.php b/Math/Stochastic/Distribution/TDistribution.php index fd58b2fab..3b0cafd28 100755 --- a/Math/Stochastic/Distribution/TDistribution.php +++ b/Math/Stochastic/Distribution/TDistribution.php @@ -72,6 +72,16 @@ final class TDistribution 1000 => ['0' => 0.000, '0.5' => 0.675, '0.6' => 0.842, '0.7' => 1.037, '0.8' => 1.282, '0.9' => 1.646, '0.95' => 1.962, '0.98' => 2.330, '0.99' => 2.581, '0.998' => 3.098, '0.999' => 3.300,], ]; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get expected value. * @@ -200,8 +210,8 @@ final class TDistribution $sum = $term; while ($i < $degrees) { $term *= $cos ** 2 * ($i - 1) / $i; - $sum += $term; - $i += 2; + $sum += $term; + $i += 2; } $sum *= $sin; diff --git a/Math/Stochastic/Distribution/UniformDistributionContinuous.php b/Math/Stochastic/Distribution/UniformDistributionContinuous.php index 7bc46b163..f931b29b4 100755 --- a/Math/Stochastic/Distribution/UniformDistributionContinuous.php +++ b/Math/Stochastic/Distribution/UniformDistributionContinuous.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class UniformDistributionContinuous { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get mode. * diff --git a/Math/Stochastic/Distribution/UniformDistributionDiscrete.php b/Math/Stochastic/Distribution/UniformDistributionDiscrete.php index c8099575f..eba8982d2 100755 --- a/Math/Stochastic/Distribution/UniformDistributionDiscrete.php +++ b/Math/Stochastic/Distribution/UniformDistributionDiscrete.php @@ -24,6 +24,16 @@ namespace phpOMS\Math\Stochastic\Distribution; */ final class UniformDistributionDiscrete { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability mass function. * diff --git a/Math/Stochastic/Distribution/WeibullDistribution.php b/Math/Stochastic/Distribution/WeibullDistribution.php index fc3159e27..dc439fa5b 100755 --- a/Math/Stochastic/Distribution/WeibullDistribution.php +++ b/Math/Stochastic/Distribution/WeibullDistribution.php @@ -26,6 +26,16 @@ use phpOMS\Math\Functions\Gamma; */ final class WeibullDistribution { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get probability density function. * diff --git a/Math/Stochastic/Distribution/ZTesting.php b/Math/Stochastic/Distribution/ZTesting.php index 1edbc1500..c50ca5ca5 100755 --- a/Math/Stochastic/Distribution/ZTesting.php +++ b/Math/Stochastic/Distribution/ZTesting.php @@ -31,15 +31,31 @@ use phpOMS\Math\Statistic\MeasureOfDispersion; */ final class ZTesting { + /** + * Percentile table + * + * @var array + * @since 1.0.0 + */ public const TABLE = [ - '2.58' => 0.99, - '2.33' => 0.98, - '1.96' => 0.95, - '1.64' => 0.90, - '1.44' => 0.85, - '1.28' => 0.80, + '0.99' => 2.58, + '0.98' => 2.33, + '0.95' => 1.96, + '0.90' => 1.64, + '0.85' => 1.44, + '0.80' => 1.28, ]; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Test hypthesis. * @@ -58,8 +74,8 @@ final class ZTesting $zSignificance = 0.0; foreach (self::TABLE as $key => $value) { - if ($significance === $value) { - $zSignificance = (float) $key; + if ($significance === (float) $key) { + $zSignificance = $value; } } @@ -77,7 +93,7 @@ final class ZTesting * * @since 1.0.0 */ - public static function zTest(float $value, array $data, float $sigma = null) : float + public static function zTest(float $value, array $data, ?float $sigma = null) : float { $sigma ??= MeasureOfDispersion::standardDeviationSample($data); diff --git a/Math/Stochastic/NaiveBayesClassifier.php b/Math/Stochastic/NaiveBayesClassifier.php index b42e3af6c..eda2acc0e 100755 --- a/Math/Stochastic/NaiveBayesClassifier.php +++ b/Math/Stochastic/NaiveBayesClassifier.php @@ -28,7 +28,7 @@ use phpOMS\Math\Statistic\MeasureOfDispersion; final class NaiveBayesClassifier { /** - * Dictionary of different criterias. + * Dictionary of different criteria. * * @var array * @since 1.0.0 @@ -70,15 +70,15 @@ final class NaiveBayesClassifier foreach ($dataset as $attr => $value) { if (!isset($this->dict[$criteria][$attr])) { $this->dict[$criteria][$attr] = [ - 'type' => \is_array($value) ? 1 : 2, - 'data' => [], + 'type' => \is_array($value) ? 1 : 2, + 'data' => [], ]; } if (!isset($this->probabilities['attr'][$attr])) { $this->probabilities['attr'][$attr] = [ - 'count' => 0, - 'data' => [], + 'count' => 0, + 'data' => [], ]; } @@ -117,7 +117,7 @@ final class NaiveBayesClassifier * * @param string $criteria Criteria to match against * @param array $toMatch Values to match - * @param int $minimum Minimum amount of ocurances for consideration + * @param int $minimum Minimum amount of occurrences for consideration * * @return float * diff --git a/Math/Topology/Kernel2D.php b/Math/Topology/Kernel2D.php index 1f5f72f65..73bf2b2fa 100644 --- a/Math/Topology/Kernel2D.php +++ b/Math/Topology/Kernel2D.php @@ -17,12 +17,14 @@ namespace phpOMS\Math\Topology; /** * Kernels. * + * The bandwidth in the following functions is equivalent with 2 * sigma. + * * @package phpOMS\Math\Topology * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 */ -final class Kernels2D +final class Kernel2D { /** * Constructor @@ -80,10 +82,10 @@ final class Kernels2D */ public static function epanechnikovKernel(float $distance, float $bandwidth) : float { - if (\abs($distance) <= $bandwidth) { - $u = \abs($distance) / $bandwidth; + if (\abs($distance) <= $bandwidth / 2) { + $u = \abs($distance) / ($bandwidth / 2); - return 0.75 * (1 - $u * $u) / $bandwidth; + return 0.75 * (1 - $u * $u) / ($bandwidth / 2); } else { return 0.0; } @@ -101,10 +103,10 @@ final class Kernels2D */ public static function quarticKernel(float $distance, float $bandwidth) : float { - if (\abs($distance) <= $bandwidth) { - $u = \abs($distance) / $bandwidth; + if (\abs($distance) <= $bandwidth / 2) { + $u = $distance / ($bandwidth / 2); - return (15 / 16) * (1 - $u * $u) * (1 - $u * $u) / $bandwidth; + return (15 / 16) * (1 - $u * $u) * (1 - $u * $u) / ($bandwidth / 2); } else { return 0.0; } @@ -122,10 +124,10 @@ final class Kernels2D */ public static function triweightKernel(float $distance, float $bandwidth) : float { - if (\abs($distance) <= $bandwidth) { - $u = \abs($distance) / $bandwidth; + if (\abs($distance) <= $bandwidth / 2) { + $u = $distance / ($bandwidth / 2); - return (35 / 32) * (1 - $u * $u) * (1 - $u * $u) * (1 - $u * $u) / $bandwidth; + return (35 / 32) * (1 - $u * $u) * (1 - $u * $u) * (1 - $u * $u) / ($bandwidth / 2); } else { return 0.0; } @@ -143,10 +145,10 @@ final class Kernels2D */ public static function tricubeKernel(float $distance, float $bandwidth) : float { - if (\abs($distance) <= $bandwidth) { - $u = \abs($distance) / $bandwidth; + if (\abs($distance) <= $bandwidth / 2) { + $u = \abs($distance) / ($bandwidth / 2); - return (70 / 81) * (1 - $u * $u * $u) * (1 - $u * $u * $u) * (1 - $u * $u * $u) / $bandwidth; + return (70 / 81) * (1 - $u * $u * $u) * (1 - $u * $u * $u) * (1 - $u * $u * $u) / ($bandwidth / 2); } else { return 0.0; } @@ -164,7 +166,7 @@ final class Kernels2D */ public static function gaussianKernel(float $distance, float $bandwidth) : float { - return \exp(-($distance * $distance) / (2 * $bandwidth * $bandwidth)) / ($bandwidth * \sqrt(2 * \M_PI)); + return \exp(-($distance * $distance) / (2 * $bandwidth * $bandwidth / 4)) / ($bandwidth / 2 * \sqrt(2 * \M_PI)); } /** @@ -179,8 +181,8 @@ final class Kernels2D */ public static function cosineKernel(float $distance, float $bandwidth) : float { - return \abs($distance) <= $bandwidth - ? (\M_PI / 4) * \cos(\M_PI * $distance / (2 * $bandwidth)) / $bandwidth + return \abs($distance) <= $bandwidth / 2 + ? \M_PI / 4 * \cos(\M_PI / 2 * ($distance / ($bandwidth / 2))) : 0.0; } @@ -196,6 +198,6 @@ final class Kernels2D */ public static function logisticKernel(float $distance, float $bandwidth) : float { - return 1 / (\exp($distance / $bandwidth) + 2 + \exp(-$distance / $bandwidth)); + return 1 / (\exp($distance / ($bandwidth / 2)) + 2 + \exp(-$distance / ($bandwidth / 2))); } } diff --git a/Math/Topology/KernelsND.php b/Math/Topology/KernelsND.php index 9fbdfcfcb..01e33e7be 100644 --- a/Math/Topology/KernelsND.php +++ b/Math/Topology/KernelsND.php @@ -51,14 +51,14 @@ final class KernelsND { $dim = \count($bandwidths); - $bandwithMatrix = Matrix::fromArray($bandwidths); - $distnaceMatrix = Matrix::fromArray($distances); - $identityMatrix = new IdentityMatrix($dim); + $bandwidthMatrix = Matrix::fromArray($bandwidths); + $distanceMatrix = Matrix::fromArray($distances); + $identityMatrix = new IdentityMatrix($dim); - $cov = $bandwithMatrix->mult($identityMatrix); + $cov = $bandwidthMatrix->mult($identityMatrix); /** @phpstan-ignore-next-line */ - $exponent = $distnaceMatrix->dot($cov->inverse())->mult($distnaceMatrix)->sum(1)->mult(-0.5); + $exponent = $distanceMatrix->mult($cov->inverse())->mult($distanceMatrix)->sum(1)->mult(-0.5); return $exponent->exp()->mult((1 / \pow(2 * \M_PI, $dim / 2)) * \pow($cov->det(), 0.5))->matrix; } diff --git a/Math/Topology/MetricsND.php b/Math/Topology/MetricsND.php index fd36db66f..d6097d1d1 100755 --- a/Math/Topology/MetricsND.php +++ b/Math/Topology/MetricsND.php @@ -106,13 +106,13 @@ final class MetricsND */ public static function cosine(array $a, array $b) : float { - if (($length = \count($a)) !== \count($b)) { + if (\count($a) !== \count($b)) { throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); } $dotProduct = 0; - for ($i = 0; $i < $length; ++$i) { - $dotProduct += $a[$i] * $b[$i]; + foreach ($a as $id => $_) { + $dotProduct += $a[$id] * $b[$id]; } $sumOfSquares = 0; diff --git a/Message/Cli/CliHeader.php b/Message/Cli/CliHeader.php index 0a7f4e280..e1b47a860 100755 --- a/Message/Cli/CliHeader.php +++ b/Message/Cli/CliHeader.php @@ -129,7 +129,7 @@ final class CliHeader extends HeaderAbstract * * @since 1.0.0 */ - public function get(string $key = null) : array + public function get(?string $key = null) : array { return $key === null ? $this->header : ($this->header[\strtolower($key)] ?? []); } diff --git a/Message/Cli/CliRequest.php b/Message/Cli/CliRequest.php index eb57db07e..b4ab87d0c 100755 --- a/Message/Cli/CliRequest.php +++ b/Message/Cli/CliRequest.php @@ -66,7 +66,7 @@ final class CliRequest extends RequestAbstract * * @since 1.0.0 */ - public function __construct(UriInterface $uri = null, Localization $l11n = null) + public function __construct(?UriInterface $uri = null, ?Localization $l11n = null) { $this->header = new CliHeader(); $this->header->l11n = $l11n ?? new Localization(); @@ -85,7 +85,7 @@ final class CliRequest extends RequestAbstract * * @since 1.0.0 */ - public function getData(string $key = null, string $type = null) : mixed + public function getData(?string $key = null, ?string $type = null) : mixed { if ($key === null) { return $this->data; diff --git a/Message/Cli/CliResponse.php b/Message/Cli/CliResponse.php index 580357a9f..3be1e9887 100755 --- a/Message/Cli/CliResponse.php +++ b/Message/Cli/CliResponse.php @@ -48,7 +48,7 @@ final class CliResponse extends ResponseAbstract implements RenderableInterface * * @since 1.0.0 */ - public function __construct(Localization $l11n = null) + public function __construct(?Localization $l11n = null) { $this->header = new CliHeader(); $this->header->l11n = $l11n ?? new Localization(); diff --git a/Message/HeaderAbstract.php b/Message/HeaderAbstract.php index 7f3ae51c0..aabffba6e 100755 --- a/Message/HeaderAbstract.php +++ b/Message/HeaderAbstract.php @@ -143,7 +143,7 @@ abstract class HeaderAbstract * * @since 1.0.0 */ - abstract public function get(string $key = null) : array; + abstract public function get(?string $key = null) : array; /** * Header has key? diff --git a/Message/Http/HttpHeader.php b/Message/Http/HttpHeader.php index 100857e5c..6192a5dee 100755 --- a/Message/Http/HttpHeader.php +++ b/Message/Http/HttpHeader.php @@ -290,7 +290,7 @@ final class HttpHeader extends HeaderAbstract /** * {@inheritdoc} */ - public function get(string $key = null) : array + public function get(?string $key = null) : array { return $key === null ? $this->header : ($this->header[\strtolower($key)] ?? []); } diff --git a/Message/Http/HttpRequest.php b/Message/Http/HttpRequest.php index e7ae85f16..361e422d8 100755 --- a/Message/Http/HttpRequest.php +++ b/Message/Http/HttpRequest.php @@ -77,7 +77,7 @@ final class HttpRequest extends RequestAbstract * * @since 1.0.0 */ - public function __construct(UriInterface $uri = null, Localization $l11n = null) + public function __construct(?UriInterface $uri = null, ?Localization $l11n = null) { $this->header = new HttpHeader(); $this->header->l11n = $l11n ?? new Localization(); @@ -115,8 +115,8 @@ final class HttpRequest extends RequestAbstract $this->data = $_POST + $_GET; $this->files = $_FILES; $this->header->initCurrentRequest(); - $this->header->l11n->setLanguage($this->getRequestLanguage()); - $this->header->l11n->setCountry($this->getRequestCountry()); + $this->header->l11n->language = $this->getRequestLanguage(); + $this->header->l11n->country = $this->getRequestCountry(); $this->initNonGetData(); } @@ -156,7 +156,7 @@ final class HttpRequest extends RequestAbstract } $input .= $lineRaw; - $size += \strlen($lineRaw); + $size += \strlen($lineRaw); } \fclose($stream); @@ -192,7 +192,7 @@ final class HttpRequest extends RequestAbstract } $content .= $lineRaw; - $size += \strlen($lineRaw); + $size += \strlen($lineRaw); } \fclose($stream); @@ -220,9 +220,7 @@ final class HttpRequest extends RequestAbstract // @codeCoverageIgnoreStart // Tested but coverage doesn't show up if (\str_starts_with($lineRaw, '--')) { - if ($boundary === null) { - $boundary = \rtrim($lineRaw); - } + $boundary ??= \rtrim($lineRaw); continue; } @@ -447,7 +445,7 @@ final class HttpRequest extends RequestAbstract */ public static function createFromSuperglobals() : self { - $request = new self(); + $request = new self(); $request->initRequest(); return $request; @@ -464,7 +462,7 @@ final class HttpRequest extends RequestAbstract */ public function setUri(UriInterface $uri) : void { - $this->uri = $uri; + $this->uri = $uri; $this->data += $uri->getQueryArray(); } diff --git a/Message/Http/HttpResponse.php b/Message/Http/HttpResponse.php index 6b72923f5..4fbe034fd 100755 --- a/Message/Http/HttpResponse.php +++ b/Message/Http/HttpResponse.php @@ -42,7 +42,7 @@ final class HttpResponse extends ResponseAbstract implements RenderableInterface * * @since 1.0.0 */ - public function __construct(Localization $l11n = null) + public function __construct(?Localization $l11n = null) { $this->header = new HttpHeader(); $this->header->l11n = $l11n ?? new Localization(); @@ -103,7 +103,7 @@ final class HttpResponse extends ResponseAbstract implements RenderableInterface /** * Generate response based on header. * - * @param mixed ...$data Data passt to render function. (0 => bool: $optimize) + * @param mixed ...$data Data passed to render function. (0 => bool: $optimize) * * @return string * @@ -141,7 +141,7 @@ final class HttpResponse extends ResponseAbstract implements RenderableInterface { $render = ''; foreach ($this->data as $response) { - // @note: Api functions return void -> null, this is where the null value is "ignored"/rendered as '' + // @note Api functions return void -> null, this is where the null value is "ignored"/rendered as '' $render .= StringUtils::stringify($response); } diff --git a/Message/Http/Rest.php b/Message/Http/Rest.php index 20e96aa53..7c4efb92d 100755 --- a/Message/Http/Rest.php +++ b/Message/Http/Rest.php @@ -66,21 +66,22 @@ final class Rest break; case RequestMethod::POST: \curl_setopt($curl, \CURLOPT_CUSTOMREQUEST, 'POST'); + \curl_setopt($curl, \CURLOPT_POST, 1); break; case RequestMethod::PUT: \curl_setopt($curl, \CURLOPT_CUSTOMREQUEST, 'PUT'); + \curl_setopt($curl, \CURLOPT_POST, 1); break; case RequestMethod::DELETE: \curl_setopt($curl, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + \curl_setopt($curl, \CURLOPT_POST, 1); break; } // handle none-get - if ($request->getMethod() !== RequestMethod::GET) { - \curl_setopt($curl, \CURLOPT_POST, 1); - + if ($request->getMethod() !== RequestMethod::GET && !empty($request->data)) { // handle different content types - $contentType = $requestHeaders['Content-Type'] ?? []; + $contentType = $request->header->get('content-type'); if (empty($contentType) || \in_array(MimeType::M_POST, $contentType)) { /* @phpstan-ignore-next-line */ \curl_setopt($curl, \CURLOPT_POSTFIELDS, \http_build_query($request->data)); @@ -89,11 +90,12 @@ final class Rest } elseif (\in_array(MimeType::M_MULT, $contentType)) { $boundary = '----' . \uniqid(); - /* @phpstan-ignore-next-line */ $data = self::createMultipartData($boundary, $request->data); - // @todo: Replace boundary/ with the correct boundary= in the future. - // Currently this cannot be done due to a bug. If we do it now the server cannot correclty populate php://input + // @todo Replace boundary/ with the correct boundary= in the future. + // Currently this cannot be done due to a bug. + // If we do it now the server cannot correctly populate php://input + // https://github.com/Karaka-Management/phpOMS/issues/345 $headers['Content-Type'] = 'Content-Type: multipart/form-data; boundary/' . $boundary; $headers['content-length'] = 'Content-Length: ' . \strlen($data); @@ -112,7 +114,7 @@ final class Rest $response = new HttpResponse(); \curl_setopt($curl, \CURLOPT_HEADERFUNCTION, - function($curl, $header) use ($response, &$cHeaderString) { + function($_, $header) use ($response, &$cHeaderString) { $cHeaderString .= $header; $length = \strlen($header); diff --git a/Message/Mail/Email.php b/Message/Mail/Email.php index 5ef5d13f2..389258b2f 100755 --- a/Message/Mail/Email.php +++ b/Message/Mail/Email.php @@ -118,6 +118,14 @@ class Email implements MessageInterface */ public string $mailer = SubmitType::MAIL; + /** + * Template strings + * + * @var array + * @since 1.0.0 + */ + public array $template = []; + /** * Mail from. * @@ -615,6 +623,20 @@ class Email implements MessageInterface return $addresses; } + public function parseTemplate() : void + { + if (empty($this->template)) { + return; + } + + $keys = \array_keys($this->template); + $values = \array_values($this->template); + + $this->subject = \str_replace($keys, $values, $this->subject); + $this->body = \str_replace($keys, $values, $this->body); + $this->bodyAlt = \str_replace($keys, $values, $this->bodyAlt); + } + /** * Pre-send preparations * @@ -626,12 +648,20 @@ class Email implements MessageInterface */ public function preSend(string $mailer) : bool { + if (empty($this->from) + || (empty($this->to) && empty($this->cc) && empty($this->bcc)) + ) { + return false; + } + $this->header = ''; $this->mailer = $mailer; - if (empty($this->to) && empty($this->cc) && empty($this->bcc)) { - return false; - } + $tempSubject = $this->subject; + $tempBody = $this->body; + $tempBodyAlt = $this->bodyAlt; + + $this->parseTemplate(); if (!empty($this->bodyAlt)) { $this->contentType = MimeType::M_ALT; @@ -642,9 +672,9 @@ class Email implements MessageInterface $this->headerMime = ''; $this->bodyMime = $this->createBody(); - $tempheaders = $this->headerMime; - $this->headerMime = $this->createHeader(); - $this->headerMime .= $tempheaders; + $tempHeaders = $this->headerMime; + $this->headerMime = $this->createHeader(); + $this->headerMime .= $tempHeaders; if ($this->mailer === SubmitType::MAIL) { $this->header .= empty($this->to) @@ -674,6 +704,10 @@ class Email implements MessageInterface self::normalizeBreaks($headerDkim, self::$LE) . self::$LE; } + $this->subject = $tempSubject; + $this->body = $tempBody; + $this->bodyAlt = $tempBodyAlt; + return true; } @@ -830,7 +864,7 @@ class Email implements MessageInterface break; default: // Catches case 'plain': and case '': - $result .= 'Content-Type: ' . $this->contentType . '; charset=' . $this->charset . self::$LE; + $result .= 'Content-Type: ' . $this->contentType . '; charset=' . $this->charset . self::$LE; $isMultipart = false; break; } @@ -1087,7 +1121,7 @@ class Email implements MessageInterface default: // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types $this->encoding = $bodyEncoding; - $body .= $this->encodeString($this->body, $this->encoding); + $body .= $this->encodeString($this->body, $this->encoding); break; } @@ -1129,9 +1163,9 @@ class Email implements MessageInterface \unlink($signed); //The message returned by openssl contains both headers and body, so need to split them up - $parts = \explode("\n\n", $body, 2); + $parts = \explode("\n\n", $body, 2); $this->headerMime .= $parts[0] . self::$LE . self::$LE; - $body = $parts[1]; + $body = $parts[1]; } return $body; @@ -1510,8 +1544,8 @@ class Email implements MessageInterface $len -= 2; } - $part = \substr($word, 0, $len); - $word = \substr($word, $len); + $part = \substr($word, 0, $len); + $word = \substr($word, $len); $buf .= ' ' . $part; $message .= $buf . \sprintf('=%s', self::$LE); } else { @@ -1553,7 +1587,7 @@ class Email implements MessageInterface $buf .= $word; if ($bufO !== '' && \strlen($buf) > $length) { $message .= $bufO . $softBreak; - $buf = $word; + $buf = $word; } } @@ -1959,7 +1993,7 @@ class Email implements MessageInterface * * @since 1.0.0 */ - public function addCustomHeader(string $name, string $value = null) : bool + public function addCustomHeader(string $name, ?string $value = null) : bool { $name = \trim($name); $value = \trim($value); @@ -2003,7 +2037,7 @@ class Email implements MessageInterface * * @since 1.0.0 */ - public function msgHTML(string $message, string $basedir = '', \Closure $advanced = null) + public function msgHTML(string $message, string $basedir = '', ?\Closure $advanced = null) { \preg_match_all('/(?= 0x21) && ($ord <= 0x3A)) || $ord === 0x3C || (($ord >= 0x3E) && ($ord <= 0x7E)) ? $txt[$i] : '=' . \sprintf('%02X', $ord); diff --git a/Message/Mail/Imap.php b/Message/Mail/Imap.php index e9823357a..9d8bbab00 100755 --- a/Message/Mail/Imap.php +++ b/Message/Mail/Imap.php @@ -21,6 +21,9 @@ namespace phpOMS\Message\Mail; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Continue implementation of email sending and receiving + * https://github.com/Karaka-Management/phpOMS/issues/258 */ class Imap implements MailBoxInterface { @@ -91,7 +94,7 @@ class Imap implements MailBoxInterface $this->password = $pass; $this->port = $port; $this->encryption = $encryption; - $this->flags .= $this->encryption !== EncryptionType::NONE ? '/ssl' : ''; + $this->flags .= $this->encryption !== EncryptionType::NONE ? '/ssl' : ''; } /** @@ -254,9 +257,9 @@ class Imap implements MailBoxInterface string $cc = '', string $from = '', string $bcc = '', - \DateTime $before = null, - \DateTime $since = null, - \DateTime $on = null, + ?\DateTime $before = null, + ?\DateTime $since = null, + ?\DateTime $on = null, bool $deleted = false, bool $flagged = false ) : array diff --git a/Message/Mail/MailBoxInterface.php b/Message/Mail/MailBoxInterface.php index 05e0c10c6..021d279a5 100755 --- a/Message/Mail/MailBoxInterface.php +++ b/Message/Mail/MailBoxInterface.php @@ -76,7 +76,7 @@ interface MailBoxInterface public function countUnseen(string $box) : int; /** - * Get messages by search criterium + * Get messages by search criteria * * @param string $box Box to count the mail in * @param string $subject Subject @@ -86,7 +86,7 @@ interface MailBoxInterface * @param string $from From * @param string $bcc BCC * @param \DateTime $before Message before - * @param \DateTime $sicne Message since + * @param \DateTime $since Message since * @param \DateTime $on Message on date * @param bool $deleted Message is deleted * @param bool $flagged Message is flagged (false = any message) @@ -103,9 +103,9 @@ interface MailBoxInterface string $cc = '', string $from = '', string $bcc = '', - \DateTime $before = null, - \DateTime $since = null, - \DateTime $on = null, + ?\DateTime $before = null, + ?\DateTime $since = null, + ?\DateTime $on = null, bool $deleted = false, bool $flagged = false ) : array; diff --git a/Message/Mail/MailHandler.php b/Message/Mail/MailHandler.php index d073444bd..5baabf506 100755 --- a/Message/Mail/MailHandler.php +++ b/Message/Mail/MailHandler.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace phpOMS\Message\Mail; +use phpOMS\Security\Guard; use phpOMS\System\SystemUtils; use phpOMS\Utils\StringUtils; use phpOMS\Validation\Network\Email as EmailValidator; @@ -322,7 +323,7 @@ class MailHandler $header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE; // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. - if (!empty($mail->sender) && StringUtils::isShellSafe($mail->sender)) { + if (!empty($mail->sender) && Guard::isShellSafe($mail->sender)) { $mailerToolFmt = $this->mailer === SubmitType::QMAIL ? '%s -f%s' : '%s -oi -f%s -t'; @@ -372,7 +373,7 @@ class MailHandler $params = null; if (!empty($mail->sender) && EmailValidator::isValid($mail->sender) - && StringUtils::isShellSafe($mail->sender) + && Guard::isShellSafe($mail->sender) ) { $params = \sprintf('-f%s', $mail->sender); } @@ -405,7 +406,7 @@ class MailHandler * * @since 1.0.0 */ - private function mailPassthru(string $to, Email $mail, string $header, string $params = null) : bool + private function mailPassthru(string $to, Email $mail, string $header, ?string $params = null) : bool { $subject = $mail->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $mail->subject))); @@ -477,19 +478,15 @@ class MailHandler * * @since 1.0.0 */ - public function smtpConnect(array $options = null) : bool + public function smtpConnect(?array $options = null) : bool { - if ($this->smtp === null) { - $this->smtp = new Smtp(); - } + $this->smtp ??= new Smtp(); if ($this->smtp->isConnected()) { return true; } - if ($options === null) { - $options = $this->smtpOptions; - } + $options ??= $this->smtpOptions; $this->smtp->timeout = $this->timeout; $this->smtp->doVerp = $this->useVerp; diff --git a/Message/Mail/Pop3.php b/Message/Mail/Pop3.php index e56be9721..760b734fd 100755 --- a/Message/Mail/Pop3.php +++ b/Message/Mail/Pop3.php @@ -21,6 +21,9 @@ namespace phpOMS\Message\Mail; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Continue implementation of email sending and receiving + * https://github.com/Karaka-Management/phpOMS/issues/258 */ class Pop3 implements MailBoxInterface { @@ -250,9 +253,9 @@ class Pop3 implements MailBoxInterface string $cc = '', string $from = '', string $bcc = '', - \DateTime $before = null, - \DateTime $since = null, - \DateTime $on = null, + ?\DateTime $before = null, + ?\DateTime $since = null, + ?\DateTime $on = null, bool $deleted = false, bool $flagged = false ) : array diff --git a/Message/Mail/Smtp.php b/Message/Mail/Smtp.php index c1496aaca..339949152 100755 --- a/Message/Mail/Smtp.php +++ b/Message/Mail/Smtp.php @@ -163,9 +163,7 @@ class Smtp protected function getSMTPConnection(string $host, int $port = 25, int $timeout = 30, array $options = []) : mixed { static $streamok; - if ($streamok === null) { - $streamok = \function_exists('stream_socket_client'); - } + $streamok ??= \function_exists('stream_socket_client'); $errno = 0; $errstr = ''; @@ -460,9 +458,9 @@ class Smtp } } - $tmpTimeLimit = $this->timeLimit; + $tmpTimeLimit = $this->timeLimit; $this->timeLimit *= 2; - $result = $this->sendCommand('DATA END', '.', [250]); + $result = $this->sendCommand('DATA END', '.', [250]); $this->recordLastTransactionId(); @@ -833,7 +831,7 @@ class Smtp } } - $str = \fgets($this->con, self::MAX_REPLY_LENGTH); + $str = \fgets($this->con, self::MAX_REPLY_LENGTH); $data .= $str; // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled), diff --git a/Message/RequestAbstract.php b/Message/RequestAbstract.php index ee15599bc..2fb181750 100755 --- a/Message/RequestAbstract.php +++ b/Message/RequestAbstract.php @@ -92,7 +92,7 @@ abstract class RequestAbstract implements MessageInterface * * @since 1.0.0 */ - public function getData(string $key = null, string $type = null) : mixed + public function getData(?string $key = null, ?string $type = null) : mixed { if ($key === null) { return $this->data; @@ -215,6 +215,49 @@ abstract class RequestAbstract implements MessageInterface : new \DateTime((string) $this->data[$key]); } + /** + * Get data. + * + * @param string $key Data key + * + * @return null|\DateTime + * + * @since 1.0.0 + */ + public function getDataDateTimeFromTimestamp(string $key) : ?\DateTime + { + $key = \mb_strtolower($key); + + if (empty($this->data[$key] ?? null)) { + return null; + } + + $dt = new \DateTime(); + $dt->setTimestamp((int) $this->data[$key]); + + return $dt; + } + + /** + * Get data. + * + * @param string $key Data key + * + * @return null|int + * + * @since 1.0.0 + */ + public function getDataTimestampFromDateTime(string $key) : ?int + { + $key = \mb_strtolower($key); + + $timestamp = empty($this->data[$key] ?? null) + ? null + : (int) \strtotime((string) $this->data[$key]); + + return $timestamp === false ? null : $timestamp; + } + /** * Get data. * @@ -232,9 +275,7 @@ abstract class RequestAbstract implements MessageInterface } $json = \json_decode($this->data[$key], true); /** @phpstan-ignore-line */ - if ($json === null) { - $json = $this->data[$key]; - } + $json ??= $this->data[$key]; return \is_array($json) ? $json : [$json]; } diff --git a/Message/ResponseAbstract.php b/Message/ResponseAbstract.php index 16aa7f393..40c8eb54c 100755 --- a/Message/ResponseAbstract.php +++ b/Message/ResponseAbstract.php @@ -51,7 +51,7 @@ abstract class ResponseAbstract implements \JsonSerializable, MessageInterface * * @since 1.0.0 */ - public function getData(mixed $key, string $type = null) : mixed + public function getData(mixed $key = null, ?string $type = null) : mixed { if ($key === null) { return $this->data; @@ -309,6 +309,25 @@ abstract class ResponseAbstract implements \JsonSerializable, MessageInterface $this->data[$key] = $response; } + /** + * Add response. + * + * @param mixed $key Response id + * @param mixed $response Response to add + * + * @return void + * + * @since 1.0.0 + */ + public function add(mixed $key, mixed $response) : void + { + if (!isset($this->data[$key])) { + $this->data[$key] = []; + } + + $this->data[$key][] = $response; + } + /** * {@inheritdoc} */ diff --git a/Message/Socket/PacketManager.php b/Message/Socket/PacketManager.php index 2d8d90d9a..2715a3e29 100755 --- a/Message/Socket/PacketManager.php +++ b/Message/Socket/PacketManager.php @@ -72,12 +72,12 @@ class PacketManager public function handle(string $data, $client) : void { $request = new SocketRequest(); - $request->header->account = $client->getAccount()->getId(); + $request->header->account = $client->account->id; $response = new SocketResponse(); $this->dispatcher->dispatch( - $this->router->route($data, null, RouteVerb::ANY, 2, 1, $client->getAccount()), + $this->router->route($data, null, RouteVerb::ANY, 2, 1, $client->account), $request, $response ); diff --git a/Message/Socket/SocketHeader.php b/Message/Socket/SocketHeader.php index e2bf15332..5505ca499 100755 --- a/Message/Socket/SocketHeader.php +++ b/Message/Socket/SocketHeader.php @@ -249,7 +249,7 @@ class SocketHeader extends HeaderAbstract implements SerializableInterface /** * {@inheritdoc} */ - public function get(string $key = null) : array + public function get(?string $key = null) : array { return $key === null ? $this->header : ($this->header[\strtolower($key)] ?? []); } diff --git a/Model/Html/Head.php b/Model/Html/Head.php index e072d6c55..9607ea11e 100755 --- a/Model/Html/Head.php +++ b/Model/Html/Head.php @@ -36,7 +36,7 @@ final class Head implements RenderableInterface * @var string * @since 1.0.0 */ - private string $language = ISO639x1Enum::_EN; + public string $language = ISO639x1Enum::_EN; /** * Page title. @@ -167,7 +167,7 @@ final class Head implements RenderableInterface */ public function render(mixed ...$data) : string { - $head = ''; + $head = ''; $head .= $this->meta->render(); $head .= $this->renderAssets(); $head .= empty($this->style) ? '' : ''; diff --git a/Module/Exception/InvalidModuleException.php b/Module/Exception/InvalidModuleException.php index 23562458d..a86b75ab6 100755 --- a/Module/Exception/InvalidModuleException.php +++ b/Module/Exception/InvalidModuleException.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace phpOMS\Module\Exception; /** - * Zero devision exception. + * Zero division exception. * * @package phpOMS\Module\Exception * @license OMS License 2.0 @@ -33,7 +33,7 @@ final class InvalidModuleException extends \UnexpectedValueException * * @since 1.0.0 */ - public function __construct(string $message, int $code = 0, \Exception $previous = null) + public function __construct(string $message, int $code = 0, ?\Exception $previous = null) { parent::__construct('Data for module "' . $message . '" could be found.', $code, $previous); } diff --git a/Module/Exception/InvalidThemeException.php b/Module/Exception/InvalidThemeException.php index 82f62f428..a7d47c889 100755 --- a/Module/Exception/InvalidThemeException.php +++ b/Module/Exception/InvalidThemeException.php @@ -33,7 +33,7 @@ final class InvalidThemeException extends \UnexpectedValueException * * @since 1.0.0 */ - public function __construct(string $message, int $code = 0, \Exception $previous = null) + public function __construct(string $message, int $code = 0, ?\Exception $previous = null) { parent::__construct('Data for theme "' . $message . '" could be found.', $code, $previous); } diff --git a/Module/InstallerAbstract.php b/Module/InstallerAbstract.php index 316520c04..8e14c189b 100755 --- a/Module/InstallerAbstract.php +++ b/Module/InstallerAbstract.php @@ -132,7 +132,7 @@ abstract class InstallerAbstract * * @since 1.0.0 */ - public static function reInit(ModuleInfo $info, ApplicationInfo $appInfo = null) : void + public static function reInit(ModuleInfo $info, ?ApplicationInfo $appInfo = null) : void { if (($path = \realpath(static::PATH)) === false) { return; // @codeCoverageIgnore diff --git a/Module/ModuleAbstract.php b/Module/ModuleAbstract.php index 99b64a168..a32f9092b 100755 --- a/Module/ModuleAbstract.php +++ b/Module/ModuleAbstract.php @@ -30,6 +30,10 @@ use phpOMS\Utils\StringUtils; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @performance The modules use the module name for identification in many places + * where the module id should be used for performance reasons + * https://github.com/Karaka-Management/Karaka/issues/159 */ abstract class ModuleAbstract { @@ -108,7 +112,7 @@ abstract class ModuleAbstract /** * Auditor for logging. * - * @var null|ModuleAbstract + * @var null|\Modules\Auditor\Controller\ApiController * @since 1.0.0 */ public static ?ModuleAbstract $auditor = null; @@ -120,7 +124,7 @@ abstract class ModuleAbstract * * @since 1.0.0 */ - public function __construct(ApplicationAbstract $app = null) + public function __construct(?ApplicationAbstract $app = null) { $this->app = $app ?? new class() extends ApplicationAbstract {}; @@ -290,6 +294,39 @@ abstract class ModuleAbstract ]; } + /** + * Create standard model background process response. + * + * The response object contains the following data: + * + * * status = Response status + * * title = Response title (e.g. for frontend reporting) + * * message = Response message (e.g. for frontend reporting) + * * response = Response object (e.g. for validation/frontend reporting/form validation) + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param mixed $obj Response object + * + * @return void + * + * @since 1.0.0 + */ + public function createStandardBackgroundResponse( + RequestAbstract $request, + ResponseAbstract $response, + mixed $obj + ) : void + { + $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true); + $response->data[$request->uri->__toString()] = [ + 'status' => NotificationLevel::INFO, + 'title' => '', + 'message' => $this->app->l11nManager->getText($response->header->l11n->language, '0', '0', 'SuccessfulBackground'), + 'response' => $obj, + ]; + } + /** * Create standard model update response. * @@ -886,6 +923,30 @@ abstract class ModuleAbstract $this->app->eventManager->triggerSimilar('POST:Module:' . $trigger, '', $data); } + /** + * Soft delete a model (only marks model as deleted) + * + * 1. Execute pre DB interaction event + * 2. Set model status to deleted in DB + * 3. Execute post DB interaction event (e.g. generates an audit log) + * + * @param int $account Account id + * @param mixed $obj Response object + * @param string | \Closure $mapper Object mapper + * @param string $trigger Trigger for the event manager + * @param string $ip Ip + * + * @return void + * + * @question Consider to implement softDelete functionality + * https://github.com/Karaka-Management/Karaka/issues/276 + * + * @since 1.0.0 + */ + protected function softDeleteModel(int $account, mixed $obj, string | \Closure $mapper, string $trigger, string $ip) : void + { + } + /** * Create a model relation * @@ -915,6 +976,10 @@ abstract class ModuleAbstract string $ip ) : void { + if (empty($rel1) || empty($rel2)) { + return; + } + $trigger = static::NAME . '-' . $trigger . '-relation-create'; $this->app->eventManager->triggerSimilar('PRE:Module:' . $trigger, '', $rel1); @@ -944,7 +1009,7 @@ abstract class ModuleAbstract * * @param int $account Account id * @param mixed $rel1 Object relation1 - * @param mixed $rel2 Object relation2 + * @param mixed $rel2 Object relation2 (null = remove all related to $rel1) * @param string $mapper Object mapper * @param string $field Relation field * @param string $trigger Trigger for the event manager @@ -959,7 +1024,13 @@ abstract class ModuleAbstract $trigger = static::NAME . '-' . $trigger . '-relation-delete'; $this->app->eventManager->triggerSimilar('PRE:Module:' . $trigger, '', $rel1); - $mapper::remover()->deleteRelationTable($field, \is_array($rel2) ? $rel2 : [$rel2], $rel1); + $mapper::remover()->deleteRelationTable( + $field, + $rel2 === null + ? null + : (\is_array($rel2) ? $rel2 : [$rel2]), + $rel1 + ); $data = [ $account, diff --git a/Module/ModuleInfo.php b/Module/ModuleInfo.php index 3da723c1b..be8553623 100755 --- a/Module/ModuleInfo.php +++ b/Module/ModuleInfo.php @@ -40,7 +40,7 @@ final class ModuleInfo /** * Info data. * - * @var array{name:array{id:int, internal:string, external:string}, category:string, vision:string, requirements:array, creator:array{name:string, website:string}, description:string, directory:string, dependencies:array, providing:array, load:array}|array + * @var array{name:array{id:int, internal:string, external:string}, category:string, vision:string, requirements:array, creator:array{name:string, website:string}, directory:string, dependencies:array, providing:array, load:array}|array * @since 1.0.0 */ private array $info = []; @@ -86,7 +86,7 @@ final class ModuleInfo $contents = \file_get_contents($this->path); - /** @var array{name:array{id:int, internal:string, external:string}, category:string, vision:string, requirements:array, creator:array{name:string, website:string}, description:string, directory:string, dependencies:array, providing:array, load:array} $info */ + /** @var array{name:array{id:int, internal:string, external:string}, category:string, vision:string, requirements:array, creator:array{name:string, website:string}, directory:string, dependencies:array, providing:array, load:array} $info */ $info = \json_decode($contents === false ? '[]' : $contents, true); $this->info = $info === false ? [] : $info; } @@ -134,7 +134,7 @@ final class ModuleInfo /** * Get info data. * - * @return array{name:array{id:int, internal:string, external:string}, category:string, vision:string, requirements:array, creator:array{name:string, website:string}, description:string, directory:string, dependencies:array, providing:array, load:array}|array + * @return array{name:array{id:int, internal:string, external:string}, category:string, vision:string, requirements:array, creator:array{name:string, website:string}, directory:string, dependencies:array, providing:array, load:array}|array * * @since 1.0.0 */ diff --git a/Module/ModuleManager.php b/Module/ModuleManager.php index 5274c9712..2ee5272d8 100755 --- a/Module/ModuleManager.php +++ b/Module/ModuleManager.php @@ -30,6 +30,14 @@ use phpOMS\Module\Exception\InvalidModuleException; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Implement a strategy for managing optional modules (e.g., comment module within the news module). + * Previously, modifications to the Mapper were made (e.g., comments were removed) when the comment module was installed. + * However, this approach is no longer viable. One potential solution is to introduce a separate Mapper + * that is dynamically replaced if the comment module is installed. + * Instead of replacing the entire file, a differential approach should be adopted, where only the ADDED lines are merged. + * Consideration must be given to uninstallation scenarios, as determining precisely what to remove is currently problematic. + * https://github.com/Karaka-Management/Karaka/issues/155 */ final class ModuleManager { @@ -113,7 +121,7 @@ final class ModuleManager * * @since 1.0.0 */ - public function getLanguageFiles(RequestAbstract $request, string $app = null) : array + public function getLanguageFiles(RequestAbstract $request, ?string $app = null) : array { $files = $this->getUriLoad($request); if (!isset($files['5'])) { @@ -249,11 +257,11 @@ final class ModuleManager * * @since 1.0.0 */ - public function isRunning(string $module, string $ctlName = null) : bool + public function isRunning(string $module, ?string $ctlName = null) : bool { $name = '\\Modules\\' . $module . '\\Controller\\' . ($ctlName ?? $this->app->appName) . 'Controller'; - return isset($this->running[$name]); + return isset($this->running[$module][$name]); } /** @@ -475,7 +483,7 @@ final class ModuleManager * * @since 1.0.0 */ - public function reInit(string $module, ApplicationInfo $appInfo = null) : void + public function reInit(string $module, ?ApplicationInfo $appInfo = null) : void { $info = $this->loadInfo($module); if ($info === null) { @@ -646,35 +654,6 @@ final class ModuleManager $class::install($this->app, $this->modulePath); } - /** - * Get module instance. - * - * This also returns inactive or uninstalled modules if they are still in the modules directory. - * - * @param string $module Module name - * @param string $ctlName Controller name (null = current) - * - * @return object|\phpOMS\Module\ModuleAbstract - * - * @todo Remove docblock type hint hack "object". - * The return type object is only used to stop the annoying warning that a method doesn't exist - * if you chain call the methods part of the returned ModuleAbstract implementation. - * Remove it once alternative inline type hinting is possible for the specific returned implementation. - * This also causes phpstan type inspection errors, which we have to live with or ignore in the settings - * - * @since 1.0.0 - */ - public function get(string $module, string $ctlName = null) : ModuleAbstract - { - $name = '\\Modules\\' . $module . '\\Controller\\' . ($ctlName ?? $this->app->appName) . 'Controller'; - if (!isset($this->running[$name])) { - $this->initModuleController($module, $ctlName); - } - - /* @phpstan-ignore-next-line */ - return $this->running[$name] ?? new NullModule(); - } - /** * Initialize module. * @@ -687,47 +666,74 @@ final class ModuleManager * * @since 1.0.0 */ - private function initModuleController(string $module, string $ctlName = null) : void + private function initModuleController(string $module, ?string $ctlName = null) : void { - $name = '\\Modules\\' . $module . '\\Controller\\' . ($ctlName ?? $this->app->appName) . 'Controller'; - $this->running[$name] = $this->getModuleInstance($module, $ctlName); + $name = '\\Modules\\' . $module . '\\Controller\\' . ($ctlName ?? $this->app->appName) . 'Controller'; + $ctrl = $this->get($module, $ctlName); if ($this->app->dispatcher !== null) { - $this->app->dispatcher->set($this->running[$name], $name); + $this->app->dispatcher->set($ctrl, $name); + } + + // Handle providing->receiving + foreach ($this->running as $mName => $controllers) { + $controller = \reset($controllers); + + foreach ($controller::$providing as $providing) { + $ctrl = \reset($this->running[$providing]); + + if (!\in_array($mName, $ctrl->receiving)) { + $ctrl->receiving[] = $mName; + } + } } } /** - * Gets and initializes modules. + * Get module instance. * - * @param string $module Module ID - * @param string $ctlName Controller name (null = current app) + * This also returns inactive or uninstalled modules if they are still in the modules directory. * - * @return ModuleAbstract + * @param string $module Module name + * @param string $ctlName Controller name (null = current) + * + * @return object|\phpOMS\Module\ModuleAbstract + * + * @todo Remove docblock type hint hack "object". + * The return type object is only used to stop the annoying warning that a method doesn't exist + * if you chain call the methods part of the returned ModuleAbstract implementation. + * Remove it once alternative inline type hinting is possible for the specific returned implementation. + * This also causes phpstan type inspection errors, which we have to live with or ignore in the settings + * https://github.com/Karaka-Management/phpOMS/issues/300 * * @since 1.0.0 */ - public function getModuleInstance(string $module, string $ctlName = null) : ModuleAbstract + public function get(string $module, ?string $ctlName = null) : ModuleAbstract { $class = '\\Modules\\' . $module . '\\Controller\\' . ($ctlName ?? $this->app->appName) . 'Controller'; - - if (!isset($this->running[$class])) { - if (Autoloader::exists($class) - || Autoloader::exists($class = '\\Modules\\' . $module . '\\Controller\\Controller') - ) { - try { - /** @var ModuleAbstract $obj */ - $obj = new $class($this->app); - $this->running[$class] = $obj; - } catch (\Throwable $_) { - $this->running[$class] = new NullModule(); - } - } else { - $this->running[$class] = new NullModule(); - } + if (!isset($this->running[$module])) { + $this->running[$module] = []; } - return $this->running[$class]; + if (isset($this->running[$module][$class])) { + return $this->running[$module][$class]; + } + + if (Autoloader::exists($class) + || Autoloader::exists($class = '\\Modules\\' . $module . '\\Controller\\Controller') + ) { + try { + /** @var ModuleAbstract $obj */ + $obj = new $class($this->app); + $this->running[$module][$class] = $obj; + } catch (\Throwable $_) { + $this->running[$module][$class] = new NullModule(); + } + } else { + $this->running[$module][$class] = new NullModule(); + } + + return $this->running[$module][$class]; } /** @@ -740,7 +746,7 @@ final class ModuleManager * * @since 1.0.0 */ - public function initRequestModules(RequestAbstract $request, string $ctlName = null) : void + public function initRequestModules(RequestAbstract $request, ?string $ctlName = null) : void { $toInit = $this->getRoutedModules($request); foreach ($toInit as $module) { diff --git a/Module/NullModule.php b/Module/NullModule.php index 55244dd0e..2cc502da7 100755 --- a/Module/NullModule.php +++ b/Module/NullModule.php @@ -17,7 +17,7 @@ namespace phpOMS\Module; use phpOMS\Log\FileLogger; /** - * Module abstraction class. + * Mull module class. * * @package phpOMS\Module * @license OMS License 2.0 diff --git a/Module/StatusAbstract.php b/Module/StatusAbstract.php index f4109077f..7ec93d6ee 100755 --- a/Module/StatusAbstract.php +++ b/Module/StatusAbstract.php @@ -69,9 +69,9 @@ abstract class StatusAbstract * * @since 1.0.0 */ - public static function activateRoutes(ModuleInfo $info, ApplicationInfo $appInfo = null) : void + public static function activateRoutes(ModuleInfo $info, ?ApplicationInfo $appInfo = null) : void { - self::activateRoutesHooks($info, $appInfo, 'Routes'); + self::activateRoutesHooks($info, 'Routes', $appInfo); } /** @@ -125,9 +125,9 @@ abstract class StatusAbstract * * @since 1.0.0 */ - public static function activateHooks(ModuleInfo $info, ApplicationInfo $appInfo = null) : void + public static function activateHooks(ModuleInfo $info, ?ApplicationInfo $appInfo = null) : void { - self::activateRoutesHooks($info, $appInfo, 'Hooks'); + self::activateRoutesHooks($info, 'Hooks', $appInfo); } /** @@ -140,7 +140,7 @@ abstract class StatusAbstract * * @since 1.0.0 */ - public static function activateRoutesHooks(ModuleInfo $info, ApplicationInfo $appInfo = null, string $type) : void + public static function activateRoutesHooks(ModuleInfo $info, string $type, ?ApplicationInfo $appInfo = null) : void { $directories = new Directory(static::PATH . '/' . $type); @@ -195,9 +195,9 @@ abstract class StatusAbstract * * @since 1.0.0 */ - public static function deactivateRoutes(ModuleInfo $info, ApplicationInfo $appInfo = null) : void + public static function deactivateRoutes(ModuleInfo $info, ?ApplicationInfo $appInfo = null) : void { - self::deactivateRoutesHooks($info, $appInfo, 'Routes'); + self::deactivateRoutesHooks($info, 'Routes', $appInfo); } /** @@ -210,7 +210,7 @@ abstract class StatusAbstract * * @since 1.0.0 */ - public static function deactivateRoutesHooks(ModuleInfo $info, ApplicationInfo $appInfo = null, string $type) : void + public static function deactivateRoutesHooks(ModuleInfo $info, string $type, ?ApplicationInfo $appInfo = null) : void { $directories = new Directory(static::PATH . '/'. $type); @@ -283,8 +283,8 @@ abstract class StatusAbstract * * @since 1.0.0 */ - public static function deactivateHooks(ModuleInfo $info, ApplicationInfo $appInfo = null) : void + public static function deactivateHooks(ModuleInfo $info, ?ApplicationInfo $appInfo = null) : void { - self::deactivateRoutesHooks($info, $appInfo, 'Hooks'); + self::deactivateRoutesHooks($info, 'Hooks', $appInfo); } } diff --git a/README.md b/README.md index a554c0131..2d308fd78 100755 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Currently Karaka is still developing the first Alpha version. As soon as we have General updates can be found in our info section at https://jingga.app/info and developer updates can be found in our developer section at https://jingga.app/dev. In our developer section you can also check out the automatically generated reports such as code coverage, code style, static analysis etc. as well as our code style guide lines and developer documentation. -* [Project Status](https://github.com/Karaka-Management/Organization-Guide/blob/master/Project/PROJECT.md) +* [Project Status](https://github.com/orgs/Karaka-Management/projects/10) ## Tech stack @@ -62,7 +62,7 @@ General updates can be found in our info section at https://jingga.app/info and ## Become a contributor -Karaka has a very open culture and we always welcome new people who share our philosophy in providing create solutions which just work. You can find the development process description which also describes how to become a contributer in the [Organization documentation](https://github.com/Karaka-Management/Organization-Guide/blob/master/Processes/Development.md). +Karaka has a very open culture and we always welcome new people who share our philosophy in providing create solutions which just work. You can find the development process description which also describes how to become a contributer in the [Organization documentation](https://github.com/Karaka-Management/Organization-Guide/blob/master/Processes/01_Development.md). ## Misc diff --git a/Router/RouterInterface.php b/Router/RouterInterface.php index 5e6be5c02..ffcfc013a 100755 --- a/Router/RouterInterface.php +++ b/Router/RouterInterface.php @@ -85,11 +85,11 @@ interface RouterInterface */ public function route( string $uri, - string $csrf = null, + ?string $csrf = null, int $verb = RouteVerb::GET, - int $app = null, - int $unitId = null, - Account $account = null, - array $data = null + ?int $app = null, + ?int $unitId = null, + ?Account $account = null, + ?array $data = null ) : array; } diff --git a/Router/SocketRouter.php b/Router/SocketRouter.php index 718d451f4..a5cd0e9e5 100755 --- a/Router/SocketRouter.php +++ b/Router/SocketRouter.php @@ -110,12 +110,12 @@ final class SocketRouter implements RouterInterface */ public function route( string $uri, - string $csrf = null, + ?string $csrf = null, int $verb = RouteVerb::GET, - int $app = null, - int $unitId = null, - Account $account = null, - array $data = null + ?int $app = null, + ?int $unitId = null, + ?Account $account = null, + ?array $data = null ) : array { $bound = []; @@ -136,7 +136,7 @@ final class SocketRouter implements RouterInterface // if permission check is invalid if (isset($d['permission']) && !empty($d['permission']) - && ($account === null || $account->getId() === 0) + && ($account === null || $account->id === 0) ) { return ['dest' => RouteStatus::NOT_LOGGED_IN]; } elseif (isset($d['permission']) && !empty($d['permission']) diff --git a/Router/WebRouter.php b/Router/WebRouter.php index e5eb36631..fd70cbd83 100755 --- a/Router/WebRouter.php +++ b/Router/WebRouter.php @@ -23,6 +23,13 @@ use phpOMS\Account\Account; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Change the url format in most modules from query parameter to path + * (e.g. `/module/profile?id=Admin` to `/module/Admin/profile`) + * https://github.com/Karaka-Management/Karaka/issues/153 + * + * @todo Instead of doing only regex matching, combine it with a tree search, this should be faster + * https://github.com/Karaka-Management/phpOMS/issues/276 */ final class WebRouter implements RouterInterface { @@ -112,12 +119,12 @@ final class WebRouter implements RouterInterface */ public function route( string $uri, - string $csrf = null, + ?string $csrf = null, int $verb = RouteVerb::GET, - int $app = null, - int $unitId = null, - Account $account = null, - array $data = null + ?int $app = null, + ?int $unitId = null, + ?Account $account = null, + ?array $data = null ) : array { $bound = []; @@ -138,7 +145,7 @@ final class WebRouter implements RouterInterface // if permission check is invalid if (isset($d['permission']) && !empty($d['permission']) - && ($account === null || $account->getId() === 0) + && ($account === null || $account->id === 0) ) { return ['dest' => RouteStatus::NOT_LOGGED_IN]; } elseif (isset($d['permission']) && !empty($d['permission']) diff --git a/Security/Guard.php b/Security/Guard.php index cb80c5cb3..4f9d773bf 100755 --- a/Security/Guard.php +++ b/Security/Guard.php @@ -84,4 +84,34 @@ final class Guard return $data; } + + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * + * @param string $string String to check + * + * @return bool + * + * @since 1.0.0 + */ + public static function isShellSafe(string $string) : bool + { + if (\escapeshellcmd($string) !== $string + || !\in_array(\escapeshellarg($string), ["'{$string}'", "\"{$string}\""]) + ) { + return false; + } + + $length = \strlen($string); + + for ($i = 0; $i < $length; ++$i) { + $c = $string[$i]; + + if (!\ctype_alnum($c) && \strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } } diff --git a/Security/PhpCode.php b/Security/PhpCode.php index c5ade693f..536e0cec6 100755 --- a/Security/PhpCode.php +++ b/Security/PhpCode.php @@ -18,7 +18,7 @@ namespace phpOMS\Security; * Php code security class. * * This can be used to ensure php code doesn't contain malicious functions and or characters. - * Additionally this can also be used in order verify that the source code is not altered compared to some expected source code. + * Additionally, this can also be used in order verify that the source code is not altered compared to some expected source code. * * @package phpOMS\Security * @license OMS License 2.0 @@ -156,12 +156,12 @@ final class PhpCode } /** - * Validate file integrety + * Validate file integrity * * @param string $source Source code path * @param string $hash Source hash (md5) * - * @return bool Returns true if filee matches expected signature otherwise false is returned + * @return bool Returns true if file matches expected signature otherwise false is returned * * @since 1.0.0 */ @@ -171,7 +171,7 @@ final class PhpCode } /** - * Validate code integrety + * Validate code integrity * * @param string $source Source code * @param string $remote Remote code diff --git a/Socket/Client/Client.php b/Socket/Client/Client.php index 64b6c1a7f..e90eeba7e 100755 --- a/Socket/Client/Client.php +++ b/Socket/Client/Client.php @@ -26,6 +26,9 @@ use phpOMS\Socket\SocketAbstract; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Implement + * https://github.com/Karaka-Management/phpOMS/issues/277 */ class Client extends SocketAbstract { @@ -147,7 +150,7 @@ class Client extends SocketAbstract } /** - * Add packet to be handeld + * Add packet to be handled * * @param mixed $packet Packet to handle * diff --git a/Socket/Client/ClientConnection.php b/Socket/Client/ClientConnection.php index 96ff84ea4..1f83b7da5 100755 --- a/Socket/Client/ClientConnection.php +++ b/Socket/Client/ClientConnection.php @@ -48,7 +48,7 @@ class ClientConnection */ public function __construct(Account $account, $socket) { - $this->id = $account->getId(); + $this->id = $account->id; $this->account = $account; $this->socket = $socket; } @@ -108,7 +108,7 @@ class ClientConnection * * @return mixed * - * @sicne 1.0.0 + * @since 1.0.0 */ public function getHandshake() { diff --git a/Socket/Server/Server.php b/Socket/Server/Server.php index facef28d3..e1225eaa1 100755 --- a/Socket/Server/Server.php +++ b/Socket/Server/Server.php @@ -27,6 +27,9 @@ use phpOMS\Socket\SocketAbstract; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Implement + * https://github.com/Karaka-Management/phpOMS/issues/278 */ class Server extends SocketAbstract { @@ -174,7 +177,7 @@ class Server extends SocketAbstract $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . - "Sec-WebSocket-Accept: ${acceptKey}" . + "Sec-WebSocket-Accept: {$acceptKey}" . "\r\n\r\n"; \socket_write($client->getSocket(), $upgrade); $client->setHandshake(true); @@ -295,11 +298,11 @@ class Server extends SocketAbstract \socket_shutdown($client->getSocket(), 2); \socket_close($client->getSocket()); - if (isset($this->conn[$client->getId()])) { - unset($this->conn[$client->getId()]); + if (isset($this->conn[$client->id])) { + unset($this->conn[$client->id]); } - $this->clientManager->remove($client->getId()); + $this->clientManager->remove($client->id); $this->app->logger->debug('Disconnected client.'); } diff --git a/Stdlib/Base/Address.php b/Stdlib/Base/Address.php index eeb7c1d63..bcda6fbbe 100755 --- a/Stdlib/Base/Address.php +++ b/Stdlib/Base/Address.php @@ -22,15 +22,23 @@ namespace phpOMS\Stdlib\Base; * @link https://jingga.app * @since 1.0.0 */ -class Address implements \JsonSerializable +class Address extends Location { + /** + * Model id. + * + * @var int + * @since 1.0.0 + */ + public int $id = 0; + /** * Name of the receiver. * * @var string * @since 1.0.0 */ - public string $recipient = ''; + public string $name = ''; /** * Sub of the address. @@ -40,24 +48,6 @@ class Address implements \JsonSerializable */ public string $fao = ''; - /** - * Location. - * - * @var Location - * @since 1.0.0 - */ - public Location $location; - - /** - * Constructor. - * - * @since 1.0.0 - */ - public function __construct() - { - $this->location = new Location(); - } - /** * {@inheritdoc} */ @@ -71,10 +61,12 @@ class Address implements \JsonSerializable */ public function toArray() : array { - return [ - 'recipient' => $this->recipient, - 'fao' => $this->fao, - 'location' => $this->location->toArray(), - ]; + return \array_merge( + [ + 'name' => $this->name, + 'fao' => $this->fao, + ], + parent::toArray() + ); } } diff --git a/Stdlib/Base/AddressType.php b/Stdlib/Base/AddressType.php index a6ecd8aab..f5cff24c3 100755 --- a/Stdlib/Base/AddressType.php +++ b/Stdlib/Base/AddressType.php @@ -34,9 +34,7 @@ abstract class AddressType extends Enum public const WORK = 5; - public const CONTRACT = 6; - - public const OTHER = 7; - public const EDUCATION = 8; + + public const OTHER = 99; } diff --git a/Stdlib/Base/Enum.php b/Stdlib/Base/Enum.php index 5000cb211..7cbd47f21 100755 --- a/Stdlib/Base/Enum.php +++ b/Stdlib/Base/Enum.php @@ -45,6 +45,27 @@ abstract class Enum return \in_array($value, $constants, true); } + /** + * Check enum value. + * + * Checking if a given value is part of this enum + * + * @param mixed $value Value to check + * + * @return mixed + * + * @since 1.0.0 + */ + public static function tryFromValue(mixed $value) : mixed + { + $reflect = new \ReflectionClass(static::class); + $constants = $reflect->getConstants(); + + return \in_array($value, $constants, true) + ? $value + : null; + } + /** * Getting all constants of this enum. * diff --git a/Stdlib/Base/Exception/InvalidEnumName.php b/Stdlib/Base/Exception/InvalidEnumName.php index 007c9dd11..dd9950fab 100755 --- a/Stdlib/Base/Exception/InvalidEnumName.php +++ b/Stdlib/Base/Exception/InvalidEnumName.php @@ -35,7 +35,7 @@ class InvalidEnumName extends \UnexpectedValueException * * @since 1.0.0 */ - public function __construct(string $message, int $code = 0, \Exception $previous = null) + public function __construct(string $message, int $code = 0, ?\Exception $previous = null) { parent::__construct('The enum name "' . $message . '" is not valid.', $code, $previous); } diff --git a/Stdlib/Base/Exception/InvalidEnumValue.php b/Stdlib/Base/Exception/InvalidEnumValue.php index f7d95ac8e..93d121ca3 100755 --- a/Stdlib/Base/Exception/InvalidEnumValue.php +++ b/Stdlib/Base/Exception/InvalidEnumValue.php @@ -35,7 +35,7 @@ class InvalidEnumValue extends \UnexpectedValueException * * @since 1.0.0 */ - public function __construct($message, int $code = 0, \Exception $previous = null) + public function __construct($message, int $code = 0, ?\Exception $previous = null) { parent::__construct('The enum value "' . $message . '" is not valid.', $code, $previous); } diff --git a/Stdlib/Base/FloatInt.php b/Stdlib/Base/FloatInt.php index 1104f0d18..cf8049ecd 100755 --- a/Stdlib/Base/FloatInt.php +++ b/Stdlib/Base/FloatInt.php @@ -34,6 +34,8 @@ class FloatInt implements SerializableInterface */ public const MAX_DECIMALS = 4; + public const DIVISOR = 10000; + /** * Thousands separator. * @@ -69,9 +71,9 @@ class FloatInt implements SerializableInterface */ public function __construct(int | float | string $value = 0, string $thousands = ',', string $decimal = '.') { - $this->value = \is_int($value) ? $value : self::toInt((string) $value); $this->thousands = $thousands; $this->decimal = $decimal; + $this->value = \is_int($value) ? $value : self::toInt((string) $value, $thousands, $decimal); } /** @@ -89,26 +91,19 @@ class FloatInt implements SerializableInterface */ public static function toInt(string $value, string $thousands = ',', string $decimal = '.') : int { - $split = \explode($decimal, $value); + $newValue = $value; + $len = \strlen($value); - if ($split === false) { - throw new \Exception('Internal explode error.'); // @codeCoverageIgnore + $decimalPos = \strrpos($value, $decimal); + if ($decimalPos === false) { + $decimalPos = $len - 1; } - $left = $split[0]; - $left = \str_replace($thousands, '', $left); - $right = ''; + $newValue = \str_pad($newValue, 4 - (- $decimalPos - 1), '0'); + $newValue = \str_replace([$thousands, $decimal], ['', ''], $newValue); + $newValue = \ltrim($newValue, '0'); - if (\count($split) > 1) { - $right = $split[1]; - } - - $right = \substr($right, 0, self::MAX_DECIMALS); - if ($right === false) { - throw new \Exception('Internal substr error.'); // @codeCoverageIgnore - } - - return ((int) $left) * 10 ** self::MAX_DECIMALS + (int) \str_pad($right, self::MAX_DECIMALS, '0'); + return (int) $newValue; } /** @@ -145,6 +140,20 @@ class FloatInt implements SerializableInterface return $this; } + public function getNormalizedValue() : float + { + return $this->value / self::DIVISOR; + } + + public function guessScalarValue() : int|float + { + $divider = self::DIVISOR; + + return $this->value % $divider === 0 + ? (int) ($this->value / $divider) + : (float) ($this->value / $divider); + } + /** * Get money. * @@ -172,9 +181,7 @@ class FloatInt implements SerializableInterface throw new \Exception(); // @codeCoverageIgnore } - if ($decimals === null) { - $decimals = \strlen(\rtrim($right, '0')); - } + $decimals ??= \strlen(\rtrim($right, '0')); return $decimals > 0 ? \number_format((float) $left, 0, $this->decimal, $this->thousands) . $this->decimal . \substr($right, 0, $decimals) @@ -194,25 +201,21 @@ class FloatInt implements SerializableInterface */ public function getFloat(?int $decimals = 2) : string { - $isNegative = $this->value < 0 ? 1 : 0; - $value = $this->value === 0 ? \str_repeat('0', self::MAX_DECIMALS) : (string) \round($this->value, -self::MAX_DECIMALS + $decimals); - $left = \substr($value, 0, -self::MAX_DECIMALS + $isNegative); + $left = \substr($value, 0, -self::MAX_DECIMALS); /** @var string $left */ $left = $left === false ? '0' : $left; - $right = \substr($value, -self::MAX_DECIMALS + $isNegative); + $right = \substr($value, -self::MAX_DECIMALS); if ($right === false) { throw new \Exception(); // @codeCoverageIgnore } - if ($decimals === null) { - $decimals = \strlen(\rtrim($right, '0')); - } + $decimals ??= \strlen(\rtrim($right, '0')); return $decimals > 0 ? \number_format((float) $left, 0, $this->decimal, '') . $this->decimal . \substr($right, 0, $decimals) @@ -378,4 +381,29 @@ class FloatInt implements SerializableInterface return $this; } + + public static function identifyNumericFormat(string $str) : ?array + { + $commaPos = \strrpos($str, ','); + $periodPos = \strrpos($str, '.'); + + if ($commaPos !== false && $periodPos !== false) { + return [ + 'thousands' => $commaPos < $periodPos ? ',' : '.', + 'decimal' => $commaPos < $periodPos ? '.' : ',', + ]; + } elseif ($commaPos === false && $periodPos === false) { + return null; + } + + // Back to normal cases + $isComma = $commaPos !== false + ? $commaPos + 3 === \strlen($str) + : $periodPos + 3 !== \strlen($str); + + return [ + 'thousands' => $isComma ? '.' : ',', + 'decimal' => $isComma ? ',' : '.' + ]; + } } diff --git a/Stdlib/Base/Heap.php b/Stdlib/Base/Heap.php index d3dbcf697..aad6b0ccc 100755 --- a/Stdlib/Base/Heap.php +++ b/Stdlib/Base/Heap.php @@ -47,7 +47,7 @@ class Heap * * @since 1.0.0 */ - public function __construct(\Closure $compare = null) + public function __construct(?\Closure $compare = null) { $this->compare = $compare ?? function($a, $b) { return $a <=> $b; diff --git a/Stdlib/Base/Iban.php b/Stdlib/Base/Iban.php index 2754d36ad..bbe49de9e 100755 --- a/Stdlib/Base/Iban.php +++ b/Stdlib/Base/Iban.php @@ -119,7 +119,7 @@ class Iban implements SerializableInterface $country = $this->getCountry(); /** @var string $iban */ - $iban = IbanEnum::getByName('C_' . $country); + $iban = IbanEnum::getByName('_' . $country); $layout = \str_replace(' ', '', $iban); $start = \stripos($layout, $sequence); $end = \strrpos($layout, $sequence); diff --git a/Stdlib/Base/Location.php b/Stdlib/Base/Location.php index d7140e4eb..2260cabef 100755 --- a/Stdlib/Base/Location.php +++ b/Stdlib/Base/Location.php @@ -67,6 +67,16 @@ class Location implements \JsonSerializable, SerializableInterface */ public string $address = ''; + /** + * Address addition. + * + * e.g. 2nd floor + * + * @var string + * @since 1.0.0 + */ + public string $addressAddition = ''; + /** * Address type * @@ -99,32 +109,6 @@ class Location implements \JsonSerializable, SerializableInterface return $this->id; } - /** - * Get location type - * - * @return int - * - * @since 1.0.0 - */ - public function getType() : int - { - return $this->type; - } - - /** - * Set location type - * - * @param int $type Location type - * - * @return void - * - * @since 1.0.0 - */ - public function setType(int $type) : void - { - $this->type = $type; - } - /** * Get country code * diff --git a/Stdlib/Base/NullAddress.php b/Stdlib/Base/NullAddress.php new file mode 100644 index 000000000..b4e477311 --- /dev/null +++ b/Stdlib/Base/NullAddress.php @@ -0,0 +1,38 @@ +id = $id; + } +} diff --git a/Stdlib/Base/NullLocation.php b/Stdlib/Base/NullLocation.php index 4da786fd9..b5d95b361 100755 --- a/Stdlib/Base/NullLocation.php +++ b/Stdlib/Base/NullLocation.php @@ -24,4 +24,15 @@ namespace phpOMS\Stdlib\Base; */ final class NullLocation extends Location { + /** + * Constructor + * + * @param int $id Model id + * + * @since 1.0.0 + */ + public function __construct(int $id = 0) + { + $this->id = $id; + } } diff --git a/Stdlib/Base/SmartDateTime.php b/Stdlib/Base/SmartDateTime.php index 30b456207..df0803902 100755 --- a/Stdlib/Base/SmartDateTime.php +++ b/Stdlib/Base/SmartDateTime.php @@ -52,7 +52,7 @@ class SmartDateTime extends \DateTime * * @since 1.0.0 */ - public function __construct(string $datetime = 'now', \DateTimeZone $timezone = null) + public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null) { $parsed = \str_replace( ['Y', 'm', 'd'], @@ -111,15 +111,18 @@ class SmartDateTime extends \DateTime */ public function smartModify(int $y = 0, int $m = 0, int $d = 0, int $calendar = \CAL_GREGORIAN) : self { - $yearChange = (int) \floor(((int) $this->format('m') - 1 + $m) / 12); - $yearNew = (int) $this->format('Y') + $y + $yearChange; + $year = (int) $this->format('Y'); + $month = (int) $this->format('m'); - $monthNew = (int) $this->format('m') + $m; + $yearChange = (int) \floor(($month - 1 + $m) / 12); + $yearNew = $year + $y + $yearChange; + + $monthNew = $month - 1 + $m; $monthNew = $monthNew < 0 - ? 12 + ($monthNew - 1) % 12 + 1 - : ($monthNew - 1) % 12 + 1; + ? ($month - 1 + $m - 12 * $yearChange) % 12 + 1 + : $monthNew % 12 + 1; - $dayMonthOld = \cal_days_in_month($calendar, (int) $this->format('m'), (int) $this->format('Y')); + $dayMonthOld = \cal_days_in_month($calendar, $month, $year); $dayMonthNew = \cal_days_in_month($calendar, $monthNew, $yearNew); $dayOld = (int) $this->format('d'); @@ -365,13 +368,13 @@ class SmartDateTime extends \DateTime * * @param int $month Start of the year (i.e. fiscal year) * - * @return \DateTime + * @return SmartDateTime * * @since 1.0.0 */ - public static function startOfYear(int $month = 1) : \DateTime + public static function startOfYear(int $month = 1) : SmartDateTime { - return new \DateTime(\date('Y') . '-' . \sprintf('%02d', $month) . '-01'); + return new SmartDateTime(\date('Y') . '-' . \sprintf('%02d', $month) . '-01'); } /** @@ -379,37 +382,37 @@ class SmartDateTime extends \DateTime * * @param int $month Start of the year (i.e. fiscal year) * - * @return \DateTime + * @return SmartDateTime * * @since 1.0.0 */ - public static function endOfYear(int $month = 1) : \DateTime + public static function endOfYear(int $month = 1) : SmartDateTime { - return new \DateTime(\date('Y') . '-' . self::calculateMonthIndex(13 - $month, $month) . '-31'); + return new SmartDateTime(\date('Y') . '-' . self::calculateMonthIndex(13 - $month, $month) . '-31'); } /** * Get the start of the month * - * @return \DateTime + * @return SmartDateTime * * @since 1.0.0 */ - public static function startOfMonth() : \DateTime + public static function startOfMonth() : SmartDateTime { - return new \DateTime(\date('Y-m') . '-01'); + return new SmartDateTime(\date('Y-m') . '-01'); } /** * Get the end of the month * - * @return \DateTime + * @return SmartDateTime * * @since 1.0.0 */ - public static function endOfMonth() : \DateTime + public static function endOfMonth() : SmartDateTime { - return new \DateTime(\date('Y-m-t')); + return new SmartDateTime(\date('Y-m-t')); } /** @@ -445,4 +448,32 @@ class SmartDateTime extends \DateTime return \abs(($mod < 0 ? 12 + $mod : $mod) % 12) + 1; } + + public static function formatDuration(int $duration) : string + { + $days = \floor($duration / (24 * 3600)); + $hours = \floor(($duration % (24 * 3600)) / 3600); + $minutes = \floor(($duration % 3600) / 60); + $seconds = $duration % 60; + + $result = ''; + + if ($days > 0) { + $result .= \sprintf('%02dd', $days); + } + + if ($hours > 0) { + $result .= \sprintf('%02dh', $hours); + } + + if ($minutes > 0) { + $result .= \sprintf('%02dm', $minutes); + } + + if ($seconds > 0) { + $result .= \sprintf('%02ds', $seconds); + } + + return \rtrim($result, ' '); + } } diff --git a/Stdlib/Graph/Graph.php b/Stdlib/Graph/Graph.php index 55955d10b..8cdf8265e 100755 --- a/Stdlib/Graph/Graph.php +++ b/Stdlib/Graph/Graph.php @@ -531,7 +531,7 @@ class Graph */ private function pathBetweenNodesDfs( Node $node1, - Node $node2 = null, + ?Node $node2 = null, array &$visited, array &$path, array &$paths @@ -675,7 +675,7 @@ class Graph /** * Get all paths between two nodes - * Inclides end node, but not start node in the paths + * Includes end node, but not start node in the paths * * @param int|string|Node $node1 Graph node * @param int|string|Node $node2 Graph node @@ -916,7 +916,7 @@ class Graph $current = \array_shift($stack); foreach ($this->nodes as $j) { - // Has neighbour + // Has neighbor if ($this->nodes[$current->getId()]->hasNeighbor($j)) { if ($distances[$j->getId()] === \PHP_INT_MAX) { $distances[$j->getId()] = $distances[$current->getId()] + 1; @@ -1017,7 +1017,7 @@ class Graph * * @since 1.0.0 */ - public function isConnected(int | string | Node $node1 = null, int | string | Node $node2 = null) : bool + public function isConnected(int | string | Node|null $node1 = null, int | string | Node|null $node2 = null) : bool { if (empty($this->nodes)) { return true; diff --git a/Stdlib/Graph/Node.php b/Stdlib/Graph/Node.php index ca0fff92a..ba0848645 100755 --- a/Stdlib/Graph/Node.php +++ b/Stdlib/Graph/Node.php @@ -125,7 +125,7 @@ class Node * * @since 1.0.0 */ - public function setNodeRelative(self $node, int $key = null, bool $isDirected = false) : Edge + public function setNodeRelative(self $node, ?int $key = null, bool $isDirected = false) : Edge { $edge = new Edge($this, $node, 0.0, $isDirected); $this->setEdge($edge, $key); @@ -147,7 +147,7 @@ class Node * * @since 1.0.0 */ - public function setEdge(Edge $edge, int $key = null) : self + public function setEdge(Edge $edge, ?int $key = null) : self { if ($key === null) { $this->edges[] = $edge; diff --git a/Stdlib/Map/MultiMap.php b/Stdlib/Map/MultiMap.php index 949f43f91..fa5fa752f 100755 --- a/Stdlib/Map/MultiMap.php +++ b/Stdlib/Map/MultiMap.php @@ -95,7 +95,7 @@ final class MultiMap implements \Countable // prevent adding elements if keys are just ordered differently if ($this->orderType === OrderType::LOOSE) { /** @var array $keysToTest */ - $keysToTest = Permutation::permut($keys, [], false); + $keysToTest = Permutation::permuteAll($keys, [], false); foreach ($keysToTest as $test) { $key = \implode(':', $test); @@ -219,7 +219,7 @@ final class MultiMap implements \Countable if (\is_array($key)) { if ($this->orderType === OrderType::LOOSE) { /** @var array $keys */ - $keys = Permutation::permut($key, [], false); + $keys = Permutation::permuteAll($key, [], false); foreach ($keys as $key => $value) { $key = \implode(':', $value); @@ -273,7 +273,7 @@ final class MultiMap implements \Countable if ($this->orderType !== OrderType::STRICT) { /** @var array $permutation */ - $permutation = Permutation::permut($key, [], false); + $permutation = Permutation::permuteAll($key, [], false); foreach ($permutation as $permut) { if ($this->setSingle(\implode(':', $permut), $value)) { @@ -344,7 +344,7 @@ final class MultiMap implements \Countable } /** @var array $keys */ - $keys = Permutation::permut($key, [], false); + $keys = Permutation::permuteAll($key, [], false); $found = false; foreach ($keys as $key => $value) { @@ -487,7 +487,7 @@ final class MultiMap implements \Countable if ($this->orderType === OrderType::LOOSE) { $key = \is_array($key) ? $key : [$key]; - return Permutation::permut($key, [], false); + return Permutation::permuteAll($key, [], false); } return []; diff --git a/Stdlib/Tree/BinarySearchTree.php b/Stdlib/Tree/BinarySearchTree.php index 7b336f8b8..f5fe8779e 100644 --- a/Stdlib/Tree/BinarySearchTree.php +++ b/Stdlib/Tree/BinarySearchTree.php @@ -9,6 +9,7 @@ * @license OMS License 2.0 * @version 1.0.0 * @link https://jingga.app + * @link https://github.com/PetarV-/Algorithms/blob/master/Data%20Structures/Binary%20Search%20Tree.cpp */ declare(strict_types=1); @@ -39,7 +40,7 @@ class BinarySearchTree * * @since 1.0.0 */ - public function __construct(Node $root = null) + public function __construct(?Node $root = null) { $this->root = $root; } @@ -180,36 +181,40 @@ class BinarySearchTree } $current = $this->root; - while (true) { + while ($current !== null) { $comparison = $node->compare($current->data); if ($comparison < 0) { if ($current->left === null) { - $BST = new BinarySearchTree(); + $BST = new self(); $new = new Node($node->key, $node->data); $new->parent = $current; $new->tree = $BST; $BST->root = $new; $current->left = $BST; + + return; } else { $current = $current->left->root; } } elseif ($comparison > 0) { if ($current->right === null) { - $BST = new BinarySearchTree(); + $BST = new self(); $new = new Node($node->key, $node->data); $new->parent = $current; $new->tree = $BST; $BST->root = $new; $current->right = $BST; + + return; } else { $current = $current->right->root; } + } else { + return; } - - return; } } @@ -222,61 +227,92 @@ class BinarySearchTree * * @since 1.0.0 */ - public function delete(Node &$node) : void + public function delete(Node $node) : void { if ($node->left === null && $node->right === null) { - if ($node->parent !== null) { - if ($node->parent->left !== null && $node->parent->left->root->compare($node->data) === 0) { - $node->parent->left = null; - } elseif ($node->parent->right !== null && $node->parent->right->root->compare($node) === 0) { - $node->parent->right = null; - } + if ($node->parent === null) { + return; } - $node = null; + if ($node->parent->left !== null + && $node->parent->left->root?->compare($node->data) === 0 + ) { + $node->parent->left = null; + } elseif ($node->parent->right !== null + && $node->parent->right->root?->compare($node->data) === 0 + ) { + $node->parent->right = null; + } return; } $temp = null; if ($node->left === null) { - $temp = $node->right->root; + $temp = $node->right?->root; + if ($temp === null) { + return; + } + if ($node->parent !== null) { - if ($node->parent->left !== null && $node->parent->left->root->compare($node->data) === 0) { + if ($node->parent->left !== null + && $node->parent->left->root?->compare($node->data) === 0 + ) { $node->parent->left = $temp->tree; - } elseif ($node->parent->right !== null && $node->parent->right->root->compare($node->data) === 0) { + } elseif ($node->parent->right !== null + && $node->parent->right->root?->compare($node->data) === 0 + ) { $node->parent->right = $temp->tree; } } $temp->parent = $node->parent; - $node = null; + //$node = null; return; } if ($node->right === null) { $temp = $node->left->root; + if ($temp === null) { + return; + } + if ($node->parent !== null) { - if ($node->parent->left !== null && $node->parent->left->root->compare($node->data) === 0) { + if ($node->parent->left !== null + && $node->parent->left->root?->compare($node->data) === 0 + ) { $node->parent->left = $temp->tree; - } elseif ($node->parent->right !== null && $node->parent->right->root->compare($node->data) === 0) { + } elseif ($node->parent->right !== null + && $node->parent->right->root?->compare($node->data) === 0 + ) { $node->parent->right = $temp->tree; } } $temp->parent = $node->parent; - $node = null; + //$node = null; return; - } else { - $temp = $this->successor($node); + } elseif (($temp = $this->successor($node)) !== null) { $node->key = $temp->key; $node->data = $temp->data; $this->delete($temp); } } + + /** + * To array + * + * @return null|array + * + * @since 1.0.0 + */ + public function toArray() : ?array + { + return $this->root?->toArray() ?? null; + } } diff --git a/Stdlib/Tree/Node.php b/Stdlib/Tree/Node.php index 35c5acd03..7a6730bec 100644 --- a/Stdlib/Tree/Node.php +++ b/Stdlib/Tree/Node.php @@ -99,4 +99,20 @@ class Node { return $this->data <=> $data; } + + /** + * To array + * + * @return array + * + * @since 1.0.0 + */ + public function toArray() : array + { + return [ + 'key' => $this->key, + 0 => $this->left?->toArray(), + 1 => $this->right?->toArray(), + ]; + } } diff --git a/System/File/DirectoryInterface.php b/System/File/DirectoryInterface.php index 4ad150435..4117bdb86 100755 --- a/System/File/DirectoryInterface.php +++ b/System/File/DirectoryInterface.php @@ -32,7 +32,7 @@ interface DirectoryInterface extends \ArrayAccess, \Iterator, ContainerInterface /** * Get node by name. * - * @param string $name File/direcotry name + * @param string $name File/directory name * * @return null|ContainerInterface * diff --git a/System/File/FileUtils.php b/System/File/FileUtils.php index 903fd44e8..7d7cb8bfa 100755 --- a/System/File/FileUtils.php +++ b/System/File/FileUtils.php @@ -164,7 +164,7 @@ final class FileUtils } /** - * Converts a string permisseion (rwx) to octal + * Converts a string permission (rwx) to octal * * @param string $permission Permission string (e.g. rwx-w-r--) * @@ -188,7 +188,7 @@ final class FileUtils } if (($i + 1) % 3 === 0) { - $perm .= $tempPermission; + $perm .= $tempPermission; $tempPermission = 0; } } @@ -206,7 +206,7 @@ final class FileUtils * * @since 1.0.0 */ - public static function mb_pathinfo(string $path, int | string $options = null) : string | array + public static function mb_pathinfo(string $path, int | string|null $options = null) : string | array { $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '']; $pathinfo = []; diff --git a/System/File/Ftp/Directory.php b/System/File/Ftp/Directory.php index 7b2522f93..8496c6c11 100755 --- a/System/File/Ftp/Directory.php +++ b/System/File/Ftp/Directory.php @@ -48,7 +48,7 @@ class Directory extends FileAbstract implements DirectoryInterface * @var array * @since 1.0.0 */ - private array $nodes = []; + public array $nodes = []; /** * Create ftp connection. @@ -87,7 +87,7 @@ class Directory extends FileAbstract implements DirectoryInterface * * @since 1.0.0 */ - public function __construct(HttpUri $uri, bool $initialize = true, \FTP\Connection $con = null) + public function __construct(HttpUri $uri, bool $initialize = true, ?\FTP\Connection $con = null) { $this->uri = $uri; $this->con = $con ?? self::ftpConnect($uri); @@ -123,7 +123,11 @@ class Directory extends FileAbstract implements DirectoryInterface $uri = clone $this->uri; $uri->setPath($filename); - $file = \ftp_size($this->con, $filename) === -1 ? new self($uri, false, $this->con) : new File($uri, $this->con); + $file = \ftp_size($this->con, $filename) === -1 + ? new self($uri, false, $this->con) + : new File($uri, $this->con); + + $file->parent = $this; $this->addNode($file); } @@ -676,8 +680,8 @@ class Directory extends FileAbstract implements DirectoryInterface */ public function addNode(ContainerInterface $node) : self { - $this->count += $node->getCount(); - $this->size += $node->getSize(); + $this->count += $node->getCount(); + $this->size += $node->getSize(); $this->nodes[$node->getBasename()] = $node; $node->createNode(); @@ -693,7 +697,7 @@ class Directory extends FileAbstract implements DirectoryInterface $uri = clone $this->uri; $uri->setPath(self::parent($this->path)); - return new self($uri, true, $this->con); + return $this->parent ?? new self($uri, true, $this->con); } /** @@ -705,7 +709,19 @@ class Directory extends FileAbstract implements DirectoryInterface return false; } - return self::copy($this->con, $this->path, $to, $overwrite); + $newParent = $this->findNode($to); + + $state = self::copy($this->con, $this->path, $to, $overwrite); + + /** @var null|Directory $newParent */ + if ($newParent !== null) { + $uri = clone $this->uri; + $uri->setPath($to); + + $newParent->addNode(new self($uri)); + } + + return $state; } /** @@ -717,7 +733,9 @@ class Directory extends FileAbstract implements DirectoryInterface return false; } - return self::move($this->con, $this->path, $to, $overwrite); + $state = $this->copyNode($to, $overwrite); + + return $state && $this->deleteNode(); } /** @@ -729,7 +747,9 @@ class Directory extends FileAbstract implements DirectoryInterface return false; } - // @todo: update parent + if (isset($this->parent)) { + unset($this->parent->nodes[$this->getBasename()]); + } return self::delete($this->con, $this->path); } @@ -843,7 +863,7 @@ class Directory extends FileAbstract implements DirectoryInterface * * @since 1.0.0 */ - public function isExisting(string $name = null) : bool + public function isExisting(?string $name = null) : bool { if ($name === null) { return \is_dir($this->path); diff --git a/System/File/Ftp/File.php b/System/File/Ftp/File.php index 6f24f4a8c..85d08ea4a 100755 --- a/System/File/Ftp/File.php +++ b/System/File/Ftp/File.php @@ -44,7 +44,7 @@ class File extends FileAbstract implements FileInterface * * @since 1.0.0 */ - public function __construct(HttpUri $uri, \FTP\Connection $con = null) + public function __construct(HttpUri $uri, ?\FTP\Connection $con = null) { $this->uri = $uri; $this->con = $con ?? self::ftpConnect($this->uri); @@ -482,7 +482,7 @@ class File extends FileAbstract implements FileInterface $uri = clone $this->uri; $uri->setPath(self::parent($this->path)); - return new Directory($uri, true, $this->con); + return $this->parent ?? new Directory($uri, true, $this->con); } /** @@ -517,7 +517,19 @@ class File extends FileAbstract implements FileInterface return false; } - return self::copy($this->con, $this->path, $to, $overwrite); + $newParent = $this->findNode($to); + + $state = self::copy($this->con, $this->path, $to, $overwrite); + + /** @var null|Directory $newParent */ + if ($newParent !== null) { + $uri = clone $this->uri; + $uri->setPath($to); + + $newParent->addNode(new self($uri)); + } + + return $state; } /** @@ -536,7 +548,9 @@ class File extends FileAbstract implements FileInterface return false; } - return self::move($this->con, $this->path, $to, $overwrite); + $state = $this->copyNode($to, $overwrite); + + return $state && $this->deleteNode(); } /** @@ -552,6 +566,10 @@ class File extends FileAbstract implements FileInterface return false; } + if (isset($this->parent)) { + unset($this->parent->nodes[$this->getBasename()]); + } + return self::delete($this->con, $this->path); } @@ -692,6 +710,6 @@ class File extends FileAbstract implements FileInterface $uri = clone $this->uri; $uri->setPath(self::dirpath($this->path)); - return new Directory($uri, true, $this->con); + return $this->parent ?? new Directory($uri, true, $this->con); } } diff --git a/System/File/Ftp/FileAbstract.php b/System/File/Ftp/FileAbstract.php index 905808132..f8d229d50 100755 --- a/System/File/Ftp/FileAbstract.php +++ b/System/File/Ftp/FileAbstract.php @@ -116,6 +116,14 @@ abstract class FileAbstract implements FtpContainerInterface */ protected bool $isInitialized = false; + /** + * Parent element + * + * @var null|Directory + * @since 1.0.0 + */ + protected ?Directory $parent = null; + /** * Constructor. * @@ -224,4 +232,68 @@ abstract class FileAbstract implements FtpContainerInterface $this->isInitialized = true; } + + /** + * Find an existing node in the node tree + * + * @param string $path Path of the node + * + * @return null|Directory + * + * @since 1.0.0 + */ + public function findNode(string $path) : ?Directory + { + // Change parent element + $currentPath = \explode('/', \trim($this->path, '/')); + $newPath = \explode('/', \trim($path, '/')); + + // Remove last element which is the current name + $currentName = \array_pop($currentPath); + $newName = \array_pop($newPath); + + $currentParentName = \end($currentPath); + $newParentName = \end($newPath); + + $currentLength = \count($currentPath); + $newLength = \count($newPath); + + $max = \max($currentLength, $newLength); + $newParent = $this; + + // Evaluate path similarity + for ($i = 0; $i < $max; ++$i) { + if (!isset($currentPath[$i]) || !isset($newPath[$i]) + || $currentPath[$i] !== $newPath[$i] + ) { + break; + } + } + + // Walk parent path + for ($j = $currentLength - $i; $j > 0; --$j) { + if ($newParent->parent === null) { + // No parent found + + $newParent = null; + break; + } + + $newParent = $newParent->parent; + } + + // Walk child path if new path even is in child path + for ($j = $i; $i < $newLength; ++$j) { + if (!isset($newParent->nodes[$newPath[$j]])) { + // Path tree is not defined that deep -> no updating needed + + $newParent = null; + break; + } + + $newParent = $newParent->nodes[$newPath[$j]]; + } + + return $newParent; + } } diff --git a/System/File/Local/Directory.php b/System/File/Local/Directory.php index b51786246..aa309a9d3 100755 --- a/System/File/Local/Directory.php +++ b/System/File/Local/Directory.php @@ -45,7 +45,7 @@ final class Directory extends FileAbstract implements DirectoryInterface * @var array * @since 1.0.0 */ - private array $nodes = []; + public array $nodes = []; /** * Constructor. @@ -172,7 +172,11 @@ final class Directory extends FileAbstract implements DirectoryInterface foreach ($files as $filename) { if (!StringUtils::endsWith(\trim($filename), '.')) { - $file = \is_dir($filename) ? new self($filename, '*', false) : new File($filename); + $file = \is_dir($filename) + ? new self($filename, '*', false) + : new File($filename); + + $file->parent = $this; $this->addNode($file); } @@ -184,8 +188,8 @@ final class Directory extends FileAbstract implements DirectoryInterface */ public function addNode(ContainerInterface $node) : self { - $this->count += $node->getCount(); - $this->size += $node->getSize(); + $this->count += $node->getCount(); + $this->size += $node->getSize(); $this->nodes[$node->getBasename()] = $node; $node->createNode(); @@ -461,7 +465,7 @@ final class Directory extends FileAbstract implements DirectoryInterface * * @since 1.0.0 */ - public function isExisting(string $name = null) : bool + public function isExisting(?string $name = null) : bool { if ($name === null) { return \is_dir($this->path); @@ -630,7 +634,7 @@ final class Directory extends FileAbstract implements DirectoryInterface */ public function getParent() : ContainerInterface { - return new self(self::parent($this->path)); + return $this->parent ?? new self(self::parent($this->path)); } /** @@ -638,7 +642,16 @@ final class Directory extends FileAbstract implements DirectoryInterface */ public function copyNode(string $to, bool $overwrite = false) : bool { - return self::copy($this->path, $to, $overwrite); + $newParent = $this->findNode($to); + + $state = self::copy($this->path, $to, $overwrite); + + /** @var null|Directory $newParent */ + if ($newParent !== null) { + $newParent->addNode(new self($to)); + } + + return $state; } /** @@ -646,7 +659,9 @@ final class Directory extends FileAbstract implements DirectoryInterface */ public function moveNode(string $to, bool $overwrite = false) : bool { - return self::move($this->path, $to, $overwrite); + $state = $this->copyNode($to, $overwrite); + + return $state && $this->deleteNode(); } /** @@ -654,7 +669,9 @@ final class Directory extends FileAbstract implements DirectoryInterface */ public function deleteNode() : bool { - // @todo: update parent + if (isset($this->parent)) { + unset($this->parent->nodes[$this->getBasename()]); + } return self::delete($this->path); } diff --git a/System/File/Local/File.php b/System/File/Local/File.php index 9f86c1bde..9caaabbdd 100755 --- a/System/File/Local/File.php +++ b/System/File/Local/File.php @@ -471,7 +471,7 @@ final class File extends FileAbstract implements FileInterface */ public function getParent() : ContainerInterface { - return new Directory(self::parent($this->path)); + return $this->parent ?? new Directory(self::parent($this->path)); } /** @@ -483,7 +483,7 @@ final class File extends FileAbstract implements FileInterface */ public function getDirectory() : ContainerInterface { - return new Directory(self::dirpath($this->path)); + return $this->parent ?? new Directory(self::dirpath($this->path)); } /** @@ -491,7 +491,16 @@ final class File extends FileAbstract implements FileInterface */ public function copyNode(string $to, bool $overwrite = false) : bool { - return self::copy($this->path, $to, $overwrite); + $newParent = $this->findNode($to); + + $state = self::copy($this->path, $to, $overwrite); + + /** @var null|Directory $newParent */ + if ($newParent !== null) { + $newParent->addNode(new self($to)); + } + + return $state; } /** @@ -499,7 +508,9 @@ final class File extends FileAbstract implements FileInterface */ public function moveNode(string $to, bool $overwrite = false) : bool { - return self::move($this->path, $to, $overwrite); + $state = $this->copyNode($to, $overwrite); + + return $state && $this->deleteNode(); } /** @@ -507,6 +518,10 @@ final class File extends FileAbstract implements FileInterface */ public function deleteNode() : bool { + if (isset($this->parent)) { + unset($this->parent->nodes[$this->getBasename()]); + } + return self::delete($this->path); } diff --git a/System/File/Local/FileAbstract.php b/System/File/Local/FileAbstract.php index 08f0e3f9f..b1ea28c0e 100755 --- a/System/File/Local/FileAbstract.php +++ b/System/File/Local/FileAbstract.php @@ -100,6 +100,14 @@ abstract class FileAbstract implements LocalContainerInterface */ protected bool $isInitialized = false; + /** + * Parent element + * + * @var null|Directory + * @since 1.0.0 + */ + protected ?Directory $parent = null; + /** * Constructor. * @@ -255,4 +263,68 @@ abstract class FileAbstract implements LocalContainerInterface $this->isInitialized = true; } + + /** + * Find an existing node in the node tree + * + * @param string $path Path of the node + * + * @return null|Directory + * + * @since 1.0.0 + */ + public function findNode(string $path) : ?Directory + { + // Change parent element + $currentPath = \explode('/', \trim($this->path, '/')); + $newPath = \explode('/', \trim($path, '/')); + + // Remove last element which is the current name + $currentName = \array_pop($currentPath); + $newName = \array_pop($newPath); + + $currentParentName = \end($currentPath); + $newParentName = \end($newPath); + + $currentLength = \count($currentPath); + $newLength = \count($newPath); + + $max = \max($currentLength, $newLength); + $newParent = $this; + + // Evaluate path similarity + for ($i = 0; $i < $max; ++$i) { + if (!isset($currentPath[$i]) || !isset($newPath[$i]) + || $currentPath[$i] !== $newPath[$i] + ) { + break; + } + } + + // Walk parent path + for ($j = $currentLength - $i; $j > 0; --$j) { + if ($newParent->parent === null) { + // No parent found + + $newParent = null; + break; + } + + $newParent = $newParent->parent; + } + + // Walk child path if new path even is in child path + for ($j = $i; $i < $newLength; ++$j) { + if (!isset($newParent->nodes[$newPath[$j]])) { + // Path tree is not defined that deep -> no updating needed + + $newParent = null; + break; + } + + $newParent = $newParent->nodes[$newPath[$j]]; + } + + return $newParent; + } } diff --git a/System/File/PathException.php b/System/File/PathException.php index 70b18080e..f399a28b9 100755 --- a/System/File/PathException.php +++ b/System/File/PathException.php @@ -33,7 +33,7 @@ final class PathException extends \UnexpectedValueException * * @since 1.0.0 */ - public function __construct(string $message, int $code = 0, \Exception $previous = null) + public function __construct(string $message, int $code = 0, ?\Exception $previous = null) { parent::__construct('The path "' . $message . '" is not a valid path.', $code, $previous); } diff --git a/System/File/PermissionException.php b/System/File/PermissionException.php index 9fa2a1d22..ef19bf726 100755 --- a/System/File/PermissionException.php +++ b/System/File/PermissionException.php @@ -33,7 +33,7 @@ final class PermissionException extends \RuntimeException * * @since 1.0.0 */ - public function __construct(string $message, int $code = 0, \Exception $previous = null) + public function __construct(string $message, int $code = 0, ?\Exception $previous = null) { parent::__construct('Insufficient permissions for "' . $message . '" operations.', $code, $previous); } diff --git a/System/File/Storage.php b/System/File/Storage.php index 56b7fb91f..7adb5956c 100755 --- a/System/File/Storage.php +++ b/System/File/Storage.php @@ -47,7 +47,7 @@ final class Storage } /** - * Get registred env instance. + * Get registered env instance. * * @param string $env Environment name * diff --git a/System/MimeType.php b/System/MimeType.php index 54fde282e..d2beb82bc 100755 --- a/System/MimeType.php +++ b/System/MimeType.php @@ -2008,7 +2008,7 @@ abstract class MimeType extends Enum public const M_123 = 'application/vnd.lotus-1-2-3'; - public const M_PEXE = 'vnd.microsoft.portable-executable'; + public const M_PEXE = 'application/vnd.microsoft.portable-executable'; public const M_EXE = 'application/exe'; diff --git a/System/Search/StringSearch.php b/System/Search/StringSearch.php index a37c1c4c9..262614280 100755 --- a/System/Search/StringSearch.php +++ b/System/Search/StringSearch.php @@ -60,7 +60,7 @@ abstract class StringSearch if ($j > 0) { $i += $shift[$j - 1]; - $j = \max($j - $shift[$j - 1], 0); + $j = \max($j - $shift[$j - 1], 0); } else { ++$i; $j = 0; @@ -98,7 +98,7 @@ abstract class StringSearch if ($j > 0) { $i += $shift[$j - 1]; - $j = \max($j - $shift[$j - 1], 0); + $j = \max($j - $shift[$j - 1], 0); } else { ++$i; $j = 0; diff --git a/System/SystemUtils.php b/System/SystemUtils.php index 970f9367e..c079aa00a 100755 --- a/System/SystemUtils.php +++ b/System/SystemUtils.php @@ -45,12 +45,12 @@ final class SystemUtils { $mem = 0; - if (\stristr(\PHP_OS, 'WIN')) { + if (\stripos(\PHP_OS, 'WIN')) { $memArr = []; \exec('wmic memorychip get capacity', $memArr); $mem = \array_sum($memArr) / 1024; - } elseif (\stristr(\PHP_OS, 'LINUX')) { + } elseif (\stripos(\PHP_OS, 'LINUX')) { $fh = \fopen('/proc/meminfo', 'r'); if ($fh === false) { @@ -82,7 +82,7 @@ final class SystemUtils { $memUsage = 0; - if (\stristr(\PHP_OS, 'LINUX')) { + if (\stripos(\PHP_OS, 'LINUX')) { $free = \shell_exec('free'); if ($free === null || $free === false) { @@ -110,11 +110,11 @@ final class SystemUtils { $cpuUsage = 0; - if (\stristr(\PHP_OS, 'WIN') !== false) { + if (\stripos(\PHP_OS, 'WIN') !== false) { $cpuUsage = null; \exec('wmic cpu get LoadPercentage', $cpuUsage); $cpuUsage = (int) ($cpuUsage[1] ?? -1); - } elseif (\stristr(\PHP_OS, 'LINUX') !== false) { + } elseif (\stripos(\PHP_OS, 'LINUX') !== false) { $loadavg = \sys_getloadavg(); if ($loadavg === false) { @@ -167,7 +167,7 @@ final class SystemUtils */ public static function runProc(string $executable, string $cmd, bool $async = false) : array { - if (\strtolower((string) \substr(\PHP_OS, 0, 3)) === 'win') { + if (\stripos(\PHP_OS, 'WIN') !== false) { $cmd = 'cd ' . \escapeshellarg(\dirname($executable)) . ' && ' . \basename($executable) . ' ' @@ -193,7 +193,6 @@ final class SystemUtils ]; $resource = \proc_open($cmd, $desc, $pipes, null, null); - if ($resource === false) { throw new \Exception(); } @@ -214,7 +213,6 @@ final class SystemUtils } $status = \proc_close($resource); - if ($status == -1) { throw new \Exception((string) $stderr); } diff --git a/Uri/Argument.php b/Uri/Argument.php index addbc7ff4..8ad4b6898 100755 --- a/Uri/Argument.php +++ b/Uri/Argument.php @@ -119,7 +119,7 @@ final class Argument implements UriInterface /** * Uri query. * - * @var array + * @var array * @since 1.0.0 */ private array $query = []; @@ -303,7 +303,7 @@ final class Argument implements UriInterface /** * {@inheritdoc} */ - public function getQuery(string $key = null) : string + public function getQuery(?string $key = null) : string { if ($key !== null) { $key = (int) \strtolower($key); diff --git a/Uri/HttpUri.php b/Uri/HttpUri.php index 6d4a7cd3a..45e53d7f7 100755 --- a/Uri/HttpUri.php +++ b/Uri/HttpUri.php @@ -121,7 +121,7 @@ final class HttpUri implements UriInterface /** * Uri query. * - * @var array + * @var array * @since 1.0.0 */ private array $query = []; @@ -362,7 +362,7 @@ final class HttpUri implements UriInterface /** * {@inheritdoc} */ - public function getQuery(string $key = null) : string + public function getQuery(?string $key = null) : string { if ($key !== null) { $key = \strtolower($key); @@ -383,6 +383,34 @@ final class HttpUri implements UriInterface $this->query = \array_change_key_case($this->query, \CASE_LOWER); } + /** + * Add query parameter + * + * @param string $key Parameter key + * @param null|string $value Value (null = omitted) + * + * @return void + * + * @since 1.0.0 + */ + public function addQuery(string $key, ?string $value = null) : void + { + $key = \strtolower($key); + $this->query[$key] = $value; + + $toAdd = (empty($this->queryString) ? '?' : '&') + . $key + . ($value === null ? '' : '=' . ((string) $value)); + + $this->queryString .= $toAdd; + + if (empty($this->fragment)) { + $this->uri .= $toAdd; + } else { + $this->uri = \substr_replace($this->uri, $toAdd, \strrpos($this->uri, '#'), 0); + } + } + /** * {@inheritdoc} */ diff --git a/Uri/InvalidUriException.php b/Uri/InvalidUriException.php index 326c88876..1ed36c379 100755 --- a/Uri/InvalidUriException.php +++ b/Uri/InvalidUriException.php @@ -33,7 +33,7 @@ class InvalidUriException extends \UnexpectedValueException * * @since 1.0.0 */ - public function __construct(string $message, int $code = 0, \Exception $previous = null) + public function __construct(string $message, int $code = 0, ?\Exception $previous = null) { parent::__construct('The uri "' . $message . '" is not valid.', $code, $previous); } diff --git a/Uri/UriFactory.php b/Uri/UriFactory.php index 7a13ca3b1..ad22286a2 100755 --- a/Uri/UriFactory.php +++ b/Uri/UriFactory.php @@ -29,7 +29,7 @@ final class UriFactory /** * Dynamic query elements. * - * @var string[] + * @var array * @since 1.0.0 */ private static array $uri = []; @@ -105,15 +105,15 @@ final class UriFactory /** * Set global query replacements. * - * @param string $key Replacement key - * @param string $value Replacement value - * @param bool $overwrite Overwrite if already exists + * @param string $key Replacement key + * @param null|string $value Replacement value + * @param bool $overwrite Overwrite if already exists * * @return bool * * @since 1.0.0 */ - public static function setQuery(string $key, string $value, bool $overwrite = false) : bool + public static function setQuery(string $key, ?string $value, bool $overwrite = false) : bool { if ($overwrite || !isset(self::$uri[$key])) { self::$uri[$key] = $value; @@ -257,8 +257,7 @@ final class UriFactory } } - $escaped = - (isset($urlStructure['scheme']) && !empty($urlStructure['scheme']) + $escaped = (isset($urlStructure['scheme']) && !empty($urlStructure['scheme']) ? $urlStructure['scheme'] . '://' : '') . (isset($urlStructure['username']) ? $urlStructure['username'] . ':' : '') diff --git a/Uri/UriInterface.php b/Uri/UriInterface.php index da6a92a59..66efac66e 100755 --- a/Uri/UriInterface.php +++ b/Uri/UriInterface.php @@ -168,12 +168,12 @@ interface UriInterface * * @since 1.0.0 */ - public function getQuery(string $key = null) : string; + public function getQuery(?string $key = null) : string; /** * Get query array. * - * @return string[] + * @return array * * @since 1.0.0 */ diff --git a/Utils/ArrayUtils.php b/Utils/ArrayUtils.php index fcca3e93a..7217ff1cb 100755 --- a/Utils/ArrayUtils.php +++ b/Utils/ArrayUtils.php @@ -14,8 +14,6 @@ declare(strict_types=1); namespace phpOMS\Utils; -use phpOMS\Math\Matrix\Exception\InvalidDimensionException; - /** * Array utils. * @@ -281,7 +279,7 @@ final class ArrayUtils * * @since 1.0.0 */ - public static function arrayToXml(array $data, \SimpleXMLElement $xml = null) : string + public static function arrayToXml(array $data, ?\SimpleXMLElement $xml = null) : string { $xml ??= new \SimpleXMLElement(''); @@ -454,6 +452,26 @@ final class ArrayUtils return $squared; } + /** + * Get a column of 2D matrix. + * + * @param array $matrix Matrix + * @param mixed $index Column index to return + * + * @return array + * + * @since 1.0.0 + */ + public static function column(array $matrix, mixed $index) : array + { + $column = []; + foreach ($matrix as $id => $row) { + $column[$id] = $row[$index]; + } + + return $column; + } + /** * Sqrt all values in array. * @@ -504,90 +522,4 @@ final class ArrayUtils return $diff; } - - /** - * Get the dot product of two arrays - * - * @param array $value1 Value 1 is a matrix or a vector - * @param array $value2 Value 2 is a matrix or vector (cannot be a matrix if value1 is a vector) - * - * @return array - * - * @throws InvalidDimensionException - * @throws \InvalidArgumentException - * - * @since 1.0.0 - */ - public static function dot(array $value1, array $value2) : int|float|array - { - $m1 = \count($value1); - $n1 = ($isMatrix1 = \is_array($value1[0])) ? \count($value1[0]) : 1; - - $m2 = \count($value2); - $n2 = ($isMatrix2 = \is_array($value2[0])) ? \count($value2[0]) : 1; - - $result = null; - - if ($isMatrix1 && $isMatrix2) { - if ($m2 !== $n1) { - throw new InvalidDimensionException($m2 . 'x' . $n2 . ' not compatible with ' . $m1 . 'x' . $n1); - } - - $result = [[]]; - for ($i = 0; $i < $m1; ++$i) { // Row of 1 - for ($c = 0; $c < $n2; ++$c) { // Column of 2 - $temp = 0; - - for ($j = 0; $j < $m2; ++$j) { // Row of 2 - $temp += $value1[$i][$j] * $value2[$j][$c]; - } - - $result[$i][$c] = $temp; - } - } - } elseif (!$isMatrix1 && !$isMatrix2) { - if ($m1 !== $m2) { - throw new InvalidDimensionException($m1 . ' vs. ' . $m2); - } - - $result = 0; - for ($i = 0; $i < $m1; ++$i) { - $result += $value1[$i] * $value2[$i]; - } - } elseif ($isMatrix1 && !$isMatrix2) { - $result = []; - for ($i = 0; $i < $m1; ++$i) { // Row of 1 - $temp = 0; - - for ($c = 0; $c < $m2; ++$c) { // Row of 2 - $temp += $value1[$i][$c] * $value2[$c]; - } - - $result[$i] = $temp; - } - } else { - throw new \InvalidArgumentException(); - } - - return $result; - } - - /** - * Calculate the vector corss product - * - * @param array $vector1 First 3 vector - * @param array $vector2 Second 3 vector - * - * @return array - * - * @since 1.0.0 - */ - public function cross3(array $vector1, array $vector2) : array - { - return [ - $vector1[1] * $vector2[2] - $vector1[2] * $vector2[1], - $vector1[2] * $vector2[0] - $vector1[0] * $vector2[2], - $vector1[0] * $vector2[1] - $vector1[1] * $vector2[0], - ]; - } } diff --git a/Utils/Barcode/BarAbstract.php b/Utils/Barcode/BarAbstract.php index e828d9cae..19025e28c 100755 --- a/Utils/Barcode/BarAbstract.php +++ b/Utils/Barcode/BarAbstract.php @@ -115,9 +115,9 @@ abstract class BarAbstract extends CodeAbstract $checksum = static::$CHECKSUM; for ($pos = 1; $pos <= $length; ++$pos) { - $activeKey = \substr($this->content, ($pos - 1), 1); + $activeKey = \substr($this->content, ($pos - 1), 1); $this->codestring .= static::$CODEARRAY[$activeKey]; - $checksum += $values[$activeKey] * $pos; + $checksum += $values[$activeKey] * $pos; } $this->codestring .= static::$CODEARRAY[$keys[($checksum - ((int) ($checksum / 103) * 103))]]; diff --git a/Utils/Barcode/C128c.php b/Utils/Barcode/C128c.php index 5236e9938..d4a202990 100755 --- a/Utils/Barcode/C128c.php +++ b/Utils/Barcode/C128c.php @@ -97,7 +97,7 @@ class C128c extends BarAbstract : \substr($this->content, ($pos - 1), 1) . '0'; $codeString .= self::$CODEARRAY[$activeKey]; - $checksum += $values[$activeKey] * $checkPos; + $checksum += $values[$activeKey] * $checkPos; ++$checkPos; } diff --git a/Utils/Barcode/Datamatrix.php b/Utils/Barcode/Datamatrix.php index c9c8c2cbd..8c2115d0b 100755 --- a/Utils/Barcode/Datamatrix.php +++ b/Utils/Barcode/Datamatrix.php @@ -487,7 +487,7 @@ class Datamatrix extends TwoDAbstract if ($this->isCharMode($chr, self::ENC_ASCII_NUM)) { $numch[self::ENC_ASCII] += (1 / 2); } elseif ($this->isCharMode($chr, self::ENC_ASCII_EXT)) { - $numch[self::ENC_ASCII] = \ceil($numch[self::ENC_ASCII]); + $numch[self::ENC_ASCII] = \ceil($numch[self::ENC_ASCII]); $numch[self::ENC_ASCII] += 2; } else { $numch[self::ENC_ASCII] = \ceil($numch[self::ENC_ASCII]); @@ -711,8 +711,8 @@ class Datamatrix extends TwoDAbstract if ($this->isCharMode($chr, self::ENC_ASCII_EXT)) { // 3. If the next data character is extended ASCII (greater than 127) encode it in ASCII mode first using the Upper Shift (value 235) character. - $cw[] = 235; - $cw[] = ($chr - 127); + $cw[] = 235; + $cw[] = ($chr - 127); $cw_num += 2; } else { // 4. Otherwise process the next data character in ASCII encodation. @@ -747,10 +747,10 @@ class Datamatrix extends TwoDAbstract return []; } - $chr &= 0x7f; + $chr &= 0x7f; $temp_cw[] = 1; // shift 2 $temp_cw[] = 30; // upper shift - $p += 2; + $p += 2; } if (isset($charset[$chr])) { @@ -774,19 +774,19 @@ class Datamatrix extends TwoDAbstract } $temp_cw[] = $shiftset[$chr]; - $p += 2; + $p += 2; } if ($p >= 3) { - $c1 = \array_shift($temp_cw); - $c2 = \array_shift($temp_cw); - $c3 = \array_shift($temp_cw); - $p -= 3; - $tmp = ((1600 * $c1) + (40 * $c2) + $c3 + 1); - $cw[] = ($tmp >> 8); - $cw[] = ($tmp % 256); + $c1 = \array_shift($temp_cw); + $c2 = \array_shift($temp_cw); + $c3 = \array_shift($temp_cw); + $p -= 3; + $tmp = ((1600 * $c1) + (40 * $c2) + $c3 + 1); + $cw[] = ($tmp >> 8); + $cw[] = ($tmp % 256); $cw_num += 2; - $pos = $epos; + $pos = $epos; // 1. If the C40 encoding is at the point of starting a new double symbol character and if the look-ahead test (starting at step J) indicates another mode, switch to that mode. $newenc = $this->lookAheadTest($data, $pos, $enc); @@ -802,7 +802,7 @@ class Datamatrix extends TwoDAbstract $cw[] = $this->getSwitchEncodingCodeword($enc); $pos -= $p; - $p = 0; + $p = 0; break; } @@ -828,22 +828,22 @@ class Datamatrix extends TwoDAbstract // c. If two symbol characters remain and only one C40 value (data character) remains to be encoded --$p; - $c1 = \array_shift($temp_cw); - $cw[] = 254; - $cw[] = ($chr + 1); - $cw_num += 2; + $c1 = \array_shift($temp_cw); + $cw[] = 254; + $cw[] = ($chr + 1); + $cw_num += 2; $pos = $epos; $enc = self::ENC_ASCII; $this->encoding = $enc; } elseif (($cwr === 2) && ($p === 2)) { // b. If two symbol characters remain and two C40 values remain to be encoded - $c1 = \array_shift($temp_cw); - $c2 = \array_shift($temp_cw); - $p -= 2; - $tmp = ((1600 * $c1) + (40 * $c2) + 1); - $cw[] = ($tmp >> 8); - $cw[] = ($tmp % 256); - $cw_num += 2; + $c1 = \array_shift($temp_cw); + $c2 = \array_shift($temp_cw); + $p -= 2; + $tmp = ((1600 * $c1) + (40 * $c2) + 1); + $cw[] = ($tmp >> 8); + $cw[] = ($tmp % 256); + $cw_num += 2; $pos = $epos; $enc = self::ENC_ASCII; $this->encoding = $enc; @@ -962,8 +962,8 @@ class Datamatrix extends TwoDAbstract $cw[] = $this->get255StateCodeword($field_length, ($cw_num + 1)); ++$cw_num; } else { - $cw[] = $this->get255StateCodeword((int) (\floor($field_length / 250) + 249), ($cw_num + 1)); - $cw[] = $this->get255StateCodeword(($field_length % 250), ($cw_num + 2)); + $cw[] = $this->get255StateCodeword((int) (\floor($field_length / 250) + 249), ($cw_num + 1)); + $cw[] = $this->get255StateCodeword(($field_length % 250), ($cw_num + 2)); $cw_num += 2; } diff --git a/Utils/Barcode/QR.php b/Utils/Barcode/QR.php index 6dc3f5d10..8530565f7 100755 --- a/Utils/Barcode/QR.php +++ b/Utils/Barcode/QR.php @@ -613,7 +613,7 @@ class QR extends TwoDAbstract for ($j = 0; $j < 8; ++$j) { $addr = $this->getNextPosition(); $this->frame[$addr['y']][$addr['x']] = 0x02 | (($bit & $code) !== 0); - $bit >>= 1; + $bit >>= 1; } } @@ -685,8 +685,8 @@ class QR extends TwoDAbstract if ($this->dir < 0) { if ($y < 0) { - $y = 0; - $x -= 2; + $y = 0; + $x -= 2; $this->dir = 1; if ($x === 6) { @@ -695,8 +695,8 @@ class QR extends TwoDAbstract } } } elseif ($y === $w) { - $y = $w - 1; - $x -= 2; + $y = $w - 1; + $x -= 2; $this->dir = -1; if ($x === 6) { @@ -746,8 +746,8 @@ class QR extends TwoDAbstract $ecc = $this->encode_rs_char($rs, $this->rsblocks[$blockNo]['data'], $ecc); $this->rsblocks[$blockNo]['ecc'] = $ecc; $this->ecccode = \array_merge(\array_slice($this->ecccode, 0, $eccPos), $ecc); - $dataPos += $dl; - $eccPos += $el; + $dataPos += $dl; + $eccPos += $el; ++$blockNo; } @@ -773,8 +773,8 @@ class QR extends TwoDAbstract $ecc = $this->encode_rs_char($rs, $this->rsblocks[$blockNo]['data'], $ecc); $this->rsblocks[$blockNo]['ecc'] = $ecc; $this->ecccode = \array_merge(\array_slice($this->ecccode, 0, $eccPos), $ecc); - $dataPos += $dl; - $eccPos += $el; + $dataPos += $dl; + $eccPos += $el; ++$blockNo; } @@ -830,7 +830,7 @@ class QR extends TwoDAbstract for ($i = 0; $i < 8; ++$i) { if (($format & 1) !== 0) { $blacks += 2; - $v = 0x85; + $v = 0x85; } else { $v = 0x84; } @@ -848,7 +848,7 @@ class QR extends TwoDAbstract for ($i = 0; $i < 7; ++$i) { if (($format & 1) !== 0) { $blacks += 2; - $v = 0x85; + $v = 0x85; } else { $v = 0x84; } @@ -1090,7 +1090,7 @@ class QR extends TwoDAbstract if (self::QR_FIND_FROM_RANDOM !== false) { $howManuOut = 8 - (self::QR_FIND_FROM_RANDOM % 9); for ($i = 0; $i < $howManuOut; ++$i) { - // @note: This is why the same content can result in different QR codes + // @note This is why the same content can result in different QR codes $remPos = \array_rand($checked_masks, 1); unset($checked_masks[$remPos]); $checked_masks = \array_values($checked_masks); @@ -1099,13 +1099,13 @@ class QR extends TwoDAbstract $bestMask = $frame; foreach ($checked_masks as $i) { - $mask = \array_fill(0, $width, \str_repeat("\0", $width)); - $demerit = 0; - $blacks = 0; - $blacks = $this->makeMaskNo($i, $width, $frame, $mask); - $blacks += $this->writeFormatInformation($width, $mask, $i, $level); - $blacks = (int) (100 * $blacks / ($width * $width)); - $demerit = (int) ((int) (\abs($blacks - 50) / 5) * self::N4); + $mask = \array_fill(0, $width, \str_repeat("\0", $width)); + $demerit = 0; + $blacks = 0; + $blacks = $this->makeMaskNo($i, $width, $frame, $mask); + $blacks += $this->writeFormatInformation($width, $mask, $i, $level); + $blacks = (int) (100 * $blacks / ($width * $width)); + $demerit = (int) ((int) (\abs($blacks - 50) / 5) * self::N4); $demerit += $this->evaluateSymbol($width, $mask); if ($demerit < $minDemerit) { @@ -1473,7 +1473,7 @@ class QR extends TwoDAbstract $ord0 = \ord('0'); for ($i = 0; $i < $words; ++$i) { - $val = (\ord($inputitem['data'][$i * 3 ]) - $ord0) * 100; + $val = (\ord($inputitem['data'][$i * 3 ]) - $ord0) * 100; $val += (\ord($inputitem['data'][$i * 3 + 1]) - $ord0) * 10; $val += (\ord($inputitem['data'][$i * 3 + 2]) - $ord0); @@ -1484,7 +1484,7 @@ class QR extends TwoDAbstract $val = \ord($inputitem['data'][$words * 3]) - $ord0; $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, $val); } elseif (($inputitem['size'] - ($words * 3)) === 2) { - $val = (\ord($inputitem['data'][$words * 3 ]) - $ord0) * 10; + $val = (\ord($inputitem['data'][$words * 3 ]) - $ord0) * 10; $val += (\ord($inputitem['data'][$words * 3 + 1]) - $ord0); $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 7, $val); @@ -1509,7 +1509,7 @@ class QR extends TwoDAbstract $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], $this->lengthIndicator(self::QR_MODE_AN, $version), $inputitem['size']); for ($i = 0; $i < $words; ++$i) { - $val = $this->lookAnTable(\ord($inputitem['data'][$i * 2])) * 45; + $val = $this->lookAnTable(\ord($inputitem['data'][$i * 2])) * 45; $val += $this->lookAnTable(\ord($inputitem['data'][($i * 2) + 1])); $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 11, $val); @@ -1537,7 +1537,7 @@ class QR extends TwoDAbstract $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, 0x4); $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], $this->lengthIndicator(self::QR_MODE_8B, $version), $inputitem['size']); - for ($i=0; $i < $inputitem['size']; ++$i) { + for ($i = 0; $i < $inputitem['size']; ++$i) { $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 8, \ord($inputitem['data'][$i])); } @@ -1900,9 +1900,9 @@ class QR extends TwoDAbstract return 0; } - $l = $this->lengthIndicator($item['mode'], $version); - $m = 1 << $l; - $num = (int) (($item['size'] + $m - 1) / $m); + $l = $this->lengthIndicator($item['mode'], $version); + $m = 1 << $l; + $num = (int) (($item['size'] + $m - 1) / $m); $bits += $num * (4 + $l); } @@ -2006,7 +2006,7 @@ class QR extends TwoDAbstract foreach ($items as $key => $item) { $items[$key] = $this->encodeBitStream($item, $this->version); $bits = \count($items[$key]['bstream']); - $total += $bits; + $total += $bits; } return [$items, $total]; @@ -2073,7 +2073,7 @@ class QR extends TwoDAbstract return $this->appendNum($bstream, $maxbits - $bits, 0); } - $bits += 4; + $bits += 4; $words = (int) (($bits + 7) / 8); $padding = []; $padding = $this->appendNum($padding, $words * 8 - $bits + 4, 0); @@ -2168,7 +2168,7 @@ class QR extends TwoDAbstract for ($i = 0; $i < $bits; ++$i) { $bstream[$i] = (($num & $mask) !== 0) ? 1 : 0; - $mask >>= 1; + $mask >>= 1; } return $bstream; @@ -2279,7 +2279,7 @@ class QR extends TwoDAbstract for ($j = 0; $j < 8; ++$j) { $v <<= 1; - $v |= $bstream[$p]; + $v |= $bstream[$p]; ++$p; } @@ -2291,7 +2291,7 @@ class QR extends TwoDAbstract for ($j = 0; $j < ($size & 7); ++$j) { $v <<= 1; - $v |= $bstream[$p]; + $v |= $bstream[$p]; ++$p; } @@ -2580,7 +2580,7 @@ class QR extends TwoDAbstract for ($x = 0; $x < 6; ++$x) { for ($y = 0; $y < 3; ++$y) { $frame[($width - 11) + $y][$x] = \chr(0x88 | ($v & 1)); - $v >>= 1; + $v >>= 1; } } @@ -2588,7 +2588,7 @@ class QR extends TwoDAbstract for ($y = 0; $y < 6; ++$y) { for ($x = 0; $x < 3; ++$x) { $frame[$y][$x + ($width - 11)] = \chr(0x88 | ($v & 1)); - $v >>= 1; + $v >>= 1; } } } @@ -2659,7 +2659,7 @@ class QR extends TwoDAbstract { while ($x >= $rs['nn']) { $x -= $rs['nn']; - $x = ($x >> $rs['mm']) + ($x & $rs['nn']); + $x = ($x >> $rs['mm']) + ($x & $rs['nn']); } return $x; @@ -2693,8 +2693,8 @@ class QR extends TwoDAbstract $rs['index_of'] = \array_fill(0, ($rs['nn'] + 1), 0); // PHP style macro replacement ;) - $NN =& $rs['nn']; - $A0 =& $NN; + $NN = & $rs['nn']; + $A0 = & $NN; // Generate Galois field lookup tables $rs['index_of'][0] = $A0; // log(zero) = -inf @@ -2704,7 +2704,7 @@ class QR extends TwoDAbstract for ($i = 0; $i < $rs['nn']; ++$i) { $rs['index_of'][$sr] = $i; $rs['alpha_to'][$i] = $sr; - $sr <<= 1; + $sr <<= 1; if (($sr & (1 << $symsize)) !== 0) { $sr ^= $gfpoly; @@ -2767,17 +2767,17 @@ class QR extends TwoDAbstract */ protected function encode_rs_char(array $rs, array $data, array $parity) : array { - $MM =& $rs['mm']; // bits per symbol - $NN =& $rs['nn']; // the total number of symbols in a RS block - $ALPHA_TO =& $rs['alpha_to']; // the address of an array of NN elements to convert Galois field elements in index (log) form to polynomial form - $INDEX_OF =& $rs['index_of']; // the address of an array of NN elements to convert Galois field elements in polynomial form to index (log) form - $GENPOLY =& $rs['genpoly']; // an array of NROOTS+1 elements containing the generator polynomial in index form - $NROOTS =& $rs['nroots']; // the number of roots in the RS code generator polynomial, which is the same as the number of parity symbols in a block - $FCR =& $rs['fcr']; // first consecutive root, index form - $PRIM =& $rs['prim']; // primitive element, index form - $IPRIM =& $rs['iprim']; // prim-th root of 1, index form - $PAD =& $rs['pad']; // the number of pad symbols in a block - $A0 =& $NN; + $MM = & $rs['mm']; // bits per symbol + $NN = & $rs['nn']; // the total number of symbols in a RS block + $ALPHA_TO = & $rs['alpha_to']; // the address of an array of NN elements to convert Galois field elements in index (log) form to polynomial form + $INDEX_OF = & $rs['index_of']; // the address of an array of NN elements to convert Galois field elements in polynomial form to index (log) form + $GENPOLY = & $rs['genpoly']; // an array of NROOTS+1 elements containing the generator polynomial in index form + $NROOTS = & $rs['nroots']; // the number of roots in the RS code generator polynomial, which is the same as the number of parity symbols in a block + $FCR = & $rs['fcr']; // first consecutive root, index form + $PRIM = & $rs['prim']; // primitive element, index form + $IPRIM = & $rs['iprim']; // prim-th root of 1, index form + $PAD = & $rs['pad']; // the number of pad symbols in a block + $A0 = & $NN; $parity = \array_fill(0, $NROOTS, 0); for ($i = 0; $i < $NN - $NROOTS - $PAD; ++$i) { diff --git a/Utils/Barcode/TwoDAbstract.php b/Utils/Barcode/TwoDAbstract.php index 5b369f610..8a9196b3f 100755 --- a/Utils/Barcode/TwoDAbstract.php +++ b/Utils/Barcode/TwoDAbstract.php @@ -80,7 +80,8 @@ abstract class TwoDAbstract extends CodeAbstract $locationX = $this->margin; - // @todo: Allow manual dimensions + // @todo Allow manual dimensions + // https://github.com/Karaka-Management/phpOMS/issues/346 for ($posX = 0; $posX < $width; ++$posX) { $locationY = $this->margin; diff --git a/Utils/ColorUtils.php b/Utils/ColorUtils.php index 818486cc9..37855ec11 100755 --- a/Utils/ColorUtils.php +++ b/Utils/ColorUtils.php @@ -65,7 +65,7 @@ final class ColorUtils */ public static function rgbToInt(array $rgb) : int { - $i = (255 & $rgb['r']) << 16; + $i = (255 & $rgb['r']) << 16; $i += (255 & $rgb['g']) << 8; $i += (255 & $rgb['b']); diff --git a/Utils/Compression/LZW.php b/Utils/Compression/LZW.php index 6a40a065c..0c4c80cd0 100755 --- a/Utils/Compression/LZW.php +++ b/Utils/Compression/LZW.php @@ -94,7 +94,7 @@ class LZW implements CompressionInterface throw new \Exception('Wrong dictionary size!' . $k . '.' . $dictSize); // @codeCoverageIgnore } - $result .= $entry; + $result .= $entry; $dictionary[$dictSize++] = $w . $entry[0]; $w = $entry; } diff --git a/Utils/Converter/EnergyPowerType.php b/Utils/Converter/EnergyPowerType.php index 807d16415..037437b1f 100755 --- a/Utils/Converter/EnergyPowerType.php +++ b/Utils/Converter/EnergyPowerType.php @@ -26,19 +26,19 @@ use phpOMS\Stdlib\Base\Enum; */ abstract class EnergyPowerType extends Enum { - public const KILOWATT_HOUERS = 'kWh'; + public const KILOWATT_HOURS = 'kWh'; - public const MEGAWATT_HOUERS = 'MWh'; + public const MEGAWATT_HOURS = 'MWh'; public const KILOTONS = 'kt'; - public const JOULS = 'J'; + public const JOULES = 'J'; public const CALORIES = 'Cal'; public const BTU = 'BTU'; - public const KILOJOULS = 'kJ'; + public const KILOJOULES = 'kJ'; public const THERMEC = 'thmEC'; diff --git a/Utils/Converter/Measurement.php b/Utils/Converter/Measurement.php index ae33a002a..9abaf33cd 100755 --- a/Utils/Converter/Measurement.php +++ b/Utils/Converter/Measurement.php @@ -1425,17 +1425,17 @@ final class Measurement * * @since 1.0.0 */ - public static function convertEnergy(float $value, string $from = EnergyPowerType::JOULS, string $to = EnergyPowerType::KILOWATT_HOUERS) : float + public static function convertEnergy(float $value, string $from = EnergyPowerType::JOULES, string $to = EnergyPowerType::KILOWATT_HOURS) : float { $value = self::normalizeEnergy($value, $from); switch ($to) { - case EnergyPowerType::JOULS: + case EnergyPowerType::JOULES: break; - case EnergyPowerType::KILOWATT_HOUERS: + case EnergyPowerType::KILOWATT_HOURS: $value *= 0.00000027778; break; - case EnergyPowerType::MEGAWATT_HOUERS: + case EnergyPowerType::MEGAWATT_HOURS: $value *= 0.00000000027778; break; case EnergyPowerType::KILOTONS: @@ -1447,7 +1447,7 @@ final class Measurement case EnergyPowerType::BTU: $value *= 0.00094782; break; - case EnergyPowerType::KILOJOULS: + case EnergyPowerType::KILOJOULES: $value *= 0.0010000; break; case EnergyPowerType::THERMEC: @@ -1477,12 +1477,12 @@ final class Measurement private static function normalizeEnergy(float $value, string $from) : float { switch ($from) { - case EnergyPowerType::JOULS: + case EnergyPowerType::JOULES: break; - case EnergyPowerType::KILOWATT_HOUERS: + case EnergyPowerType::KILOWATT_HOURS: $value /= 0.00000027778; break; - case EnergyPowerType::MEGAWATT_HOUERS: + case EnergyPowerType::MEGAWATT_HOURS: $value /= 0.00000000027778; break; case EnergyPowerType::KILOTONS: @@ -1494,7 +1494,7 @@ final class Measurement case EnergyPowerType::BTU: $value /= 0.00094782; break; - case EnergyPowerType::KILOJOULS: + case EnergyPowerType::KILOJOULES: $value /= 0.0010000; break; case EnergyPowerType::THERMEC: diff --git a/Utils/Converter/Numeric.php b/Utils/Converter/Numeric.php index d35a05c50..26206e86f 100755 --- a/Utils/Converter/Numeric.php +++ b/Utils/Converter/Numeric.php @@ -145,7 +145,7 @@ final class Numeric foreach (self::ROMANS as $key => $value) { while (\str_starts_with($roman, $key)) { $result += $value; - $temp = \substr($roman, \strlen($key)); + $temp = \substr($roman, \strlen($key)); if ($temp !== false) { $roman = $temp; @@ -172,7 +172,7 @@ final class Numeric $alpha = ''; for ($i = 1; $number >= 0 && $i < 10; ++$i) { - $alpha = \chr(0x41 + (int) ($number % \pow(26, $i) / \pow(26, $i - 1))) . $alpha; + $alpha = \chr(0x41 + (int) ($number % \pow(26, $i) / \pow(26, $i - 1))) . $alpha; $number -= \pow(26, $i); } diff --git a/Utils/Encoding/Base64Url.php b/Utils/Encoding/Base64Url.php index eb13cf429..d647a1647 100644 --- a/Utils/Encoding/Base64Url.php +++ b/Utils/Encoding/Base64Url.php @@ -24,6 +24,16 @@ namespace phpOMS\Utils\Encoding; */ final class Base64Url { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Encode source text * @@ -42,9 +52,9 @@ final class Base64Url } /** - * Dedecodes text + * Decodes text * - * @param string $b64 Encoded value to dedecode + * @param string $b64 Encoded value to decode * * @return string * diff --git a/Utils/Encoding/Caesar.php b/Utils/Encoding/Caesar.php index e7d13e6d7..ac8cba292 100755 --- a/Utils/Encoding/Caesar.php +++ b/Utils/Encoding/Caesar.php @@ -40,6 +40,16 @@ class Caesar */ public const LIMIT_UPPER = 127; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Encode source text * diff --git a/Utils/Encoding/EncodingInterface.php b/Utils/Encoding/EncodingInterface.php index 656a2922d..2b071827f 100755 --- a/Utils/Encoding/EncodingInterface.php +++ b/Utils/Encoding/EncodingInterface.php @@ -36,9 +36,9 @@ interface EncodingInterface public static function encode(mixed $source); /** - * Dedecodes text + * Decodes text * - * @param string $decoded encoded text to dedecode + * @param string $decoded encoded text to decode * * @return string * diff --git a/Utils/Encoding/Gray.php b/Utils/Encoding/Gray.php index a73bcde62..63a47b1fb 100755 --- a/Utils/Encoding/Gray.php +++ b/Utils/Encoding/Gray.php @@ -24,6 +24,16 @@ namespace phpOMS\Utils\Encoding; */ final class Gray { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Encode source text * @@ -39,9 +49,9 @@ final class Gray } /** - * Dedecodes text + * Decodes text * - * @param int $gray encoded value to dedecode + * @param int $gray encoded value to decode * * @return int * diff --git a/Utils/Encoding/Huffman/Huffman.php b/Utils/Encoding/Huffman/Huffman.php index cdf29374d..b44befdce 100755 --- a/Utils/Encoding/Huffman/Huffman.php +++ b/Utils/Encoding/Huffman/Huffman.php @@ -118,10 +118,10 @@ final class Huffman } $binary = ''; - $rawLenght = \strlen($raw); + $rawLength = \strlen($raw); $source = ''; - for ($i = 0; $i < $rawLenght; ++$i) { + for ($i = 0; $i < $rawLength; ++$i) { $decbin = \decbin(\ord($raw[$i])); while (\strlen($decbin) < 8) { @@ -141,7 +141,7 @@ final class Huffman } } - if ($i + 1 === $rawLenght) { + if ($i + 1 === $rawLength) { $pos = \strrpos($decbin, '1'); if ($pos === false) { diff --git a/Utils/Encoding/XorEncoding.php b/Utils/Encoding/XorEncoding.php index d7505e216..953d38191 100755 --- a/Utils/Encoding/XorEncoding.php +++ b/Utils/Encoding/XorEncoding.php @@ -24,6 +24,16 @@ namespace phpOMS\Utils\Encoding; */ final class XorEncoding { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Decode text * @@ -60,7 +70,7 @@ final class XorEncoding $j = 0; } - $ascii = \ord($source[$i]) ^ \ord($key[$j]); + $ascii = \ord($source[$i]) ^ \ord($key[$j]); $result .= \chr($ascii); } diff --git a/Utils/Formatter/HtmlFormatter.php b/Utils/Formatter/HtmlFormatter.php index 3cb6bbf79..f8c396b1b 100644 --- a/Utils/Formatter/HtmlFormatter.php +++ b/Utils/Formatter/HtmlFormatter.php @@ -22,8 +22,18 @@ namespace phpOMS\Utils\Formatter; * @link https://jingga.app * @since 1.0.0 */ -class HtmlFormatter +final class HtmlFormatter { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Format html code * diff --git a/Utils/Git/Repository.php b/Utils/Git/Repository.php index 92e584666..3efbacdef 100755 --- a/Utils/Git/Repository.php +++ b/Utils/Git/Repository.php @@ -254,7 +254,7 @@ class Repository * * @since 1.0.0 */ - public function create(string $source = null) : void + public function create(?string $source = null) : void { if (!\is_dir($this->path) || \is_dir($this->path . '/.git')) { throw new \Exception('Already repository'); @@ -690,15 +690,10 @@ class Repository * * @since 1.0.0 */ - public function getContributors(\DateTime $start = null, \DateTime $end = null) : array + public function getContributors(?\DateTime $start = null, ?\DateTime $end = null) : array { - if ($start === null) { - $start = new \DateTime('1970-12-31'); - } - - if ($end === null) { - $end = new \DateTime('now'); - } + $start ??= new \DateTime('1970-12-31'); + $end ??= new \DateTime('now'); $lines = $this->run('shortlog -s -n --since="' . $start->format('Y-m-d') . '" --before="' . $end->format('Y-m-d') . '" --all'); $contributors = []; @@ -730,15 +725,10 @@ class Repository * * @since 1.0.0 */ - public function getCommitsCount(\DateTime $start = null, \DateTime $end = null) : array + public function getCommitsCount(?\DateTime $start = null, ?\DateTime $end = null) : array { - if ($start === null) { - $start = new \DateTime('1970-12-31'); - } - - if ($end === null) { - $end = new \DateTime('now'); - } + $start ??= new \DateTime('1970-12-31'); + $end ??= new \DateTime('now'); $lines = $this->run('shortlog -s -n --since="' . $start->format('Y-m-d') . '" --before="' . $end->format('Y-m-d') . '" --all'); $commits = []; @@ -766,15 +756,10 @@ class Repository * * @since 1.0.0 */ - public function getAdditionsRemovalsByContributor(Author $author, \DateTime $start = null, \DateTime $end = null) : array + public function getAdditionsRemovalsByContributor(Author $author, ?\DateTime $start = null, ?\DateTime $end = null) : array { - if ($start === null) { - $start = new \DateTime('1900-01-01'); - } - - if ($end === null) { - $end = new \DateTime('now'); - } + $start ??= new \DateTime('1900-01-01'); + $end ??= new \DateTime('now'); $addremove = ['added' => 0, 'removed' => 0]; $lines = $this->run( @@ -817,15 +802,10 @@ class Repository * * @since 1.0.0 */ - public function getCommitsBy(\DateTime $start = null, \DateTime $end = null, Author $author = null) : array + public function getCommitsBy(?\DateTime $start = null, ?\DateTime $end = null, ?Author $author = null) : array { - if ($start === null) { - $start = new \DateTime('1970-12-31'); - } - - if ($end === null) { - $end = new \DateTime('now'); - } + $start ??= new \DateTime('1970-12-31'); + $end ??= new \DateTime('now'); $author = $author === null ? '' : ' --author=' . \escapeshellarg($author->name) . ''; diff --git a/Utils/IO/Csv/CsvDatabaseMapper.php b/Utils/IO/Csv/CsvDatabaseMapper.php index f746d8683..5f884db05 100644 --- a/Utils/IO/Csv/CsvDatabaseMapper.php +++ b/Utils/IO/Csv/CsvDatabaseMapper.php @@ -26,7 +26,7 @@ use phpOMS\Utils\IO\IODatabaseMapper; * @link https://jingga.app * @since 1.0.0 */ -class CsvDatabaseMapper implements IODatabaseMapper +final class CsvDatabaseMapper implements IODatabaseMapper { /** * Database connection diff --git a/Utils/IO/Json/InvalidJsonException.php b/Utils/IO/Json/InvalidJsonException.php index 8899d571d..9a0a24bda 100755 --- a/Utils/IO/Json/InvalidJsonException.php +++ b/Utils/IO/Json/InvalidJsonException.php @@ -22,7 +22,7 @@ namespace phpOMS\Utils\IO\Json; * @link https://jingga.app * @since 1.0.0 */ -class InvalidJsonException extends \UnexpectedValueException +final class InvalidJsonException extends \UnexpectedValueException { /** * Constructor. @@ -33,7 +33,7 @@ class InvalidJsonException extends \UnexpectedValueException * * @since 1.0.0 */ - public function __construct($message, $code = 0, \Exception $previous = null) + public function __construct($message, $code = 0, ?\Exception $previous = null) { parent::__construct('Couldn\'t parse "' . $message . '" as valid json.', $code, $previous); } diff --git a/Utils/IO/Spreadsheet/SpreadsheetDatabaseMapper.php b/Utils/IO/Spreadsheet/SpreadsheetDatabaseMapper.php index 3f460740e..dfdcba4c5 100755 --- a/Utils/IO/Spreadsheet/SpreadsheetDatabaseMapper.php +++ b/Utils/IO/Spreadsheet/SpreadsheetDatabaseMapper.php @@ -27,7 +27,7 @@ use phpOMS\Utils\StringUtils; * @link https://jingga.app * @since 1.0.0 */ -class SpreadsheetDatabaseMapper implements IODatabaseMapper +final class SpreadsheetDatabaseMapper implements IODatabaseMapper { /** * Database connection @@ -123,8 +123,8 @@ class SpreadsheetDatabaseMapper implements IODatabaseMapper { $sheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $sheet->getProperties() - ->setCreator('Karaka') - ->setLastModifiedBy('Karaka') + ->setCreator('Jingga') + ->setLastModifiedBy('Jingga') ->setTitle('Database export') ->setSubject('Database export') ->setDescription('This document is automatically generated from a database export.'); diff --git a/Utils/IO/Zip/Gz.php b/Utils/IO/Zip/Gz.php index 24b8cfd95..11144277e 100755 --- a/Utils/IO/Zip/Gz.php +++ b/Utils/IO/Zip/Gz.php @@ -24,8 +24,18 @@ namespace phpOMS\Utils\IO\Zip; * @link https://jingga.app * @since 1.0.0 */ -class Gz implements ArchiveInterface +final class Gz implements ArchiveInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * {@inheritdoc} */ diff --git a/Utils/IO/Zip/Tar.php b/Utils/IO/Zip/Tar.php index 98fefacf5..a9b7380f3 100755 --- a/Utils/IO/Zip/Tar.php +++ b/Utils/IO/Zip/Tar.php @@ -18,12 +18,13 @@ use phpOMS\System\File\FileUtils; use phpOMS\System\File\Local\Directory; /** - * Zip class for handling zip files. + * Tar class for handling zip files. * * Providing basic zip support * * IMPORTANT: - * PharData seems to cache created files, which means even if the previously created file is deleted, it cannot create a new file with the same destination. + * PharData seems to cache created files, which means even if the previously created file is deleted, + * it cannot create a new file with the same destination. * bug? https://bugs.php.net/bug.php?id=75101 * * @package phpOMS\Utils\IO\Zip @@ -31,8 +32,18 @@ use phpOMS\System\File\Local\Directory; * @link https://jingga.app * @since 1.0.0 */ -class Tar implements ArchiveInterface +final class Tar implements ArchiveInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * {@inheritdoc} */ diff --git a/Utils/IO/Zip/TarGz.php b/Utils/IO/Zip/TarGz.php index 27fcf58c5..b960a2838 100755 --- a/Utils/IO/Zip/TarGz.php +++ b/Utils/IO/Zip/TarGz.php @@ -26,8 +26,18 @@ use phpOMS\System\File\Local\File; * @link https://jingga.app * @since 1.0.0 */ -class TarGz implements ArchiveInterface +final class TarGz implements ArchiveInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * {@inheritdoc} */ diff --git a/Utils/IO/Zip/Zip.php b/Utils/IO/Zip/Zip.php index 1bb1a4132..d1b2a80ae 100755 --- a/Utils/IO/Zip/Zip.php +++ b/Utils/IO/Zip/Zip.php @@ -27,8 +27,18 @@ use phpOMS\System\File\Local\Directory; * @link https://jingga.app * @since 1.0.0 */ -class Zip implements ArchiveInterface +final class Zip implements ArchiveInterface { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * {@inheritdoc} */ diff --git a/Utils/ImageUtils.php b/Utils/ImageUtils.php index 72d596ad2..0f7ca4575 100755 --- a/Utils/ImageUtils.php +++ b/Utils/ImageUtils.php @@ -315,7 +315,7 @@ final class ImageUtils //$color1 = \imagecolorat($src1, $i, $j); $color2 = \imagecolorat($src2, $i, $j); - if (\abs($color1Avg - $color2Avg) / $color1Avg > 0.05 && $color1Avg > 0 && $color2Avg > 0) { + if ($color1Avg > 0 && \abs($color1Avg - $color2Avg) / $color1Avg > 0.05 && $color2Avg > 0) { ++$difference; if ($diff === 0) { diff --git a/Utils/MbStringUtils.php b/Utils/MbStringUtils.php index 6748f4e43..94139b509 100755 --- a/Utils/MbStringUtils.php +++ b/Utils/MbStringUtils.php @@ -242,7 +242,7 @@ final class MbStringUtils $countChars = self::mb_count_chars($value); foreach ($countChars as $v) { - $p = $v / $size; + $p = $v / $size; $entropy -= $p * \log($p) / \log(2); } diff --git a/Utils/NumericUtils.php b/Utils/NumericUtils.php index fc41d4f0e..27adc32f7 100755 --- a/Utils/NumericUtils.php +++ b/Utils/NumericUtils.php @@ -47,7 +47,7 @@ final class NumericUtils public static function uRightShift(int $a, int $b) : int { if ($b >= 32 || $b < -32) { - $m = (int) ($b / 32); + $m = (int) ($b / 32); $b -= $m * 32; } @@ -61,8 +61,8 @@ final class NumericUtils if ($a < 0) { $a >>= 1; - $a &= 0x7fffffff; - $a |= 0x40000000; + $a &= 0x7fffffff; + $a |= 0x40000000; $a >>= $b - 1; } else { $a >>= $b; diff --git a/Utils/Parser/Calendar/ICalParser.php b/Utils/Parser/Calendar/ICalParser.php index 9c5015a90..86d730bce 100755 --- a/Utils/Parser/Calendar/ICalParser.php +++ b/Utils/Parser/Calendar/ICalParser.php @@ -22,8 +22,18 @@ namespace phpOMS\Utils\Parser\Calendar; * @link https://jingga.app * @since 1.0.0 */ -class ICalParser +final class ICalParser { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Parse iCal data * @@ -42,32 +52,42 @@ class ICalParser foreach ($matches as $match) { $event = []; - \preg_match('/UID:(.*?)\r\n/', $match[1], $uidMatch); - $event['uid'] = \DateTime::createFromFormat('Ymd\THis', $uidMatch[1]); + \preg_match('/UID:(.*?)\n/', $match[1], $uidMatch); + $event['uid'] = $uidMatch[1] ?? null; - \preg_match('/STATUS:(.*?)\r\n/', $match[1], $statusMatch); - $event['status'] = \DateTime::createFromFormat('Ymd\THis', $statusMatch[1]); + \preg_match('/STATUS:(.*?)\n/', $match[1], $statusMatch); + $event['status'] = $statusMatch[1] ?? null; - \preg_match('/DTSTART:(.*?)\r\n/', $match[1], $startMatch); - $event['start'] = \DateTime::createFromFormat('Ymd\THis', $startMatch[1]); + \preg_match('/DTSTART:(.*?)\n/', $match[1], $startMatch); + $event['start'] = $startMatch[1] ?? null; - \preg_match('/DTEND:(.*?)\r\n/', $match[1], $endMatch); - $event['end'] = \DateTime::createFromFormat('Ymd\THis', $endMatch[1]); + \preg_match('/DTEND:(.*?)\n/', $match[1], $endMatch); + $event['end'] = $endMatch[1] ?? null; - \preg_match('/ORGANIZER:(.*?)\r\n/', $match[1], $organizerMatch); - $event['organizer'] = \DateTime::createFromFormat('Ymd\THis', $organizerMatch[1]); + \preg_match('/ORGANIZER:(.*?)\n/', $match[1], $organizerMatch); + $event['organizer'] = $organizerMatch[1] ?? null; - \preg_match('/SUMMARY:(.*?)\r\n/', $match[1], $summaryMatch); - $event['summary'] = $summaryMatch[1]; + \preg_match('/SUMMARY:(.*?)\n/', $match[1], $summaryMatch); + $event['summary'] = $summaryMatch[1] ?? null; - \preg_match('/DESCRIPTION:(.*?)\r\n/', $match[1], $descriptionMatch); - $event['description'] = $descriptionMatch[1]; + \preg_match('/DESCRIPTION:(.*?)\n/', $match[1], $descriptionMatch); + $event['description'] = $descriptionMatch[1] ?? null; - \preg_match('/LOCATION:(.*?)\r\n/', $match[1], $locationMatch); - $event['location'] = $locationMatch[1]; + \preg_match('/LOCATION:(.*?)\n/', $match[1], $locationMatch); + $event['location'] = $locationMatch[1] ?? null; + + \preg_match('/GEO:(.*?)\n/', $match[1], $geo); + $temp = \explode(';', $geo[1]); + $event['geo'] = [ + 'lat' => (float) \trim($temp[0] ?? '0'), + 'lon' => (float) \trim($temp[1] ?? '0'), + ]; + + \preg_match('/URL:(.*?)\n/', $match[1], $url); + $event['url'] = $url[1] ?? null; // Check if this event is recurring - if (\preg_match('/RRULE:(.*?)\r\n/', $match[1], $rruleMatch)) { + if (\preg_match('/RRULE:(.*?)\n/', $match[1], $rruleMatch)) { $rrule = self::parseRRule($rruleMatch[1]); $event = \array_merge($event, $rrule); } @@ -92,18 +112,24 @@ class ICalParser $rrule = []; \preg_match('/FREQ=(.*?);/', $rruleString, $freqMatch); - $rrule['freq'] = $freqMatch[1]; + $rrule['freq'] = $freqMatch[1] ?? null; \preg_match('/INTERVAL=(.*?);/', $rruleString, $intervalMatch); - $rrule['interval'] = (int) $intervalMatch[1]; + $rrule['interval'] = $intervalMatch[1] ?? null; - if (\preg_match('/COUNT=(.*?);/', $rruleString, $countMatch)) { - $rrule['count'] = (int) $countMatch[1]; - } + \preg_match('/BYMONTH=(.*?);/', $rruleString, $monthMatch); + $rrule['bymonth'] = $monthMatch[1] ?? null; - if (\preg_match('/UNTIL=(.*?);/', $rruleString, $untilMatch)) { - $rrule['until'] = \DateTime::createFromFormat('Ymd\THis', $untilMatch[1]); - } + \preg_match('/BYMONTHDAY=(.*?);/', $rruleString, $monthdayMatch); + $rrule['bymonthday'] = $monthdayMatch[1] ?? null; + + $rrule['count'] = \preg_match('/COUNT=(.*?);/', $rruleString, $countMatch) + ? (int) ($countMatch[1] ?? 0) + : null; + + $rrule['until'] = \preg_match('/UNTIL=(.*?);/', $rruleString, $untilMatch) + ? $untilMatch[1] ?? null + : null; return $rrule; } diff --git a/Utils/Parser/Document/DocumentParser.php b/Utils/Parser/Document/DocumentParser.php index 5e1870521..1a63ebe9f 100755 --- a/Utils/Parser/Document/DocumentParser.php +++ b/Utils/Parser/Document/DocumentParser.php @@ -25,8 +25,18 @@ use PhpOffice\PhpWord\Writer\HTML; * @link https://jingga.app * @since 1.0.0 */ -class DocumentParser +final class DocumentParser { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Document to string * @@ -38,18 +48,33 @@ class DocumentParser */ public static function parseDocument(string $path, string $output = 'html') : string { - if ($output === 'html') { - $doc = IOFactory::load($path); + $doc = IOFactory::load($path); + if ($output === 'html') { $writer = new HTML($doc); return $writer->getContent(); } elseif ($output === 'pdf') { - $doc = IOFactory::load($path); - $writer = new DocumentWriter($doc); return $writer->toPdfString(); + } elseif ($output === 'txt') { + $writer = new HTML($doc); + $html = $writer->getContent(); + + $doc = new \DOMDocument(); + $html = \preg_replace( + ['~~', '~~'], + ['', ''], + $html + ); + + $doc->loadHTMLFile($path); + + $body = $doc->getElementsByTagName('body'); + $node = $body->item(0); + + return empty($node->textContent) ? '' : $node->textContent; } return ''; diff --git a/Utils/Parser/Document/DocumentWriter.php b/Utils/Parser/Document/DocumentWriter.php index 61a29a1aa..b3af2ad5d 100755 --- a/Utils/Parser/Document/DocumentWriter.php +++ b/Utils/Parser/Document/DocumentWriter.php @@ -14,8 +14,6 @@ declare(strict_types=1); namespace phpOMS\Utils\Parser\Document; -use PhpOffice\PhpWord\PhpWord; -use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Writer\PDF\AbstractRenderer; use PhpOffice\PhpWord\Writer\WriterInterface; diff --git a/Utils/Parser/Html/HtmlParser.php b/Utils/Parser/Html/HtmlParser.php new file mode 100644 index 000000000..7de40702a --- /dev/null +++ b/Utils/Parser/Html/HtmlParser.php @@ -0,0 +1,84 @@ +~', '~~'], + ['', ''], + $html + ); + + $doc->loadHTMLFile($path); + + if (empty($xpath)) { + $body = $doc->getElementsByTagName('body'); + $node = $body->item(0); + + return empty($node->textContent) ? '' : $node->textContent; + } + + $content = ''; + $xNode = new \DOMXpath($doc); + $elements = $xNode->query($xpath); + + if ($elements === false) { + return $content; + } + + foreach ($elements as $element) { + $nodes = $element->childNodes; + + foreach ($nodes as $node) { + $content .= $node->textContent . "\n"; + } + } + + return $content; + } +} diff --git a/Utils/Parser/Markdown/License.txt b/Utils/Parser/Markdown/License.txt index baca86f5b..2122767d2 100755 --- a/Utils/Parser/Markdown/License.txt +++ b/Utils/Parser/Markdown/License.txt @@ -1,6 +1,8 @@ The MIT License (MIT) -Copyright (c) 2013 Emanuil Rusev, erusev.com +Copyright (c) 2013 Original & extra license Emanuil Rusev, erusev.com (MIT) +Copyright (c) Extended license Benjamin Hoegh (MIT) +Copyright (c) Extreme license doowzs (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -17,4 +19,9 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +LIMITATIONS: + +THIS NEW EXPANDED VERSION IS COPYRIGHTED BY Jingga e. K. AND Dennis Eichhorn WITH +THE LICENSE OMS License 2.0 \ No newline at end of file diff --git a/Utils/Parser/Markdown/Markdown.php b/Utils/Parser/Markdown/Markdown.php index 7c1d8bdda..245db4a54 100755 --- a/Utils/Parser/Markdown/Markdown.php +++ b/Utils/Parser/Markdown/Markdown.php @@ -5,7 +5,10 @@ * PHP Version 8.1 * * @package phpOMS\Utils\Parser\Markdown - * @license Original license Emanuil Rusev, erusev.com (MIT) + * @license Original & extra license Emanuil Rusev, erusev.com (MIT) + * @license Extended license Benjamin Hoegh (MIT) + * @license Extreme license doowzs (MIT) + * @license This version: OMS License 2.0 * @version 1.0.0 * @link https://jingga.app */ @@ -21,189 +24,617 @@ use phpOMS\Uri\UriFactory; * @package phpOMS\Utils\Parser\Markdown * @license Original & extra license Emanuil Rusev, erusev.com (MIT) * @license Extended license Benjamin Hoegh (MIT) + * @license Extreme license doowzs (MIT) + * @license This version: OMS License 2.0 * @link https://jingga.app * @see https://github.com/erusev/parsedown * @see https://github.com/erusev/parsedown-extra * @see https://github.com/BenjaminHoegh/ParsedownExtended + * @see https://github.com/doowzs/parsedown-extreme * @since 1.0.0 + * + * @todo Add special markdown content + * 1. Calendar (own widget) + * 2. Event (own widget) + * 3. Tasks (own widget) + * 4. Vote/Survey (own widget) + * 5. Website link/embed widgets (facebook, linkedIn, twitter, ...) + * 6. User/Supplier/Client/Employee (own widget, should make use of schema) + * 7. Address (own widget, should make use of schema) + * 8. Contact (own widget, should make use of schema) + * 9. Item (own widget, should make use of schema) + * 10. Progress radial + * 11. Timeline horizontal/vertical/matrix + * 12. Tabs horizontal/vertical + * 13. Checklist (own widget) + * 14. Gallery + * 15. Form (own widget) + * https://github.com/Karaka-Management/phpOMS/issues/290 */ class Markdown { - # ~ + /** + * Parsedown version + * + * @var string + * @since 1.0.0 + */ + public const VERSION = '2.0.0'; - public const version = '1.8.0-beta-7'; + /** + * Special markdown characters + * + * @var string[] + * @since 1.0.0 + */ + protected array $specialCharacters = [ + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '?', '"', "'", '<', + ]; + /** + * Regexes for html strong + * + * @var array + * @since 1.0.0 + */ + protected array $strongRegex = [ + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + ]; + + /** + * Regexes for html underline + * + * @var array + * @since 1.0.0 + */ + protected array $underlineRegex = [ + '_' => '/^[_]{2}((?:\\\\\_|[^_]|[_][^_]_+[_])+?)[_]{2}(?![_])/s', + ]; + + /** + * Regexes for html emphasizes + * + * @var array + * @since 1.0.0 + */ + protected array $emRegex = [ + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ]; + + /** + * Regex for html attributes + * + * @var string + * @since 1.0.0 + */ + protected string $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + /** + * Regex for for classes and ids + * + * @var string + * @since 1.0.0 + */ + protected string $regexAttribute = '(?:[#.][-\w]+[ ]*)'; + + /** + * Elements without closing + * + * @var string[] + * @since 1.0.0 + */ + protected array $voidElements = [ + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ]; + + /** + * Text elements + * + * @var string[] + * @since 1.0.0 + */ + protected array $textLevelElements = [ + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ]; + + /** + * Inline special characters (not block elements) + * + * @var array + * @since 1.0.0 + */ + protected array $inlineTypes = [ + '!' => ['Image'], + '*' => ['Emphasis'], + '_' => ['Emphasis'], + '&' => ['SpecialCharacter'], + '[' => ['FootnoteMarker', 'Link'], + ':' => ['Url'], + '<' => ['UrlTag', 'EmailTag', 'Markup'], + '`' => ['Code'], + '~' => ['Strikethrough'], + '\\' => ['EscapeSequence'], + ]; + + /** + * Inline special characters (not block elements) (see $inlineTypes) + * + * @var string + * @since 1.0.0 + */ + protected string $inlineMarkerList = '!*_&[:<`~'; + + /** + * Uses strict mode? + * + * Less forgiving with regards to formatting. + * + * @var bool + * @since 1.0.0 + */ + public bool $strictMode = false; + + /** + * Uses safe mode (true -> no html allowed)? + * + * Important for parsing or sanitizing html. + * + * @var bool + * @since 1.0.0 + */ + public bool $safeMode = false; + + /** + * Urls are always handled as links + * + * @var bool + * @since 1.0.0 + */ + public bool $urlsLinked = true; + + /** + * Should html get escaped? + * + * @var bool + * @since 1.0.0 + */ + public bool $markupEscaped = false; + + /** + * Replaces new lines with
in inline elements + * + * true -> replaces new line with br + * false -> replaces double whitespace followed with new line with br + * + * @var bool + * @since 1.0.0 + */ + public bool $breaksEnabled = false; + + /** + * Save link prefixes + * + * @var string[] + * @since 1.0.0 + */ + protected array $safeLinksWhitelist = [ + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ]; + + /** + * Block special characters + * + * @var array + * @since 1.0.0 + */ + protected array $blockTypes = [ + '#' => ['Header'], + '*' => ['Rule', 'List', 'Abbreviation'], + '+' => ['List'], + '-' => ['SetextHeader', 'Table', 'Rule', 'List'], + '0' => ['List'], + '1' => ['List'], + '2' => ['List'], + '3' => ['List'], + '4' => ['List'], + '5' => ['List'], + '6' => ['List'], + '7' => ['List'], + '8' => ['List'], + '9' => ['List'], + ':' => ['Table', 'DefinitionList'], + '<' => ['Comment', 'Markup'], + '=' => ['SetextHeader'], + '>' => ['Quote'], + '[' => ['Footnote', 'Reference'], + '_' => ['Rule'], + '`' => ['FencedCode'], + '|' => ['Table'], + '~' => ['FencedCode'], + ]; + + /** + * Unmarked block types + * + * @var string[] + * @since 1.0.0 + */ + protected array $unmarkedBlockTypes = [ + 'Code', + ]; + + /** + * Is continuable + * + * @var string[] + * @since 1.0.0 + */ + private const CONTINUABLE = [ + 'Code', 'Comment', 'FencedCode', 'List', 'Quote', 'Table', 'Math', 'Spoiler', 'Checkbox', 'Footnote', 'DefinitionList', 'Markup', + ]; + + /** + * Is completable + * + * @var string[] + * @since 1.0.0 + */ + private const COMPLETABLE = [ + 'Math', 'Spoiler', 'Table', 'Checkbox', 'Footnote', 'Markup', 'Code', 'FencedCode', 'List', + ]; + + /** + * Parsing options + * + * @var array + * @since 1.0.0 + */ private array $options = []; - # ~ + /** + * Definition data + * + * E.g. abbreviations, footnotes + * + * @var array + * @since 1.0.0 + */ + protected array $definitionData = []; - private string $idToc = ''; + // TOC: start + /** + * Table of content id + * + * @var string + * @since 1.0.0 + */ + public string $idToc = 'toc'; + /** + * TOC array after parsing headers + * + * @var array{text:string, id:string, level:string} + * @since 1.0.0 + */ + protected $contentsListArray = []; + + /** + * TOC string after parsing headers + * + * @var string + * @since 1.0.0 + */ + protected $contentsListString = ''; + + /** + * First head level + * + * @var int + * @since 1.0.0 + */ + protected int $firstHeadLevel = 0; + + /** + * Is header blacklist (for table of contents/TOC) initialized + * + * @var bool + * @since 1.0.0 + */ + protected $isBlacklistInitialized = false; + + /** + * Header duplicates (same header text) + * + * @var array + * @since 1.0.0 + */ + protected $anchorDuplicates = []; + // TOC: end + + /** + * Footnote count + * + * @var int + * @since 1.0.0 + */ + private int $footnoteCount = 0; + + /** + * Current abbreviation + * + * @var string + * @since 1.0.0 + */ + private string $currentAbreviation; + + /** + * Current abbreviation meaning + * + * @var string + * @since 1.0.0 + */ + private string $currentMeaning; + + /** + * Instances + * + * @var array + * @since 1.0.0 + */ + private static $instances = []; + + /** + * Clean up state + * + * @return void + * + * @since 1.0.0 + */ + public function clean() : void + { + $this->definitionData = []; + $this->contentsListArray = []; + $this->contentsListString = ''; + $this->firstHeadLevel = 0; + $this->anchorDuplicates = []; + $this->footnoteCount = 0; + $this->currentAbreviation = ''; + $this->currentMeaning = ''; + } + + /** + * Create instance for static use + * + * @param string $name Instance name + * + * @return self + * + * @since 1.0.0 + */ + public static function getInstance(string $name = 'default') : self + { + if (isset(self::$instances[$name])) { + $obj = self::$instances[$name]; + $obj->clean(); + + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + /** + * Constructor. + * + * @param array $params Parameters + * + * @since 1.0.0 + */ public function __construct(array $params = []) { - $this->options = $params; - + $this->options = $params; $this->options['toc'] = $this->options['toc'] ?? false; // Marks - $state = $this->options['mark'] ?? true; - if ($state !== false) { - $this->InlineTypes['='][] = 'mark'; + if ($this->options['mark'] ?? true) { + $this->inlineTypes['='][] = 'Mark'; $this->inlineMarkerList .= '='; } // Keystrokes - $state = $this->options['keystrokes'] ?? true; - if ($state !== false) { - $this->InlineTypes['['][] = 'Keystrokes'; - $this->inlineMarkerList .= '['; + if ($this->options['keystrokes'] ?? true) { + $this->inlineTypes['['][] = 'Keystrokes'; + } + + // Spoiler + if ($this->options['spoiler'] ?? false) { + $this->inlineTypes['>'][] = 'Spoiler'; + $this->inlineMarkerList .= '>'; } // Inline Math - $state = $this->options['math'] ?? false; - if ($state !== false) { - $this->InlineTypes['\\'][] = 'Math'; - $this->inlineMarkerList .= '\\'; - $this->InlineTypes['$'][] = 'Math'; + if ($this->options['math'] ?? false) { + $this->inlineTypes['\\'][] = 'Math'; + $this->inlineTypes['$'][] = 'Math'; $this->inlineMarkerList .= '$'; } // Superscript - $state = $this->options['sup'] ?? false; - if ($state !== false) { - $this->InlineTypes['^'][] = 'Superscript'; + if ($this->options['sup'] ?? false) { + $this->inlineTypes['^'][] = 'Superscript'; $this->inlineMarkerList .= '^'; } // Subscript - $state = $this->options['sub'] ?? false; - if ($state !== false) { - $this->InlineTypes['~'][] = 'Subscript'; + if ($this->options['sub'] ?? false) { + $this->inlineTypes['~'][] = 'Subscript'; } // Emojis - $state = $this->options['emojis'] ?? true; - if ($state !== false) { - $this->InlineTypes[':'][] = 'Emojis'; - $this->inlineMarkerList .= ':'; + if ($this->options['emojis'] ?? true) { + $this->inlineTypes[':'][] = 'Emojis'; } // Typographer - $state = $this->options['typographer'] ?? false; - if ($state !== false) { - $this->InlineTypes['('][] = 'Typographer'; + if ($this->options['typographer'] ?? false) { + $this->inlineTypes['('][] = 'Typographer'; $this->inlineMarkerList .= '('; - $this->InlineTypes['.'][] = 'Typographer'; + $this->inlineTypes['.'][] = 'Typographer'; $this->inlineMarkerList .= '.'; - $this->InlineTypes['+'][] = 'Typographer'; + $this->inlineTypes['+'][] = 'Typographer'; $this->inlineMarkerList .= '+'; - $this->InlineTypes['!'][] = 'Typographer'; - $this->inlineMarkerList .= '!'; - $this->InlineTypes['?'][] = 'Typographer'; + $this->inlineTypes['!'][] = 'Typographer'; + $this->inlineTypes['?'][] = 'Typographer'; $this->inlineMarkerList .= '?'; } - // Smartypants - $state = $this->options['smarty'] ?? false; - if ($state !== false) { - $this->InlineTypes['<'][] = 'Smartypants'; - $this->inlineMarkerList .= '<'; - $this->InlineTypes['>'][] = 'Smartypants'; - $this->inlineMarkerList .= '>'; - $this->InlineTypes['-'][] = 'Smartypants'; - $this->inlineMarkerList .= '-'; - $this->InlineTypes['.'][] = 'Smartypants'; - $this->inlineMarkerList .= '.'; - $this->InlineTypes["'"][] = 'Smartypants'; - $this->inlineMarkerList .= "'"; - $this->InlineTypes['"'][] = 'Smartypants'; - $this->inlineMarkerList .= '"'; - $this->InlineTypes['`'][] = 'Smartypants'; - $this->inlineMarkerList .= '`'; - } - - /* - * Blocks - * ------------------------------------------------------------------------ - */ - // Block Math - $state = $this->options['math'] ?? false; - if ($state !== false) { - $this->BlockTypes['\\'][] = 'Math'; - $this->BlockTypes['$'][] = 'Math'; + if ($this->options['math'] ?? false) { + $this->blockTypes['\\'][] = 'Math'; + $this->blockTypes['$'][] = 'Math'; } - // Task - $state = $this->options['lists']['tasks'] ?? true; - if ($state !== false) { - $this->BlockTypes['['][] = 'Checkbox'; - } - } - - public function textParent($text) - { - $Elements = $this->textElements($text); - - # convert to markup - $markup = $this->elements($Elements); - - # trim line breaks - $markup = \trim($markup, "\n"); - - # merge consecutive dl elements - - $markup = \preg_replace('/<\/dl>\s+
\s+/', '', $markup); - - # add footnotes - - if (isset($this->DefinitionData['Footnote'])) - { - $Element = $this->buildFootnoteElement(); - - $markup .= "\n" . $this->element($Element); + // Block Spoiler + if ($this->options['spoiler'] ?? false) { + $this->blockTypes['?'][] = 'Spoiler'; } - return $markup; + // Checkbox + if ($this->options['lists']['checkbox'] ?? true) { + $this->blockTypes['['][] = 'Checkbox'; + } + + // Embedding + if ($this->options['embedding'] ?? false) { + $this->inlineTypes['['][] = 'Embedding'; + } + + // Map + if ($this->options['map'] ?? false) { + $this->inlineTypes['['][] = 'Map'; + } + + // Address + if ($this->options['address'] ?? false) { + $this->inlineTypes['['][] = 'Address'; + } + + // Contact + if ($this->options['contact'] ?? false) { + $this->inlineTypes['['][] = 'Contact'; + } + + // Progress + if ($this->options['progress'] ?? false) { + $this->inlineTypes['['][] = 'Progress'; + } + + // Escaping needs to happen at the end + $this->inlineMarkerList .= '\\'; } /** - * Parses the given markdown string to an HTML string but it leaves the ToC - * tag as is. It's an alias of the parent method "\DynamicParent::text()". + * Parses the given markdown string to a HTML + * + * @param string $text Markdown text to parse + * + * @return string + * + * @since 1.0.0 */ - public function body($text) : string + public static function parse(string $text) : string { - $text = $this->encodeTagToHash($text); // Escapes ToC tag temporary - $html = $this->textParent($text); // Parses the markdown text + $parsedown = self::getInstance(); - return $this->decodeTagFromHash($html); // Unescape the ToC tag + return $parsedown->text($text); + } + + /** + * Parses the given markdown string to a HTML string but it ignores ToC + * + * @param string $text Markdown text to parse + * + * @return string + * + * @since 1.0.0 + */ + public function body(string $text) : string + { + $text = $this->encodeToCTagToHash($text); // Escapes ToC tag temporary + + $elements = $this->textElements($text); + $html = $this->elements($elements); + $html = \trim($html, "\n"); + + // Merge consecutive dl elements + $html = \preg_replace('/<\/dl>\s+
\s+/', '', $html); + + // Add footnotes + if (isset($this->definitionData['Footnote'])) { + $element = $this->buildFootnoteElement(); + $html .= "\n" . $this->element($element); + } + + return $this->decodeToCTagFromHash($html); // Unescape the ToC tag } /** * Parses markdown string to HTML and also the "[toc]" tag as well. - * It overrides the parent method: \Parsedown::text(). + * + * @param string $text Markdown text to parse + * + * @return string + * + * @since 1.0.0 */ - public function text($text) + public function text(string $text) : string { // Parses the markdown text except the ToC tag. This also searches // the list of contents and available to get from "contentsList()" // method. $html = $this->body($text); - if (isset($this->options['toc']) && $this->options['toc'] == false) { + if (isset($this->options['toc']) && $this->options['toc'] === false) { return $html; } - $tagOrigin = $this->getTagToC(); + // Handle toc + $tagOrigin = $this->options['toc']['set_toc_tag'] ?? '[toc]'; if (\strpos($text, $tagOrigin) === false) { return $html; } $tocData = $this->contentsList(); - $tocId = $this->getIdAttributeToC(); - $needle = '

'.$tagOrigin.'

'; - $replace = "
{$tocData}
"; + $needle = '

' . $tagOrigin . '

'; + $replace = '
' . $tocData . '
'; return \str_replace($needle, $replace, $html); } @@ -214,132 +645,138 @@ class Markdown * @param string $typeReturn Type of the return format. "html" or "json". * * @return string HTML/JSON string of ToC + * + * @since 1.0.0 */ - public function contentsList($typeReturn = 'html') + public function contentsList($typeReturn = 'html') : string { - if (\strtolower($typeReturn) === 'html') { - $result = ''; - if (!empty($this->contentsListString)) { - // Parses the ToC list in markdown to HTML - $result = $this->body($this->contentsListString); - } - - return $result; - } - if (\strtolower($typeReturn) === 'json') { return \json_encode($this->contentsListArray); } - // Forces to return ToC as "html" - \error_log( - 'Unknown return type given while parsing ToC.' - .' At: '.__FUNCTION__.'() ' - .' in Line:'.__LINE__.' (Using default type)' - ); + $result = ''; + if (!empty($this->contentsListString)) { + // Parses the ToC list in markdown to HTML + $result = $this->body($this->contentsListString); + } - return $this->contentsList('html'); + return $result; } /** - * ------------------------------------------------------------------------ - * Inline - * ------------------------------------------------------------------------. + * Handle inline code + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 */ - - // inlineCode - protected function inlineCode($Excerpt) + protected function inlineCode(array $excerpt) : ?array { - $codeSnippets = $this->options['code']['inline'] ?? true; - $codeMain = $this->options['code'] ?? true; - - if ($codeSnippets !== true || $codeMain !== true) { - return; + if (!($this->options['code']['inline'] ?? true) + || !($this->options['code'] ?? true) + ) { + return null; } - $marker = $Excerpt['text'][0]; + $marker = $excerpt['text'][0]; - if (\preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? \strlen($matches[0]), - 'element' => [ - 'name' => 'code', - 'text' => $text, - ], - ]; + if (\preg_match( + '/^([' . $marker . ']++)[ ]*+(.+?)[ ]*+(? \strlen($matches[0]), + 'element' => [ + 'name' => 'code', + 'text' => $text, + ], + ]; } - protected function inlineEmailTag($Excerpt) + /** + * Handle inline email + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineEmailTag(array $excerpt) : ?array { - $mainState = $this->options['links'] ?? true; - $state = $this->options['links']['email_links'] ?? true; - - if (!$mainState || !$state) { - return; + if (!($this->options['links'] ?? true) + || !($this->options['links']['email_links'] ?? true) + ) { + return null; } - $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; - $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' - . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + if (\strpos($excerpt['text'], '>') === false + || \preg_match('/^<((mailto:)?' . $commonMarkEmail . ')>/i', $excerpt['text'], $matches) !== 1 + ) { + return null; + } - if (\strpos($Excerpt['text'], '>') !== false - && \preg_match("/^<((mailto:)?{$commonMarkEmail})>/i", $Excerpt['text'], $matches) - ){ - $url = UriFactory::build($matches[1]); + $url = UriFactory::build($matches[1]); - if (!isset($matches[2])) - { - $url = "mailto:{$url}"; - } + if (!isset($matches[2])) { + $url = "mailto:{$url}"; + } - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'name' => 'a', - 'text' => $matches[1], - 'attributes' => [ - 'href' => $url, - ], + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => [ + 'href' => $url, ], - ]; - } + ], + ]; } - protected function inlineEmphasis($Excerpt) + /** + * Inline emphasis + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineEmphasis(array $excerpt) : ?array { - $state = $this->options['emphasis'] ?? true; - if (!$state) { - return; + if (!($this->options['emphasis'] ?? true) + || !isset($excerpt['text'][1]) + ) { + return null; } - if (!isset($Excerpt['text'][1])) - { - return; - } + $marker = $excerpt['text'][0]; - $marker = $Excerpt['text'][0]; - - if ($Excerpt['text'][1] === $marker && isset($this->StrongRegex[$marker]) && \preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) - { + if ($excerpt['text'][1] === $marker + && isset($this->strongRegex[$marker]) && \preg_match($this->strongRegex[$marker], $excerpt['text'], $matches) + ) { $emphasis = 'strong'; - } - elseif ($Excerpt['text'][1] === $marker && isset($this->UnderlineRegex[$marker]) && \preg_match($this->UnderlineRegex[$marker], $Excerpt['text'], $matches)) - { + } elseif ($excerpt['text'][1] === $marker + && isset($this->underlineRegex[$marker]) && \preg_match($this->underlineRegex[$marker], $excerpt['text'], $matches) + ) { $emphasis = 'u'; - } - elseif (\preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) - { + } elseif (\preg_match($this->emRegex[$marker], $excerpt['text'], $matches)) { $emphasis = 'em'; - } - else - { - return; + } else { + return null; } return [ @@ -355,189 +792,217 @@ class Markdown ]; } - protected function inlineImage($Excerpt) + /** + * Handle image + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineImage(array $excerpt) : ?array { - $state = $this->options['images'] ?? true; - if (!$state) { - return; + if (!($this->options['images'] ?? true) + || !\str_starts_with($excerpt['text'], '![') + ) { + return null; } - if (!isset($Excerpt['text'][1]) || $Excerpt['text'][1] !== '[') - { - return; + $excerpt['text'] = \substr($excerpt['text'], 1); + $link = $this->inlineLink($excerpt); + + if ($link === null) { + return null; } - $Excerpt['text']= \substr($Excerpt['text'], 1); - - $Link = $this->inlineLink($Excerpt); - - if ($Link === null) - { - return; - } - - $Inline = [ - 'extent' => $Link['extent'] + 1, + $inline = [ + 'extent' => $link['extent'] + 1, 'element' => [ 'name' => 'img', 'attributes' => [ - 'src' => $Link['element']['attributes']['href'], - 'alt' => $Link['element']['handler']['argument'], + 'src' => $link['element']['attributes']['href'], + 'alt' => $link['element']['handler']['argument'], ], 'autobreak' => true, ], ]; - $Inline['element']['attributes'] += $Link['element']['attributes']; + $inline['element']['attributes'] += $link['element']['attributes']; - unset($Inline['element']['attributes']['href']); + unset($inline['element']['attributes']['href']); - return $Inline; + return $inline; } - protected function inlineLink($Excerpt) + /** + * Handle link + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array + * + * @since 1.0.0 + */ + protected function inlineLink(array $excerpt) : ?array { - $state = $this->options['links'] ?? true; - if (!$state) { - return; + if (!($this->options['links'] ?? true)) { + return null; } - $Link = $this->inlineLinkParent($Excerpt); + $link = $this->inlineLinkParent($excerpt); + $remainder = $link !== null ? \substr($excerpt['text'], $link['extent']) : ''; - $remainder = $Link !== null ? \substr($Excerpt['text'], $Link['extent']) : ''; - - if (\preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) - { - $Link['element']['attributes'] += $this->parseAttributeData($matches[1]); - - $Link['extent'] += \strlen($matches[0]); + if (\preg_match('/^[ ]*{(' . $this->regexAttribute . '+)}/', $remainder, $matches)) { + $link['extent'] += \strlen($matches[0]); + $link['element']['attributes'] += $this->parseAttributeData($matches[1]); } - return $Link; + return $link; } - protected function inlineMarkup($Excerpt) + /** + * Handle markup + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineMarkup(array $excerpt) : ?array { - $state = $this->options['markup'] ?? true; - if (!$state) { - return; - } - - if ($this->markupEscaped || $this->safeMode || \strpos($Excerpt['text'], '>') === false) - { - return; - } - - if ($Excerpt['text'][1] === '/' && \preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) - { - return [ - 'element' => ['rawHtml' => $matches[0]], - 'extent' => \strlen($matches[0]), - ]; - } - - if ($Excerpt['text'][1] === '!' && \preg_match('/^/s', $Excerpt['text'], $matches)) - { - return [ - 'element' => ['rawHtml' => $matches[0]], - 'extent' => \strlen($matches[0]), - ]; - } - - if ($Excerpt['text'][1] !== ' ' && \preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) - { - return [ - 'element' => ['rawHtml' => $matches[0]], - 'extent' => \strlen($matches[0]), - ]; - } - } - - protected function inlineStrikethrough($Excerpt) - { - $state = $this->options['strikethroughs'] ?? true; - if (!$state) { - return; - } - - if (!isset($Excerpt['text'][1])) - { - return; - } - - if ($Excerpt['text'][1] === '~' && \preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) - { - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'name' => 'del', - 'handler' => [ - 'function' => 'lineElements', - 'argument' => $matches[1], - 'destination' => 'elements', - ], - ], - ]; - } - } - - protected function inlineUrl($Excerpt) - { - $state = $this->options['links'] ?? true; - if (!$state) { - return; - } - - if ($this->urlsLinked !== true || !isset($Excerpt['text'][2]) || $Excerpt['text'][2] !== '/') - { - return; - } - - if (\strpos($Excerpt['context'], 'http') !== false - && \preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, \PREG_OFFSET_CAPTURE) + if (!($this->options['markup'] ?? true) + || $this->markupEscaped || $this->safeMode || \strpos($excerpt['text'], '>') === false ) { - $url = UriFactory::build($matches[0][0]); - - return [ - 'extent' => \strlen($matches[0][0]), - 'position' => $matches[0][1], - 'element' => [ - 'name' => 'a', - 'text' => $url, - 'attributes' => [ - 'href' => $url, - ], - ], - ]; - } - } - - protected function inlineUrlTag($Excerpt) - { - $state = $this->options['links'] ?? true; - if (!$state) { - return; + return null; } - if (\strpos($Excerpt['text'], '>') !== false && \preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) - { - $url = UriFactory::build($matches[1]); - + if (($excerpt['text'][1] === '/' && \preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $excerpt['text'], $matches)) + || ($excerpt['text'][1] === '!' && \preg_match('/^/s', $excerpt['text'], $matches)) + || ($excerpt['text'][1] !== ' ' && \preg_match('/^<\w[\w-]*+(?:[ ]*+' . $this->regexHtmlAttribute . ')*+[ ]*+\/?>/s', $excerpt['text'], $matches)) + ) { return [ 'extent' => \strlen($matches[0]), - 'element' => [ - 'name' => 'a', - 'text' => $url, - 'attributes' => [ - 'href' => $url, - ], - ], + 'element' => ['rawHtml' => $matches[0]], ]; } + + return null; } - protected function inlineEmojis($excerpt) + /** + * Handle striketrhough + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineStrikethrough(array $excerpt) : ?array { + if (!($this->options['strikethroughs'] ?? true) + || !\str_starts_with($excerpt['text'], '~~') + || \preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $excerpt['text'], $matches) !== 1 + ) { + return null; + } + + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'del', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ], + ], + ]; + } + + /** + * Handle url + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, position:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineUrl(array $excerpt) : ?array + { + if (!($this->options['links'] ?? true) + || !$this->urlsLinked || !\str_starts_with($excerpt['text'], '://') + || \strpos($excerpt['context'], 'http') === false + || \preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $excerpt['context'], $matches, \PREG_OFFSET_CAPTURE) !== 1 + ) { + return null; + } + + $url = UriFactory::build($matches[0][0]); + + return [ + 'extent' => \strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => [ + 'name' => 'a', + 'text' => $url, + 'attributes' => [ + 'href' => $url, + ], + ], + ]; + } + + /** + * Handle url + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineUrlTag(array $excerpt) : ?array + { + if (!($this->options['links'] ?? true) + || \strpos($excerpt['text'], '>') === false + || \preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $excerpt['text'], $matches) !== 1 + ) { + return null; + } + + $url = UriFactory::build($matches[1]); + + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'a', + 'text' => $url, + 'attributes' => [ + 'href' => $url, + ], + ], + ]; + } + + /** + * Handle emojis + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineEmojis(array $excerpt) : ?array + { + if (\preg_match('/^(:)([^: ]*?)(:)/', $excerpt['text'], $matches) !== 1) { + return null; + } + $emojiMap = [ ':smile:' => '😄', ':laughing:' => '😆', ':blush:' => '😊', ':smiley:' => '😃', ':relaxed:' => '☺️', ':smirk:' => '😏', ':heart_eyes:' => '😍', ':kissing_heart:' => '😘', @@ -756,82 +1221,498 @@ class Markdown ':white_large_square:' => '⬜', ]; - if (\preg_match('/^(:)([^: ]*?)(:)/', $excerpt['text'], $matches)) { + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'text' => \str_replace(\array_keys($emojiMap), $emojiMap, $matches[0]), + ], + ]; + } + + /** + * Handle marks + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineMark(array $excerpt) : ?array + { + if (!\str_starts_with($excerpt['text'], '==') + || \preg_match('/^(==)([^=]*?)(==)/', $excerpt['text'], $matches) !== 1 + ) { + return null; + } + + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'mark', + 'text' => $matches[2], + ], + ]; + } + + /** + * Handle marks + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineSpoiler(array $excerpt) : ?array + { + if (!\str_starts_with($excerpt['text'], '>!') + || \preg_match('/^>!(.*?)! \strlen($matches[0]), + 'element' => [ + 'name' => 'span', + 'attributes' => [ + 'class' => 'spoiler', + ], + 'elements' => [ + [ + 'name' => 'input', + 'attributes' => [ + 'type' => 'checkbox', + ], + ], + [ + 'name' => 'span', + 'text' => $matches[1], + ], + ], + ], + ]; + } + + /** + * Handle keystrokes + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineKeystrokes(array $excerpt) : ?array + { + if (!\str_starts_with($excerpt['text'], '[[') + || \preg_match('/^(? \strlen($matches[0]), + 'element' => [ + 'name' => 'kbd', + 'text' => $matches[1], + ], + ]; + } + + /** + * Handle embedding + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineEmbedding(array $excerpt) : ?array + { + if (!($this->options['embedding'] ?? false) + || !(\str_starts_with($excerpt['text'], '[video') || \str_starts_with($excerpt['text'], '[audio')) + || (!($video = (\preg_match('/\[video.*src="([^"]*)".*\]/', $excerpt['text'], $matches) === 1)) + && !($audio = (\preg_match('/\[audio.*src="([^"]*)".*\]/', $excerpt['text'], $matches) === 1))) + ) { + return null; + } + + $url = $matches[1]; + if ($video) { + $type = ''; + + $needles = ['youtube', 'vimeo', 'dailymotion']; + foreach ($needles as $needle) { + if (\strpos($url, $needle) !== false) { + $type = $needle; + } + } + + switch ($type) { + case 'youtube': + $element = 'iframe'; + $attributes = [ + 'src' => \preg_replace('/.*\?v=([^\&\]]*).*/', 'https://www.youtube.com/embed/$1', $url), + 'frameborder' => '0', + 'allow' => 'autoplay', + 'allowfullscreen' => '', + 'sandbox' => 'allow-same-origin allow-scripts allow-forms', + ]; + break; + case 'vimeo': + $element = 'iframe'; + $attributes = [ + 'src' => \preg_replace('/(?:https?:\/\/(?:[\w]{3}\.|player\.)*vimeo\.com(?:[\/\w:]*(?:\/videos)?)?\/([0-9]+)[^\s]*)/', 'https://player.vimeo.com/video/$1', $url), + 'frameborder' => '0', + 'allow' => 'autoplay', + 'allowfullscreen' => '', + 'sandbox' => 'allow-same-origin allow-scripts allow-forms', + ]; + break; + case 'dailymotion': + $element = 'iframe'; + $attributes = [ + 'src' => $url, + 'frameborder' => '0', + 'allow' => 'autoplay', + 'allowfullscreen' => '', + 'sandbox' => 'allow-same-origin allow-scripts allow-forms', + ]; + break; + default: + $element = 'video'; + $attributes = [ + 'src' => UriFactory::build($url), + 'controls' => '', + ]; + } + return [ 'extent' => \strlen($matches[0]), 'element' => [ - 'text' => \str_replace(\array_keys($emojiMap), $emojiMap, $matches[0]), + 'name' => $element, + 'text' => $matches[1], + 'attributes' => $attributes, ], ]; - } - } - - // Inline Marks - - protected function inlineMark($excerpt) - { - if (\preg_match('/^(==)([^=]*?)(==)/', $excerpt['text'], $matches)) { + } elseif ($audio) { return [ 'extent' => \strlen($matches[0]), 'element' => [ - 'name' => 'mark', - 'text' => $matches[2], + 'name' => 'audio', + 'text' => $matches[1], + 'attributes' => [ + 'src' => UriFactory::build($url), + 'controls' => '', + ], ], ]; } + + return null; } - // Inline Keystrokes - - protected function inlineKeystrokes($excerpt) + /** + * Handle map + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineMap(array $excerpt) : ?array { - if (\preg_match('/^(? \strlen($matches[0]), - 'element' => [ - 'name' => 'kbd', - 'text' => $matches[1], - ], - ]; + if (!($this->options['map'] ?? false) + || !\str_starts_with($excerpt['text'], '[map') + || (\preg_match('/\[map(?:\s+(?:name="([^"]+)"|country="([^"]+)"|city="([^"]+)"|zip="([^"]+)"|address="([^"]+)"|lat="([^"]+)"|lon="([^"]+)")){0,7}\]/', $excerpt['text'], $matches) !== 1) + ) { + return null; } - } - // Inline Superscript + $name = $matches[1]; + $country = $matches[2]; + $city = $matches[3]; + $zip = $matches[4]; + $address = $matches[5]; - protected function inlineSuperscript($excerpt) - { - if (\preg_match('/(?:\^(?!\^)([^\^ ]*)\^(?!\^))/', $excerpt['text'], $matches)) { - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'name' => 'sup', - 'text' => $matches[1], - 'function' => 'lineElements', - ], - ]; + $lat = $matches[6]; + $lon = $matches[7]; + + if ($lat === '' || $lon === '') { + [$lat, $lon] = \phpOMS\Api\Geocoding\Nominatim::geocoding($country, $city, $address, $zip); } - } - // Inline Subscript - - protected function inlineSubscript($excerpt) - { - if (\preg_match('/(?:~(?!~)([^~ ]*)~(?!~))/', $excerpt['text'], $matches)) { - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'name' => 'sub', - 'text' => $matches[1], - 'function' => 'lineElements', + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'div', + 'text' => '', + 'attributes' => [ + 'id' => 'i' . \bin2hex(\random_bytes(4)), + 'class' => 'map', + 'data-lat' => $lat, + 'data-lon' => $lon, ], - ]; - } + ], + ]; } - // Inline typographer - - protected function inlineTypographer($excerpt) + /** + * Handle address + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineAddress(array $excerpt) : ?array { + if (!($this->options['address'] ?? false) + || !\str_starts_with($excerpt['text'], '[addr') + || (\preg_match('/\[addr(?:\s+(?:name="([^"]+)"|country="([^"]+)"|city="([^"]+)"|zip="([^"]+)"|address="([^"]+)")){0,5}\]/', $excerpt['text'], $matches) !== 1) + ) { + return null; + } + + $name = $matches[1]; + $country = $matches[2]; + $city = $matches[3]; + $zip = $matches[4]; + $address = $matches[5]; + + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'div', + //'text' => '', + 'attributes' => [ + 'class' => 'addressWidget', + ], + 'elements' => [ + [ + 'name' => 'span', + 'text' => $name, + 'attributes' => ['class' => 'addressWidget-name'], + ], + [ + 'name' => 'span', + 'text' => $address, + 'attributes' => ['class' => 'addressWidget-address'], + ], + [ + 'name' => 'span', + 'text' => $zip, + 'attributes' => ['class' => 'addressWidget-zip'], + ], + [ + 'name' => 'span', + 'text' => $city, + 'attributes' => ['class' => 'addressWidget-city'], + ], + [ + 'name' => 'span', + 'text' => $country, + 'attributes' => ['class' => 'addressWidget-country'], + ], + ], + ], + ]; + } + + /** + * Handle contact + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineContact(array $excerpt) : ?array + { + if (!($this->options['contact'] ?? false) + || !\str_starts_with($excerpt['text'], '[contact') + || (\preg_match('/\[contact.*?([a-zA-Z]+)="(.*?)"\]/', $excerpt['text'], $matches) !== 1) + ) { + return null; + } + + $src = ''; + switch ($matches[1]) { + case 'email': + $src = 'Resources/icons/company/email.svg'; + break; + case 'phone': + $src = 'Resources/icons/company/phone.svg'; + break; + case 'twitter': + $src = 'Resources/icons/company/twitter.svg'; + break; + case 'instagram': + $src = 'Resources/icons/company/instagram.svg'; + break; + case 'discord': + $src = 'Resources/icons/company/discord.svg'; + break; + case 'slack': + $src = 'Resources/icons/company/slack.svg'; + break; + case 'teams': + $src = 'Resources/icons/company/teams.svg'; + break; + case 'facebook': + $src = 'Resources/icons/company/facebook.svg'; + break; + case 'youtube': + $src = 'Resources/icons/company/youtube.svg'; + break; + case 'paypal': + $src = 'Resources/icons/company/paypal.svg'; + break; + case 'linkedin': + $src = 'Resources/icons/company/linkedin.svg'; + break; + } + + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'a', + //'text' => '', + 'attributes' => [ + 'class' => 'contactWidget', + 'href' => '', + ], + 'elements' => [ + [ + 'name' => 'img', + 'attributes' => [ + 'class' => 'contactWidget-icon', + 'src' => $src, + ], + ], + [ + 'name' => 'span', + 'text' => $matches[2], + 'attributes' => ['class' => 'contactWidget-contact'], + ], + ], + + ], + ]; + } + + /** + * Handle progress + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineProgress(array $excerpt) : ?array + { + if (!($this->options['progress'] ?? false) + || !\str_starts_with($excerpt['text'], '[progress') + || (\preg_match('/\[progress(?:\s+(?:type="([^"]+)"|percent="([^"]+)"|value="([^"]+)")){0,3}\]/', $excerpt['text'], $matches) !== 1) + ) { + return null; + } + + // $type = empty($matches[1]) ? 'meter' : $matches[1]; + $percent = empty($matches[2]) ? $matches[3] : $matches[2]; + $value = empty($matches[3]) ? $matches[2] : $matches[3]; + + if ($percent === '' + || $value === '' + ) { + return null; + } + + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'progress', + 'text' => '', + 'attributes' => [ + 'value' => $value, + 'max' => '100', + ], + ], + ]; + } + + /** + * Handle super script + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineSuperscript(array $excerpt) : ?array + { + if (\preg_match('/(?:\^(?!\^)([^\^ ]*)\^(?!\^))/', $excerpt['text'], $matches) !== 1) { + return null; + } + + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'sup', + 'text' => $matches[1], + 'function' => 'lineElements', + ], + ]; + } + + /** + * Handle sub script + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineSubscript(array $excerpt) : ?array + { + if (\preg_match('/(?:~(?!~)([^~ ]*)~(?!~))/', $excerpt['text'], $matches) !== 1) { + return null; + } + + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'name' => 'sub', + 'text' => $matches[1], + 'function' => 'lineElements', + ], + ]; + } + + /** + * Handle typographer + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineTypographer(array $excerpt) : ?array + { + if (\preg_match('/\+-|\(p\)|\(tm\)|\(r\)|\(c\)|\.{2,}|\!\.{3,}|\?\.{3,}/i', $excerpt['text'], $matches) !== 1) { + return null; + } + $substitutions = [ '/\(c\)/i' => '©', '/\(r\)/i' => '®', @@ -843,144 +1724,26 @@ class Markdown '/\?\.{3,}/i' => '?..', ]; - if (\preg_match('/\+-|\(p\)|\(tm\)|\(r\)|\(c\)|\.{2,}|\!\.{3,}|\?\.{3,}/i', $excerpt['text'], $matches)) { - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'rawHtml' => \preg_replace(\array_keys($substitutions), \array_values($substitutions), $matches[0]), - ], - ]; - } + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ + 'rawHtml' => \preg_replace(\array_keys($substitutions), \array_values($substitutions), $matches[0]), + ], + ]; } - // Inline Smartypants - - protected function inlineSmartypants($excerpt) - { - // Substitutions - $backtickDoublequoteOpen = $this->options['smarty']['substitutions']['left-double-quote'] ?? '“'; - $backtickDoublequoteClose = $this->options['smarty']['substitutions']['right-double-quote'] ?? '”'; - - $smartDoublequoteOpen = $this->options['smarty']['substitutions']['left-double-quote'] ?? '“'; - $smartDoublequoteClose = $this->options['smarty']['substitutions']['right-double-quote'] ?? '”'; - $smartSinglequoteOpen = $this->options['smarty']['substitutions']['left-single-quote'] ?? '‘'; - $smartSinglequoteClose = $this->options['smarty']['substitutions']['right-single-quote'] ?? '’'; - - $leftAngleQuote = $this->options['smarty']['substitutions']['left-angle-quote'] ?? '«'; - $rightAngleQuote = $this->options['smarty']['substitutions']['right-angle-quote'] ?? '»'; - - if (\preg_match('/(``)(?!\s)([^"\'`]{1,})(\'\')|(\")(?!\s)([^\"]{1,})(\")|(\')(?!\s)([^\']{1,})(\')|(<{2})(?!\s)([^<>]{1,})(>{2})|(\.{3})|(-{3})|(-{2})/i', $excerpt['text'], $matches)) { - $matches = \array_values(\array_filter($matches)); - - // Smart backticks - $smartBackticks = $this->options['smarty']['smart_backticks'] ?? false; - - if ($smartBackticks && $matches[1] === '``') { - $length = \strlen(\trim($excerpt['before'])); - if ($length > 0) { - return; - } - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'text' => \html_entity_decode($backtickDoublequoteOpen).$matches[2].\html_entity_decode($backtickDoublequoteClose), - ], - ]; - } - - // Smart quotes - $smartQuotes = $this->options['smarty']['smart_quotes'] ?? true; - - if ($smartQuotes) { - if ($matches[1] === "'") { - $length = \strlen(\trim($excerpt['before'])); - if ($length > 0) { - return; - } - - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'text' => \html_entity_decode($smartSinglequoteOpen).$matches[2].\html_entity_decode($smartSinglequoteClose), - ], - ]; - } - - if ($matches[1] === '"') { - $length = \strlen(\trim($excerpt['before'])); - if ($length > 0) { - return; - } - - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'text' => \html_entity_decode($smartDoublequoteOpen).$matches[2].\html_entity_decode($smartDoublequoteClose), - ], - ]; - } - } - - // Smart angled quotes - $smartAngledQuotes = $this->options['smarty']['smart_angled_quotes'] ?? true; - - if ($smartAngledQuotes && $matches[1] === '<<') { - $length = \strlen(\trim($excerpt['before'])); - if ($length > 0) { - return; - } - - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'text' => \html_entity_decode($leftAngleQuote).$matches[2].\html_entity_decode($rightAngleQuote), - ], - ]; - } - - // Smart dashes - $smartDashes = $this->options['smarty']['smart_dashes'] ?? true; - - if ($smartDashes) { - if ($matches[1] === '---') { - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'rawHtml' => $this->options['smarty']['substitutions']['mdash'] ?? '—', - ], - ]; - } - - if ($matches[1] === '--') { - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'rawHtml' => $this->options['smarty']['substitutions']['ndash'] ?? '–', - ], - ]; - } - } - - // Smart ellipses - $smartEllipses = $this->options['smarty']['smart_ellipses'] ?? true; - - if ($smartEllipses && $matches[1] === '...') { - return [ - 'extent' => \strlen($matches[0]), - 'element' => [ - 'rawHtml' => $this->options['smarty']['substitutions']['ellipses'] ?? '…', - ], - ]; - } - } - } - - // Inline Math - - protected function inlineMath($excerpt) + /** + * Handle math + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineMath(array $excerpt) : ?array { $matchSingleDollar = $this->options['math']['single_dollar'] ?? false; - // Inline Matches if ($matchSingleDollar) { // Match single dollar - experimental if (\preg_match('/^(? \strlen($mathMatch), - 'element' => [ - 'text' => $mathMatch, - ], - ]; + if (!isset($mathMatch)) { + return null; } - } - protected function inlineEscapeSequence($excerpt) - { - $element = [ + return [ + 'extent' => \strlen($mathMatch), 'element' => [ - 'rawHtml' => $excerpt['text'][1], + 'text' => $mathMatch, ], - 'extent' => 2, ]; - - $state = $this->options['math'] ?? false; - - if ($state) { - if (isset($excerpt['text'][1]) && \in_array($excerpt['text'][1], $this->specialCharacters) && !\preg_match('/^(?specialCharacters)) { - return $element; - } } /** - * ------------------------------------------------------------------------ - * Blocks. - * ------------------------------------------------------------------------ + * Handle escape sequence + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 */ - protected function blockFootnote($line, array $_ = null) + protected function inlineEscapeSequence(array $excerpt) : ?array { - $state = $this->options['footnotes'] ?? true; - if ($state) { - return $this->blockFootnoteBase($line); + if (!isset($excerpt['text'][1]) + || !\in_array($excerpt['text'][1], $this->specialCharacters) + ) { + return null; } + + $state = $this->options['math'] ?? false; + if (!$state + || !\preg_match('/^(? 2, + 'element' => [ + 'rawHtml' => $excerpt['text'][1], + ], + ]; + } + + return null; } - protected function blockDefinitionList($line, $block) + /** + * Handle block footnote + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockFootnote(array $line, ?array $_ = null) : ?array { - $state = $this->options['definition_lists'] ?? true; - if ($state) { - return $this->blockDefinitionListBase($line, $block); - } + return ($this->options['footnotes'] ?? true) + && \preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $line['text'], $matches) == 1 + ? [ + 'label' => $matches[1], + 'text' => $matches[2], + 'hidden' => true, + ] + : null; } - protected function blockCode($line, $block = null) + /** + * Handle block definition list + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockDefinitionList(array $line, ?array $block = null) : ?array { - $codeBlock = $this->options['code']['blocks'] ?? true; - $codeMain = $this->options['code'] ?? true; - if ($codeBlock === true && $codeMain === true) { - return $this->blockCodeBase($line, $block); + if (!($this->options['definition_lists'] ?? true) + || $block === null + || $block['type'] !== 'Paragraph' + ) { + return null; } + + $element = [ + 'name' => 'dl', + 'elements' => [], + ]; + + $terms = \explode("\n", $block['element']['handler']['argument']); + + foreach ($terms as $term) { + $element['elements'][] = [ + 'name' => 'dt', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $term, + 'destination' => 'elements', + ], + ]; + } + + $block['element'] = $element; + + return $this->addDdElement($line, $block); } - protected function blockComment($line, array $_ = null) + /** + * Handle block code + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockCode(array $line, ?array $block = null) : ?array { - $state = $this->options['comments'] ?? true; - if ($state) { - return $this->blockCommentBase($line); + if (!($this->options['code']['blocks'] ?? true) + || !($this->options['code'] ?? true) + || ($block !== null && $block['type'] === 'Paragraph' && !isset($block['interrupted'])) + || $line['indent'] < 4 + ) { + return null; } + + return [ + 'element' => [ + 'name' => 'pre', + 'element' => [ + 'name' => 'code', + 'text' => \substr($line['body'], 4), + ], + ], + ]; } - protected function blockHeader($line, array $_ = null) + /** + * Handle block comment + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockComment(array $line, ?array $_ = null) : ?array { - $state = $this->options['headings'] ?? true; - if (!$state) { - return; + if (!($this->options['comments'] ?? true) + || $this->markupEscaped || $this->safeMode + || !\str_starts_with($line['text'], '') !== false) { + $block['closed'] = true; + } + + return $block; + } + + /** + * Handle block comment + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockHeader(array $line, ?array $_ = null) : ?array + { + if (!($this->options['headings'] ?? true)) { + return null; + } + + $level = \strspn($line['text'], '#'); + if ($level > 6) { + return null; + } + + $text = \trim($line['text'], '#'); + if ($this->strictMode && isset($text[0]) && $text[0] !== ' ') { + return null; + } + + $text = \trim($text, ' '); + + $block = [ + 'element' => [ + 'name' => 'h' . $level, + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ], + ], + ]; + + if (\preg_match('/[ #]*{(' . $this->regexAttribute . '+)}[ ]*$/', $block['element']['handler']['argument'], $matches, \PREG_OFFSET_CAPTURE)) { + $attributeString = $matches[1][0]; + + $block['element']['attributes'] = $this->parseAttributeData($attributeString); + $block['element']['handler']['argument'] = \substr($block['element']['handler']['argument'], 0, (int) $matches[0][1]); + } + + // Get the text of the heading + if (isset($block['element']['handler']['argument'])) { + $text = $block['element']['handler']['argument']; + } + + // Get the heading level. Levels are h1, h2, ..., h6 + $level = $block['element']['name']; + + $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + if (!\in_array($level, $headersAllowed)) { + return null; + } + + // Checks if auto generated anchors is allowed + $autoAnchors = $this->options['headings']['auto_anchors'] ?? true; + + $id = $block['element']['attributes']['id'] ?? ($autoAnchors ? $this->createAnchorID($text) : null); + + // Set attributes to head tags + $block['element']['attributes']['id'] = $id; + + $tocHeaders = $this->options['toc']['headings'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + // Check if level are defined as a heading + if (\in_array($level, $tocHeaders)) { + // Add/stores the heading element info to the ToC list + $this->setContentsList([ + 'text' => $text, + 'id' => $id, + 'level' => $level, + ]); + } + + return $block; + } + + /** + * Handle block list + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockList(array $line, ?array $block = null) : ?array + { + if (!($this->options['lists'] ?? true)) { + return null; + } + + [$name, $pattern] = $line['text'][0] <= '-' ? ['ul', '[*+-]'] : ['ol', '[0-9]{1,9}+[.\)]']; + + if (\preg_match('/^(' . $pattern . '([ ]++|$))(.*+)/', $line['text'], $matches) !== 1) { + return null; + } + + $contentIndent = \strlen($matches[2]); + if ($contentIndent >= 5) { + --$contentIndent; + + $matches[1] = \substr($matches[1], 0, -$contentIndent); + $matches[3] = \str_repeat(' ', $contentIndent) . $matches[3]; + } elseif ($contentIndent === 0) { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = \strstr($matches[1], ' ', true); + + $block = [ + 'indent' => $line['indent'], + 'pattern' => $pattern, + 'data' => [ + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : \substr($markerWithoutWhitespace, -1)), + ], + 'element' => [ + 'name' => $name, + 'elements' => [], + ], + ]; + + $block['data']['markerTypeRegex'] = \preg_quote($block['data']['markerType'], '/'); + + if ($name === 'ol') { + $listStart = \ltrim(\strstr($matches[1], $block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') { + if (isset($currentBlock) + && $currentBlock['type'] === 'Paragraph' + && !isset($currentBlock['interrupted']) + ) { + return null; + } + + $block['element']['attributes'] = ['start' => $listStart]; + } + } + + $block['li'] = [ + 'name' => 'li', + 'handler' => [ + 'function' => 'li', + 'argument' => empty($matches[3]) ? [] : [$matches[3]], + 'destination' => 'elements', + ], + ]; + + $block['element']['elements'][] = &$block['li']; + + return $block; + } + + /** + * Handle block quote + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockQuote(array $line, ?array $_ = null) : ?array + { + if (!($this->options['qoutes'] ?? true) + || \preg_match('/^>[ ]?+(.*+)/', $line['text'], $matches) !== 1 + ) { + return null; + } + + return [ + 'element' => [ + 'name' => 'blockquote', + 'handler' => [ + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ], + ], + ]; + } + + /** + * Handle block rule + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockRule(array $line, ?array $_ = null) : ?array + { + if (!($this->options['thematic_breaks'] ?? true)) { + return null; + } + + $marker = $line['text'][0]; + if (\substr_count($line['text'], $marker) >= 3 && \rtrim($line['text'], " {$marker}") === '') { + return [ + 'element' => [ + 'name' => 'hr', + ], + ]; + } + + return null; + } + + /** + * Handle block header + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockSetextHeader(array $line, ?array $block = null) : ?array + { + if (!($this->options['headings'] ?? true) + || $block === null + || $block['type'] !== 'Paragraph' + || isset($block['interrupted']) + ) { + return null; + } + + if ($line['indent'] < 4 && \rtrim(\rtrim($line['text'], ' '), $line['text'][0]) === '') { + $block['element']['name'] = $line['text'][0] === '=' ? 'h1' : 'h2'; + } + + if (\preg_match('/[ ]*{(' . $this->regexAttribute . '+)}[ ]*$/', $block['element']['handler']['argument'], $matches, \PREG_OFFSET_CAPTURE)) { + $attributeString = $matches[1][0]; + + $block['element']['attributes'] = $this->parseAttributeData($attributeString); + $block['element']['handler']['argument'] = \substr($block['element']['handler']['argument'], 0, (int) $matches[0][1]); + } + + // Get the text of the heading + if (isset($block['element']['handler']['argument'])) { + $text = $block['element']['handler']['argument']; + } + + // Get the heading level. Levels are h1, h2, ..., h6 + $level = $block['element']['name']; + + $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + if (!\in_array($level, $headersAllowed)) { + return null; + } + + // Checks if auto generated anchors is allowed + $autoAnchors = $this->options['headings']['auto_anchors'] ?? true; + + $id = $block['element']['attributes']['id'] ?? ($autoAnchors ? $this->createAnchorID($text) : null); + + // Set attributes to head tags + $block['element']['attributes']['id'] = $id; + + $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + + // Check if level are defined as a heading + if (\in_array($level, $headersAllowed)) { + // Add/stores the heading element info to the ToC list + $this->setContentsList([ + 'text' => $text, + 'id' => $id, + 'level' => $level, + ]); + } + + return $block; + } + + /** + * Handle block markup + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockMarkup(array $line, ?array $_ = null) : ?array + { + if (!($this->options['markup'] ?? true) + || $this->markupEscaped || $this->safeMode + || \preg_match('/^<(\w[\w-]*)(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*(\/)?>/', $line['text'], $matches) !== 1 + ) { + return null; + } + + $element = \strtolower($matches[1]); + + if (\in_array($element, $this->textLevelElements)) { + return null; + } + + $block = [ + 'name' => $matches[1], + 'depth' => 0, + 'element' => [ + 'rawHtml' => $line['text'], + 'autobreak' => true, + ], + ]; + + $length = \strlen($matches[0]); + $remainder = \substr($line['text'], $length); + + if (\trim($remainder) === '') { + if (isset($matches[2]) || \in_array($matches[1], $this->voidElements)) { + $block['closed'] = true; + $block['void'] = true; + } + } else { + if (isset($matches[2]) || \in_array($matches[1], $this->voidElements)) { + return null; } - // Get the heading level. Levels are h1, h2, ..., h6 - $level = $block['element']['name']; + if (\preg_match('/<\/' . $matches[1] . '>[ ]*$/i', $remainder)) { + $block['closed'] = true; + } + } - $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - if (!\in_array($level, $headersAllowed)) { - return; + return $block; + } + + /** + * Handle block reference + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockReference(array $line, ?array $_ = null) : ?array + { + if (!($this->options['references'] ?? true) + || \strpos($line['text'], ']') === false + || \preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $line['text'], $matches) !== 1 + + ) { + return null; + } + + $id = \strtolower($matches[1]); + + $this->definitionData['Reference'][$id] = [ + 'url' => UriFactory::build($matches[2]), + 'title' => isset($matches[3]) ? $matches[3] : null, + ]; + + return [ + 'element' => [], + ]; + } + + /** + * Handle block table + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockTable(array $line, ?array $block = null) : ?array + { + if (!($this->options['tables'] ?? true) + || $block === null || $block['type'] !== 'Paragraph' || isset($block['interrupted']) + || (\strpos($block['element']['handler']['argument'], '|') === false + && \strpos($line['text'], '|') === false + && \strpos($line['text'], ':') === false + || \strpos($block['element']['handler']['argument'], "\n") !== false) + || \rtrim($line['text'], ' -:|') !== '' + ) { + return null; + } + + $alignments = []; + + $divider = $line['text']; + $divider = \trim($divider); + $divider = \trim($divider, '|'); + + $dividerCells = \explode('|', $divider); + + foreach ($dividerCells as $dividerCell) { + $dividerCell = \trim($dividerCell); + + if ($dividerCell === '') { + return null; } - // Checks if auto generated anchors is allowed - $autoAnchors = $this->options['headings']['auto_anchors'] ?? true; + $alignment = null; - if ($autoAnchors) { - // Get the anchor of the heading to link from the ToC list - $id = $block['element']['attributes']['id'] ?? $this->createAnchorID($text); - } else { - // Get the anchor of the heading to link from the ToC list - $id = $block['element']['attributes']['id'] ?? null; + if ($dividerCell[0] === ':') { + $alignment = 'left'; } - // Set attributes to head tags - $block['element']['attributes']['id'] = $id; - - $tocHeaders = $this->options['toc']['headings'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - // Check if level are defined as a heading - if (\in_array($level, $tocHeaders)) { - // Add/stores the heading element info to the ToC list - $this->setContentsList([ - 'text' => $text, - 'id' => $id, - 'level' => $level, - ]); + if (\substr($dividerCell, - 1) === ':') { + $alignment = $alignment === 'left' ? 'center' : 'right'; } - return $block; + $alignments [] = $alignment; } - } - protected function blockList($line, array $CurrentBlock = null) - { - $state = $this->options['lists'] ?? true; - if ($state) { - return $this->blockListBase($line, $CurrentBlock); - } - } + $headerElements = []; - protected function blockQuote($line, array $_ = null) - { - $state = $this->options['qoutes'] ?? true; - if ($state) { - return $this->blockQuoteBase($line); - } - } + $header = $block['element']['handler']['argument']; + $header = \trim($header); + $header = \trim($header, '|'); - protected function blockRule($line, array $_ = null) - { - $state = $this->options['thematic_breaks'] ?? true; - if ($state) { - return $this->blockRuleBase($line); - } - } + $headerCells = \explode('|', $header); - protected function blockSetextHeader($line, $block = null) - { - $state = $this->options['headings'] ?? true; - if (!$state) { - return; + if (\count($headerCells) !== \count($alignments)) { + return null; } - $block = $this->blockSetextHeaderBase($line, $block); - if (!empty($block)) { - // Get the text of the heading - if (isset($block['element']['handler']['argument'])) { - $text = $block['element']['handler']['argument']; + + foreach ($headerCells as $index => $headerCell) { + $headerCell = \trim($headerCell); + + $headerElement = [ + 'name' => 'th', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ], + ]; + + if (isset($alignments[$index])) { + $alignment = $alignments[$index]; + + $headerElement['attributes'] = [ + 'style' => "text-align: {$alignment};", + ]; } - // Get the heading level. Levels are h1, h2, ..., h6 - $level = $block['element']['name']; - - $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - if (!\in_array($level, $headersAllowed)) { - return; - } - - // Checks if auto generated anchors is allowed - $autoAnchors = $this->options['headings']['auto_anchors'] ?? true; - - if ($autoAnchors) { - // Get the anchor of the heading to link from the ToC list - $id = $block['element']['attributes']['id'] ?? $this->createAnchorID($text); - } else { - // Get the anchor of the heading to link from the ToC list - $id = $block['element']['attributes']['id'] ?? null; - } - - // Set attributes to head tags - $block['element']['attributes']['id'] = $id; - - $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - - // Check if level are defined as a heading - if (\in_array($level, $headersAllowed)) { - // Add/stores the heading element info to the ToC list - $this->setContentsList([ - 'text' => $text, - 'id' => $id, - 'level' => $level, - ]); - } - - return $block; + $headerElements[] = $headerElement; } + + $block = [ + 'alignments' => $alignments, + 'identified' => true, + 'element' => [ + 'name' => 'table', + 'elements' => [], + ], + ]; + + $block['element']['elements'][] = [ + 'name' => 'thead', + ]; + + $block['element']['elements'][] = [ + 'name' => 'tbody', + 'elements' => [], + ]; + + $block['element']['elements'][0]['elements'][] = [ + 'name' => 'tr', + 'elements' => $headerElements, + ]; + + return $block; } - protected function blockMarkup($line, array $_ = null) + /** + * Handle block abbreviation + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockAbbreviation(array $line, ?array $_ = null) : ?array { - $state = $this->options['markup'] ?? true; - if ($state) { - return $this->blockMarkupBase($line); + if (!($this->options['abbreviations'] ?? true)) { + return null; } - } - protected function blockReference($line, array $_ = null) - { - $state = $this->options['references'] ?? true; - if ($state) { - return $this->blockReferenceBase($line); - } - } - - protected function blockTable($line, $block = null) - { - $state = $this->options['tables'] ?? true; - if ($state) { - return $this->blockTableBase($line, $block); - } - } - - protected function blockAbbreviation($line, array $_ = null) - { $allowCustomAbbr = $this->options['abbreviations']['allow_custom_abbr'] ?? true; - $state = $this->options['abbreviations'] ?? true; - if ($state) { - if (isset($this->options['abbreviations']['predefine'])) { - foreach ($this->options['abbreviations']['predefine'] as $abbreviations => $description) { - $this->DefinitionData['Abbreviation'][$abbreviations] = $description; - } + if (isset($this->options['abbreviations']['predefine'])) { + foreach ($this->options['abbreviations']['predefine'] as $abbreviations => $description) { + $this->definitionData['Abbreviation'][$abbreviations] = $description; } - - if ($allowCustomAbbr == true) { - return $this->blockAbbreviationBase($line); - } - - return; } + + if (!$allowCustomAbbr + || \preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $line['text'], $matches) !== 1 + ) { + return null; + } + + $this->definitionData['Abbreviation'][$matches[1]] = $matches[2]; + + return [ + 'hidden' => true, + ]; } - // Block Math - - protected function blockMath($line, array $_ = null) + /** + * Handle block math + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockMath(array $line, ?array $_ = null) : ?array { $block = [ 'element' => [ @@ -1243,26 +2469,34 @@ class Markdown return $block; } + if (\preg_match('/^(?options['code']['blocks'] ?? true; - $codeMain = $this->options['code'] ?? true; - if ($codeBlock === false || $codeMain === false) { - return; - } - $block = $this->blockFencedCodeBase($line); - - $marker = $line['text'][0]; - $openerLength = \strspn($line['text'], $marker); - $language = \trim( - \preg_replace('/^`{3}([^\s]+)(.+)?/s', '$1', $line['text']) - ); - - $state = $this->options['diagrams'] ?? true; - if ($state) { - // Mermaid.js https://mermaidjs.github.io - if (\strtolower($language) == 'mermaid') { - $element = [ - 'text' => '', - ]; - - return [ - 'char' => $marker, - 'openerLength' => $openerLength, - 'element' => [ - 'element' => $element, - 'name' => 'div', - 'attributes' => [ - 'class' => 'mermaid', - ], - ], - ]; - } - - // Chart.js https://www.chartjs.org/ - if (\strtolower($language) == 'chart') { - $element = [ - 'text' => '', - ]; - - return [ - 'char' => $marker, - 'openerLength' => $openerLength, - 'element' => [ - 'element' => $element, - 'name' => 'canvas', - 'attributes' => [ - 'class' => 'chartjs', - ], - ], - ]; - } - } - - return $block; - } - - // Parsedown Tablespan from @KENNYSOFT - protected function blockTableComplete(array $block) - { - $state = $this->options['tables']['tablespan'] ?? false; - if ($state === false) { - return $block; - } - - if (!isset($block)) { + if (!($this->options['code']['blocks'] ?? true) + || !($this->options['code'] ?? true) + ) { return null; } - $HeaderElements = &$block['element']['elements'][0]['elements'][0]['elements']; + $marker = $line['text'][0]; + $openerLength = \strspn($line['text'], $marker); - for ($index = \count($HeaderElements) - 1; $index >= 0; --$index) { + if ($openerLength < 3) { + return null; + } + + $language = \trim(\preg_replace('/^`{3}([^\s]+)(.+)?/s', '$1', $line['text'])); + + if (!($this->options['diagrams'] ?? true) + || !\in_array($language, ['mermaid', 'chart']) + ) { + // Is code block + $element = [ + 'name' => 'code', + 'text' => '', + ]; + + if ($language !== '```' && !empty($language)) { + $element['attributes'] = ['class' => "language-{$language}"]; + } + + return [ + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => [ + 'name' => 'pre', + 'element' => $element, + ], + ]; + } elseif (\strtolower($language) === 'chartjs') { + // Chart.js https://www.chartjs.org/ + $element = [ + 'text' => '', + ]; + + return [ + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => [ + 'element' => $element, + 'name' => 'canvas', + 'attributes' => [ + 'class' => 'chartjs', + ], + ], + ]; + } elseif (\in_array(\strtolower($language), ['mermaid', 'tuichart'])) { + // Mermaid.js https://mermaidjs.github.io + // TUI.chart https://github.com/nhn/tui.chart + $element = [ + 'text' => '', + ]; + + return [ + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => [ + 'element' => $element, + 'name' => 'div', + 'attributes' => [ + 'class' => \strtolower($language), + ], + ], + ]; + } + + + return null; + } + + /** + * Continue block spoiler + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockSpoiler(array $line, ?array $_ = null) : ?array + { + if (!($this->options['code']['blocks'] ?? true) + || !($this->options['code'] ?? true) + ) { + return null; + } + + $marker = $line['text'][0]; + $openerLength = \strspn($line['text'], $marker); + + if ($openerLength < 3) { + return null; + } + + $summary = \trim(\preg_replace('/^\?{3}(.+)?/s', '$1', $line['text'])); + + $infostring = \trim(\substr($line['text'], $openerLength), "\t "); + if (\strpos($infostring, '?') !== false) { + return null; + } + + // @performance Optimize away the child element for spoilers (if reasonable) + // https://github.com/Karaka-Management/phpOMS/issues/367 + return [ + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => [ + 'name' => 'details', + 'element' => [ + 'text' => '', + 'elements' => [ + [ + 'name' => 'summary', + 'text' => $summary, + ], + [ + 'name' => 'span', + 'text' => '', + ], + ], + ], + ], + ]; + } + + /** + * Complete block table + * + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockTableComplete(array $block) : ?array + { + if (!($this->options['tables']['tablespan'] ?? true)) { + return $block; + } + + $headerElements = &$block['element']['elements'][0]['elements'][0]['elements']; + $headerElementsCount = \count($headerElements); + + for ($index = $headerElementsCount - 1; $index >= 0; --$index) { $colspan = 1; - $HeaderElement = &$HeaderElements[$index]; + $headerElement = &$headerElements[$index]; - while ($index && $HeaderElements[$index - 1]['handler']['argument'] === '>') { + while ($index && $headerElements[$index - 1]['handler']['argument'] === '>') { ++$colspan; - $PreviousHeaderElement = &$HeaderElements[--$index]; - $PreviousHeaderElement['merged'] = true; - if (isset($PreviousHeaderElement['attributes'])) { - $HeaderElement['attributes'] = $PreviousHeaderElement['attributes']; + $previousHeaderElement = &$headerElements[--$index]; + $previousHeaderElement['merged'] = true; + + if (isset($previousHeaderElement['attributes'])) { + $headerElement['attributes'] = $previousHeaderElement['attributes']; } } if ($colspan > 1) { - if (!isset($HeaderElement['attributes'])) { - $HeaderElement['attributes'] = []; + if (!isset($headerElement['attributes'])) { + $headerElement['attributes'] = []; } - $HeaderElement['attributes']['colspan'] = $colspan; + + $headerElement['attributes']['colspan'] = $colspan; } } - for ($index = \count($HeaderElements) - 1; $index >= 0; --$index) { - if (isset($HeaderElements[$index]['merged'])) { - \array_splice($HeaderElements, $index, 1); + for ($index = 0; $index < $headerElementsCount; ++$index) { + if (isset($headerElements[$index]['merged'])) { + unset($headerElements[$index]); } } + $headerElements = \array_values($headerElements); + $rows = &$block['element']['elements'][1]['elements']; foreach ($rows as $rowNo => &$row) { @@ -1421,11 +2752,14 @@ class Markdown if (!isset($element['attributes'])) { $element['attributes'] = []; } + $element['attributes']['colspan'] = $colspan; } } } + $rowCount = \count($rows); + foreach ($rows as $rowNo => &$row) { $elements = &$row['elements']; @@ -1436,7 +2770,11 @@ class Markdown continue; } - while ($rowNo + $rowspan < \count($rows) && $index < \count($rows[$rowNo + $rowspan]['elements']) && $rows[$rowNo + $rowspan]['elements'][$index]['handler']['argument'] === '^' && (@$element['attributes']['colspan'] ?: null) === (@$rows[$rowNo + $rowspan]['elements'][$index]['attributes']['colspan'] ?: null)) { + while ($rowNo + $rowspan < $rowCount + && $index < \count($rows[$rowNo + $rowspan]['elements']) + && $rows[$rowNo + $rowspan]['elements'][$index]['handler']['argument'] === '^' + && ($element['attributes']['colspan'] ?? null) === ($rows[$rowNo + $rowspan]['elements'][$index]['attributes']['colspan'] ?? null) + ) { $rows[$rowNo + $rowspan]['elements'][$index]['merged'] = true; ++$rowspan; } @@ -1445,6 +2783,7 @@ class Markdown if (!isset($element['attributes'])) { $element['attributes'] = []; } + $element['attributes']['rowspan'] = $rowspan; } } @@ -1455,126 +2794,177 @@ class Markdown for ($index = \count($elements) - 1; $index >= 0; --$index) { if (isset($elements[$index]['merged'])) { - \array_splice($elements, $index, 1); + unset($elements[$index]); } } + + $row['elements'] = \array_values($elements); } return $block; } - /* - * Checkbox - * ------------------------------------------------------------------------- - */ - protected function blockCheckbox($line) + /** + * Handle block checkbox + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param null|array $_ Current block (unused parameter) + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockCheckbox(array $line, ?array $_ = null) : ?array { $text = \trim($line['text']); $beginLine = \substr($text, 0, 4); + if ($beginLine === '[ ] ') { return [ - 'handler' => 'checkboxUnchecked', + 'handler' => 'unchecked', 'text' => \substr(\trim($text), 4), ]; - } - - if ($beginLine === '[x] ') { + } elseif ($beginLine === '[x] ') { return [ - 'handler' => 'checkboxChecked', + 'handler' => 'checked', 'text' => \substr(\trim($text), 4), ]; } + + return null; } - protected function blockCheckboxContinue(array $block) : void + /** + * Continue checkbox. + * + * This function doesn't do anything! + * However required as per the parsing workflow since it is automatically called. + * + * @param array{body:string, indent:int, text:string} $_ Line data + * @param array $__ Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockCheckboxContinue(array $_, array $__) : ?array { - // This is here because Parsedown require it. + return null; } - protected function blockCheckboxComplete(array $block) + /** + * Complete block checkbox + * + * @param array $block Current block + * + * @return array + * + * @since 1.0.0 + */ + protected function blockCheckboxComplete(array $block) : array { + $text = $block['text']; + if ($this->markupEscaped || $this->safeMode) { + $text = \htmlspecialchars($text, \ENT_QUOTES, 'UTF-8'); + } + + $html = $block['handler'] === 'unchecked' + ? ' ' . $this->formatOnce($text) + : ' ' . $this->formatOnce($text); + $block['element'] = [ - 'rawHtml' => $this->{$block['handler']}($block['text']), + 'rawHtml' => $html, 'allowRawHtmlInSafeMode' => true, ]; return $block; } - protected function checkboxUnchecked($text) : string - { - if ($this->markupEscaped || $this->safeMode) { - $text = self::escape($text); - } - - return ' '.$this->format($text); - } - - protected function checkboxChecked($text) : string - { - if ($this->markupEscaped || $this->safeMode) { - $text = self::escape($text); - } - - return ' '.$this->format($text); - } - /** - * ------------------------------------------------------------------------ - * Helpers. - * ------------------------------------------------------------------------. + * Formats text without double escaping + * + * @param string $text Text to format + * + * @return string + * + * @since 1.0.0 */ - - /** - * Formats the checkbox label without double escaping. - */ - protected function format($text) + protected function formatOnce(string $text) : string { // backup settings $markupEscaped = $this->markupEscaped; $safeMode = $this->safeMode; // disable rules to prevent double escaping. - $this->setMarkupEscaped(false); - $this->setSafeMode(false); + $this->markupEscaped = false; + $this->safeMode = false; // format line - $text = $this->line($text); + $text = $this->elements($this->lineElements($text)); // reset old values - $this->setMarkupEscaped($markupEscaped); - $this->setSafeMode($safeMode); + $this->markupEscaped = $markupEscaped; + $this->safeMode = $safeMode; return $text; } - protected function parseAttributeData($attributeString) + /** + * Parse attribute data + * + * @param string $attribute Attribute string + * + * @return array + * + * @since 1.0.0 + */ + protected function parseAttributeData(string $attribute) : array { - $state = $this->options['special_attributes'] ?? true; - if ($state) { - return $this->parseAttributeDataBase($attributeString); + if (!($this->options['special_attributes'] ?? true)) { + return []; } - return []; + $data = []; + $attributes = \preg_split('/[ ]+/', $attribute, - 1, \PREG_SPLIT_NO_EMPTY); + $classes = []; + + foreach ($attributes as $attribute) { + if ($attribute[0] === '#') { + $data['id'] = \substr($attribute, 1); + } else { // "." + $classes[] = \substr($attribute, 1); + } + } + + if (!empty($classes)) { + $data['class'] = \implode(' ', $classes); + } + + return $data; } /** * Encodes the ToC tag to a hashed tag and replace. * * This is used to avoid parsing user defined ToC tag which includes "_" in - * their tag such as "[[_toc_]]". Unless it will be parsed as: - * "

[[TOC]]

" + * their tag such as "[[_toc_]]". + * + * @param string $text Tag text to encode + * + * @return string + * + * @since 1.0.0 */ - protected function encodeTagToHash($text) + protected function encodeToCTagToHash(string $text) : string { - $salt = $this->getSalt(); - $tagOrigin = $this->getTagToC(); + $salt = \bin2hex(\random_bytes(4)); + $tagOrigin = $this->options['toc']['set_toc_tag'] ?? '[toc]'; if (\strpos($text, $tagOrigin) === false) { return $text; } - $tagHashed = \hash('sha256', $salt.$tagOrigin); + $tagHashed = \hash('sha256', $salt . $tagOrigin); return \str_replace($tagOrigin, $tagHashed, $text); } @@ -1583,14 +2973,19 @@ class Markdown * Decodes the hashed ToC tag to an original tag and replaces. * * This is used to avoid parsing user defined ToC tag which includes "_" in - * their tag such as "[[_toc_]]". Unless it will be parsed as: - * "

[[TOC]]

" + * their tag such as "[[_toc_]]". + * + * @param string $text Tag text to encode + * + * @return string + * + * @since 1.0.0 */ - protected function decodeTagFromHash($text) + protected function decodeToCTagFromHash(string $text) : string { - $salt = $this->getSalt(); - $tagOrigin = $this->getTagToC(); - $tagHashed = \hash('sha256', $salt.$tagOrigin); + $salt = \bin2hex(\random_bytes(4)); + $tagOrigin = $this->options['toc']['set_toc_tag'] ?? '[toc]'; + $tagHashed = \hash('sha256', $salt . $tagOrigin); if (\strpos($text, $tagHashed) === false) { return $text; @@ -1600,49 +2995,17 @@ class Markdown } /** - * Unique string to use as a salt value. + * Generates an anchor text that are link-able even if the heading is not in ASCII. + * + * @param string $str Header text + * + * @return string + * + * @since 1.0.0 */ - protected function getSalt() + protected function createAnchorID(string $str) : string { - static $salt; - if (isset($salt)) { - return $salt; - } - - $salt = \hash('md5', (string) \time()); - - return $salt; - } - - /** - * Gets the markdown tag for ToC. - */ - protected function getTagToC() - { - return $this->options['toc']['set_toc_tag'] ?? '[toc]'; - } - - /** - * Gets the ID attribute of the ToC for HTML tags. - */ - protected function getIdAttributeToC() - { - if (!empty($this->idToc)) { - return $this->idToc; - } - - return self::ID_ATTRIBUTE_DEFAULT; - } - - /** - * Generates an anchor text that are link-able even if the heading is not in - * ASCII. - */ - protected function createAnchorID($str) : string - { - $optionUrlEncode = $this->options['toc']['urlencode'] ?? false; - if ($optionUrlEncode) { - // Check AnchorID is unique + if ($this->options['toc']['urlencode'] ?? false) { $str = $this->incrementAnchorId($str); return \urlencode($str); @@ -1714,8 +3077,7 @@ class Markdown ]; // Transliterate characters to ASCII - $optionTransliterate = $this->options['toc']['transliterate'] ?? false; - if ($optionTransliterate) { + if ($this->options['toc']['transliterate'] ?? false) { $str = \str_replace(\array_keys($charMap), $charMap, $str); } @@ -1724,7 +3086,7 @@ class Markdown $str = \preg_replace('/[^\p{L}\p{Nd}]+/u', $optionDelimiter, $str); // Remove duplicate delimiters - $str = \preg_replace('/('.\preg_quote($optionDelimiter, '/').'){2,}/', '$1', $str); + $str = \preg_replace('/(' . \preg_quote($optionDelimiter, '/') . '){2,}/', '$1', $str); // Truncate slug to max. characters $optionLimit = $this->options['toc']['limit'] ?? \mb_strlen($str, 'UTF-8'); @@ -1739,51 +3101,26 @@ class Markdown return $this->incrementAnchorId($str); } - /** - * Get only the text from a markdown string. - * It parses to HTML once then trims the tags to get the text. - */ - protected function fetchText($text) : string - { - return \trim(\strip_tags($this->line($text))); - } - /** * Set/stores the heading block to ToC list in a string and array format. + * + * @param array $content ToC content + * + * @return void + * + * @since 1.0.0 */ - protected function setContentsList(array $Content) : void + protected function setContentsList(array $content) : void { - // Stores as an array - $this->setContentsListAsArray($Content); - // Stores as string in markdown list format. - $this->setContentsListAsString($Content); - } + $this->contentsListArray[] = $content; - /** - * Sets/stores the heading block info as an array. - */ - protected function setContentsListAsArray(array $Content) : void - { - $this->contentsListArray[] = $Content; - } - - /** - * Sets/stores the heading block info as a list in markdown format. - */ - protected function setContentsListAsString(array $Content) : void - { - $text = $this->fetchText($Content['text']); - $id = $Content['id']; - $level = (int) \trim($Content['level'], 'h'); - $link = "[{$text}](#{$id})"; + $text = \trim(\strip_tags($this->elements($this->lineElements($content['text'])))); + $id = $content['id']; + $level = (int) \trim($content['level'], 'h'); if ($this->firstHeadLevel === 0) { $this->firstHeadLevel = $level; } - $cutIndent = $this->firstHeadLevel - 1; - $level = $cutIndent > $level ? 1 : $level - $cutIndent; - - $indent = \str_repeat(' ', $level); // Stores in markdown list format as below: // - [Header1](#Header1) @@ -1791,40 +3128,55 @@ class Markdown // - [Header3](#Header3) // - [Header2-2](#Header2-2) // ... - $this->contentsListString .= "{$indent}- {$link}".\PHP_EOL; + $this->contentsListString .= \str_repeat( + ' ', + $this->firstHeadLevel - 1 > $level + ? 1 + : $level - ($this->firstHeadLevel - 1) + ) . ' - [' . $text . '](#' . $id . ")\n"; } /** - * Collect and count anchors in use to prevent duplicated ids. Return string - * with incremental, numeric suffix. Also init optional blacklist of ids. + * Collect and count anchors in use to prevent duplicated ids. + * + * Also init optional blacklist of ids. + * + * @param string $str Header anchor + * + * @return string Incremental, numeric suffix + * + * @since 1.0.0 */ - protected function incrementAnchorId($str) + protected function incrementAnchorId(string $str) : string { // add blacklist to list of used anchors if (!$this->isBlacklistInitialized) { $this->initBlacklist(); } - $this->anchorDuplicates[$str] = isset($this->anchorDuplicates[$str]) ? ++$this->anchorDuplicates[$str] : 0; + do { + $this->anchorDuplicates[$str] = isset($this->anchorDuplicates[$str]) ? ++$this->anchorDuplicates[$str] : 0; - $newStr = $str; + $newStr = $str; - if ($count = $this->anchorDuplicates[$str]) { - $newStr .= "-{$count}"; - - // increment until conversion doesn't produce new duplicates anymore - if (isset($this->anchorDuplicates[$newStr])) { - $newStr = $this->incrementAnchorId($str); - } else { - $this->anchorDuplicates[$newStr] = 0; + if (($count = $this->anchorDuplicates[$str]) === 0) { + return $newStr; } - } + + $newStr .= '-' . $count; + } while(isset($this->anchorDuplicates[$newStr])); + + $this->anchorDuplicates[$newStr] = 0; return $newStr; } /** * Add blacklisted ids to anchor list. + * + * @return void + * + * @since 1.0.0 */ protected function initBlacklist() : void { @@ -1834,494 +3186,388 @@ class Markdown if (!empty($this->options['headings']['blacklist']) && \is_array($this->options['headings']['blacklist'])) { foreach ($this->options['headings']['blacklist'] as $v) { - if (\is_string($v)) { - $this->anchorDuplicates[$v] = 0; - } + $this->anchorDuplicates[$v] = 0; } } $this->isBlacklistInitialized = true; } - protected function lineElements($text, $nonNestables = []) + /** + * Parse inline elements + * + * @param string $text Text to parse + * @param array $nonNestables Inline elements that are not allowed to be nested + * + * @return array + * + * @since 1.0.0 + */ + protected function lineElements(string $text, array $nonNestables = []) : array { - $Elements = []; + $elements = []; - $nonNestables = ( - empty($nonNestables) - ? [] - : \array_combine($nonNestables, $nonNestables) - ); + if (!empty($nonNestables)) { + $nonNestables = \array_combine($nonNestables, $nonNestables); + } - // $excerpt is based on the first occurrence of a marker - - while ($excerpt = \strpbrk($text, $this->inlineMarkerList)) { - $marker = $excerpt[0]; - - $markerPosition = \strlen($text) - \strlen($excerpt); + // $exc is based on the first occurrence of a marker + while (($exc = \strpbrk($text, $this->inlineMarkerList)) !== false) { + $marker = $exc[0]; + $markerPosition = \strlen($text) - \strlen($exc); // Get the first char before the marker $beforeMarkerPosition = $markerPosition - 1; $charBeforeMarker = $beforeMarkerPosition >= 0 ? $text[$markerPosition - 1] : ''; - $Excerpt = ['text' => $excerpt, 'context' => $text, 'before' => $charBeforeMarker]; + $excerpt = ['text' => $exc, 'context' => $text, 'before' => $charBeforeMarker]; - foreach ($this->InlineTypes[$marker] as $inlineType) { + foreach ($this->inlineTypes[$marker] as $inlineType) { // check to see if the current inline type is nestable in the current context - if (isset($nonNestables[$inlineType])) { continue; } - $Inline = $this->{"inline{$inlineType}"}($Excerpt); - - if (!isset($Inline)) { + $inline = $this->{"inline{$inlineType}"}($excerpt); + if ($inline === null) { continue; } // makes sure that the inline belongs to "our" marker - - if (isset($Inline['position']) && $Inline['position'] > $markerPosition) { + if (isset($inline['position']) && $inline['position'] > $markerPosition) { continue; } // sets a default inline position - - if (!isset($Inline['position'])) { - $Inline['position'] = $markerPosition; + if (!isset($inline['position'])) { + $inline['position'] = $markerPosition; } // cause the new element to 'inherit' our non nestables - - $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) - ? \array_merge($Inline['element']['nonNestables'], $nonNestables) - : $nonNestables - ; + $inline['element']['nonNestables'] = isset($inline['element']['nonNestables']) + ? \array_merge($inline['element']['nonNestables'], $nonNestables) + : $nonNestables; // the text that comes before the inline - $unmarkedText = \substr($text, 0, $Inline['position']); + $unmarkedText = \substr($text, 0, $inline['position']); // compile the unmarked text - $InlineText = $this->inlineText($unmarkedText); - $Elements[] = $InlineText['element']; + $inlineText = $this->inlineText($unmarkedText); + $elements[] = $inlineText['element']; // compile the inline - $Elements[] = $this->extractElement($Inline); + $elements[] = $this->extractElement($inline); // remove the examined text - $text = \substr($text, $Inline['position'] + $Inline['extent']); + $text = \substr($text, $inline['position'] + $inline['extent']); continue 2; } // the marker does not belong to an inline - $unmarkedText = \substr($text, 0, $markerPosition + 1); - $InlineText = $this->inlineText($unmarkedText); - $Elements[] = $InlineText['element']; + $inlineText = $this->inlineText($unmarkedText); + $elements[] = $inlineText['element']; $text = \substr($text, $markerPosition + 1); } - $InlineText = $this->inlineText($text); - $Elements[] = $InlineText['element']; + $inlineText = $this->inlineText($text); + $elements[] = $inlineText['element']; - foreach ($Elements as &$Element) { - if (!isset($Element['autobreak'])) { - $Element['autobreak'] = false; + foreach ($elements as &$element) { + if (!isset($element['autobreak'])) { + $element['autobreak'] = false; } } - return $Elements; + return $elements; } - private function pregReplaceAssoc(array $replace, $subject) + /** + * Continue block footnote + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockFootnoteContinue(array $line, array $block) : ?array { - return \preg_replace(\array_keys($replace), \array_values($replace), $subject); - } - - # - # Blocks - # - - # - # Abbreviation - - protected function blockAbbreviationBase($Line) - { - if (\preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches)) - { - $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2]; - - return [ - 'hidden' => true, - ]; - } - } - - # - # Footnote - - protected function blockFootnoteBase($Line) - { - if (\preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches)) - { - return [ - 'label' => $matches[1], - 'text' => $matches[2], - 'hidden' => true, - ]; - } - } - - protected function blockFootnoteContinue($Line, $Block) - { - if ($Line['text'][0] === '[' && \preg_match('/^\[\^(.+?)\]:/', $Line['text'])) - { - return; + if ($line['text'][0] === '[' + && \preg_match('/^\[\^(.+?)\]:/', $line['text']) + ) { + return null; } - if (isset($Block['interrupted'])) - { - if ($Line['indent'] >= 4) - { - $Block['text'] .= "\n\n" . $Line['text']; + if (isset($block['interrupted'])) { + if ($line['indent'] >= 4) { + $block['text'] .= "\n\n" . $line['text']; - return $Block; + return $block; } - } - else - { - $Block['text'] .= "\n" . $Line['text']; + } else { + $block['text'] .= "\n" . $line['text']; - return $Block; + return $block; } + + return null; } - protected function blockFootnoteComplete($Block) + /** + * Complete block footnote + * + * @param array $block Current block + * + * @return array + * + * @since 1.0.0 + */ + protected function blockFootnoteComplete(array $block) : array { - $this->DefinitionData['Footnote'][$Block['label']] = [ - 'text' => $Block['text'], + $this->definitionData['Footnote'][$block['label']] = [ + 'text' => $block['text'], 'count' => null, 'number' => null, ]; - return $Block; + return $block; } - # - # Definition List - - protected function blockDefinitionListBase($Line, $Block) + /** + * Continue block footnote + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockDefinitionListContinue(array $line, array $block) : ?array { - if (!isset($Block) || $Block['type'] !== 'Paragraph') - { - return; + if ($line['text'][0] === ':') { + return $this->addDdElement($line, $block); } - $Element = [ - 'name' => 'dl', - 'elements' => [], - ]; - - $terms = \explode("\n", $Block['element']['handler']['argument']); - - foreach ($terms as $term) - { - $Element['elements'] []= [ - 'name' => 'dt', - 'handler' => [ - 'function' => 'lineElements', - 'argument' => $term, - 'destination' => 'elements', - ], - ]; + if (isset($block['interrupted']) && $line['indent'] === 0) { + return null; } - $Block['element'] = $Element; + if (isset($block['interrupted'])) { + $block['dd']['handler']['function'] = 'textElements'; + $block['dd']['handler']['argument'] .= "\n\n"; - return $this->addDdElement($Line, $Block); + $block['dd']['handler']['destination'] = 'elements'; + + unset($block['interrupted']); + } + + $text = \substr($line['body'], \min($line['indent'], 4)); + + $block['dd']['handler']['argument'] .= "\n" . $text; + + return $block; } - protected function blockDefinitionListContinue($Line, array $Block) + /** + * Continue block markup + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockMarkupContinue(array $line, array $block) : ?array { - if ($Line['text'][0] === ':') - { - return $this->addDdElement($Line, $Block); + if (isset($block['closed'])) { + return null; } - else - { - if (isset($Block['interrupted']) && $Line['indent'] === 0) - { - return; - } - if (isset($Block['interrupted'])) - { - $Block['dd']['handler']['function'] = 'textElements'; - $Block['dd']['handler']['argument'] .= "\n\n"; - - $Block['dd']['handler']['destination'] = 'elements'; - - unset($Block['interrupted']); - } - - $text = \substr($Line['body'], \min($Line['indent'], 4)); - - $Block['dd']['handler']['argument'] .= "\n" . $text; - - return $Block; + if (\preg_match('/^<' . $block['name'] . '(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*>/i', $line['text'])) { + // open + ++$block['depth']; } + + if (\preg_match('/(.*?)<\/' . $block['name'] . '>[ ]*$/i', $line['text'], $matches)) { + // close + if ($block['depth'] > 0) { + --$block['depth']; + } else { + $block['closed'] = true; + } + } + + if (isset($block['interrupted'])) { + $block['element']['rawHtml'] .= "\n"; + unset($block['interrupted']); + } + + $block['element']['rawHtml'] .= "\n".$line['body']; + + return $block; } - # - # Header - - protected function blockHeaderBase($Line) + /** + * Complete block markup + * + * @param array $block Current block + * + * @return array + * + * @since 1.0.0 + */ + protected function blockMarkupComplete(array $block) : array { - $Block = $this->blockHeaderParent($Line); - - if ($Block !== null && \preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, \PREG_OFFSET_CAPTURE)) - { - $attributeString = $matches[1][0]; - - $Block['element']['attributes'] = $this->parseAttributeData($attributeString); - - $Block['element']['handler']['argument'] = \substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + if (!isset($block['void'])) { + $block['element']['rawHtml'] = $this->processTag($block['element']['rawHtml']); } - return $Block; + return $block; } - # - # Markup - - protected function blockMarkupBase($Line) + /** + * Handle footnote marker + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array{extent:int, element:array} + * + * @since 1.0.0 + */ + protected function inlineFootnoteMarker(array $excerpt) : ?array { - if ($this->markupEscaped || $this->safeMode) - { - return; + if (\preg_match('/^\[\^(.+?)\]/', $excerpt['text'], $matches) !== 1) { + return null; } - if (\preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) - { - $element = \strtolower($matches[1]); + $name = $matches[1]; - if (\in_array($element, $this->textLevelElements)) - { - return; - } - - $Block = [ - 'name' => $matches[1], - 'depth' => 0, - 'element' => [ - 'rawHtml' => $Line['text'], - 'autobreak' => true, - ], - ]; - - $length = \strlen($matches[0]); - $remainder = \substr($Line['text'], $length); - - if (\trim($remainder) === '') - { - if (isset($matches[2]) || \in_array($matches[1], $this->voidElements)) - { - $Block['closed'] = true; - $Block['void'] = true; - } - } - else - { - if (isset($matches[2]) || \in_array($matches[1], $this->voidElements)) - { - return; - } - if (\preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) - { - $Block['closed'] = true; - } - } - - return $Block; - } - } - - protected function blockMarkupContinue($Line, array $Block) - { - if (isset($Block['closed'])) - { - return; + if (!isset($this->definitionData['Footnote'][$name])) { + return null; } - if (\preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open - { - ++$Block['depth']; + ++$this->definitionData['Footnote'][$name]['count']; + + if (!isset($this->definitionData['Footnote'][$name]['number'])) { + $this->definitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; // » & } - if (\preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close - { - if ($Block['depth'] > 0) - { - --$Block['depth']; - } - else - { - $Block['closed'] = true; - } - } - - if (isset($Block['interrupted'])) - { - $Block['element']['rawHtml'] .= "\n"; - unset($Block['interrupted']); - } - - $Block['element']['rawHtml'] .= "\n".$Line['body']; - - return $Block; - } - - protected function blockMarkupComplete($Block) - { - if (!isset($Block['void'])) - { - $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']); - } - - return $Block; - } - - # - # Setext - - protected function blockSetextHeaderBase($Line, array $Block = null) - { - $Block = $this->blockSetextHeaderParent($Line, $Block); - - if ($Block !== null && \preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, \PREG_OFFSET_CAPTURE)) - { - $attributeString = $matches[1][0]; - - $Block['element']['attributes'] = $this->parseAttributeData($attributeString); - - $Block['element']['handler']['argument'] = \substr($Block['element']['handler']['argument'], 0, $matches[0][1]); - } - - return $Block; - } - - # - # Inline Elements - # - - # - # Footnote Marker - - protected function inlineFootnoteMarker($Excerpt) - { - if (\preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) - { - $name = $matches[1]; - - if (!isset($this->DefinitionData['Footnote'][$name])) - { - return; - } - - ++$this->DefinitionData['Footnote'][$name]['count']; - - if (!isset($this->DefinitionData['Footnote'][$name]['number'])) - { - $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » & - } - - $Element = [ + return [ + 'extent' => \strlen($matches[0]), + 'element' => [ 'name' => 'sup', - 'attributes' => ['id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name], + 'attributes' => ['id' => 'fnref' . $this->definitionData['Footnote'][$name]['count'] . ':' . $name], 'element' => [ 'name' => 'a', - 'attributes' => ['href' => '#fn:'.$name, 'class' => 'footnote-ref'], - 'text' => $this->DefinitionData['Footnote'][$name]['number'], + 'attributes' => ['href' => '#fn:' . $name, 'class' => 'footnote-ref'], + 'text' => $this->definitionData['Footnote'][$name]['number'], ], - ]; - - return [ - 'extent' => \strlen($matches[0]), - 'element' => $Element, - ]; - } + ], + ]; } - private $footnoteCount = 0; - - # - # ~ - # - - private $currentAbreviation; - - private $currentMeaning; - - protected function insertAbreviation(array $Element) + /** + * Insert/replace text with abbreviation + * + * @param array $element Element to insert abbreviation into + * + * @return array + * + * @since 1.0.0 + */ + protected function insertAbreviation(array $element) : array { - if (isset($Element['text'])) - { - $Element['elements'] = self::pregReplaceElements( - '/\b'.\preg_quote($this->currentAbreviation, '/').'\b/', + if (!isset($element['text'])) { + return $element; + } + + $element['elements'] = self::pregReplaceElements( + '/\b' . \preg_quote($this->currentAbreviation, '/') . '\b/', + [ [ - [ - 'name' => 'abbr', - 'attributes' => [ - 'title' => $this->currentMeaning, - ], - 'text' => $this->currentAbreviation, + 'name' => 'abbr', + 'attributes' => [ + 'title' => $this->currentMeaning, ], + 'text' => $this->currentAbreviation, ], - $Element['text'] + ], + $element['text'] + ); + + unset($element['text']); + + return $element; + } + + /** + * Inline elements in text + * + * @param string $text Text to search for inlinable elements + * + * @return array + * + * @since 1.0.0 + */ + protected function inlineText(string $text) : array + { + $inline = [ + 'extent' => \strlen($text), + 'element' => [], + ]; + + $inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + [ + ['name' => 'br'], + ['text' => "\n"], + ], + $text + ); + + // Handle abbreviations + if (!isset($this->definitionData['Abbreviation'])) { + return $inline; + } + + foreach ($this->definitionData['Abbreviation'] as $abbreviation => $meaning) { + $this->currentAbreviation = $abbreviation; + $this->currentMeaning = $meaning; + + $inline['element'] = $this->elementApplyRecursiveDepthFirst( + 'insertAbreviation', + $inline['element'] ); - - unset($Element['text']); } - return $Element; + return $inline; } - protected function inlineText($text) + /** + * Handle block list + * + * @param array $line Line data + * @param array $block Current block + * + * @return array + * + * @since 1.0.0 + */ + protected function addDdElement(array $line, array $block) : array { - $Inline = $this->inlineTextParent($text); - - if (isset($this->DefinitionData['Abbreviation'])) - { - foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) - { - $this->currentAbreviation = $abbreviation; - $this->currentMeaning = $meaning; - - $Inline['element'] = $this->elementApplyRecursiveDepthFirst( - [$this, 'insertAbreviation'], - $Inline['element'] - ); - } - } - - return $Inline; - } - - # - # Util Methods - # - - protected function addDdElement(array $Line, array $Block) - { - $text = \substr($Line['text'], 1); + $text = \substr($line['text'], 1); $text = \trim($text); - unset($Block['dd']); + unset($block['dd']); - $Block['dd'] = [ + $block['dd'] = [ 'name' => 'dd', 'handler' => [ 'function' => 'lineElements', @@ -2330,21 +3576,27 @@ class Markdown ], ]; - if (isset($Block['interrupted'])) - { - $Block['dd']['handler']['function'] = 'textElements'; + if (isset($block['interrupted'])) { + $block['dd']['handler']['function'] = 'textElements'; - unset($Block['interrupted']); + unset($block['interrupted']); } - $Block['element']['elements'] []= & $Block['dd']; + $block['element']['elements'][] = &$block['dd']; - return $Block; + return $block; } - protected function buildFootnoteElement() + /** + * Create footnotes + * + * @return array + * + * @since 1.0.0 + */ + protected function buildFootnoteElement() : array { - $Element = [ + $element = [ 'name' => 'div', 'attributes' => ['class' => 'footnotes'], 'elements' => [ @@ -2356,25 +3608,19 @@ class Markdown ], ]; - \uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes'); + \uasort($this->definitionData['Footnote'], 'self::sortFootnotes'); - foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) - { - if (!isset($DefinitionData['number'])) - { + foreach ($this->definitionData['Footnote'] as $definitionId => $definitionData) { + if (!isset($definitionData['number'])) { continue; } - $text = $DefinitionData['text']; - - $textElements = $this->textElements($text); - - $numbers = \range(1, $DefinitionData['count']); - + $text = $definitionData['text']; + $textElements = $this->textElements($text); + $numbers = \range(1, $definitionData['count']); $backLinkElements = []; - foreach ($numbers as $number) - { + foreach ($numbers as $number) { $backLinkElements[] = ['text' => ' ']; $backLinkElements[] = [ 'name' => 'a', @@ -2393,8 +3639,7 @@ class Markdown $n = \count($textElements) - 1; - if ($textElements[$n]['name'] === 'p') - { + if ($textElements[$n]['name'] === 'p') { $backLinkElements = \array_merge( [ [ @@ -2414,752 +3659,478 @@ class Markdown $backLinkElements ), ]; - } - else - { + } else { $textElements[] = [ 'name' => 'p', 'elements' => $backLinkElements, ]; } - $Element['elements'][1]['elements'] []= [ + $element['elements'][1]['elements'][] = [ 'name' => 'li', - 'attributes' => ['id' => 'fn:'.$definitionId], + 'attributes' => ['id' => 'fn:' . $definitionId], 'elements' => \array_merge( $textElements ), ]; } - return $Element; + return $element; } - # ~ - - protected function parseAttributeDataBase($attributeString) + /** + * Handle markup/html. + * + * Ensures that html is well formed. + * + * This function is called recursively + * + * @param string $elementMarkup Markup + * + * @return string + * + * @since 1.0.0 + */ + protected function processTag(string $elementMarkup) : string { - $Data = []; - - $attributes = \preg_split('/[ ]+/', $attributeString, - 1, \PREG_SPLIT_NO_EMPTY); - - foreach ($attributes as $attribute) - { - if ($attribute[0] === '#') - { - $Data['id'] = \substr($attribute, 1); - } - else # "." - { - $classes []= \substr($attribute, 1); - } - } - - if (isset($classes)) - { - $Data['class'] = \implode(' ', $classes); - } - - return $Data; - } - - # ~ - - protected function processTag($elementMarkup) # recursive - { - # http://stackoverflow.com/q/1148928/200145 + // http://stackoverflow.com/q/1148928/200145 \libxml_use_internal_errors(true); - $DOMDocument = new \DOMDocument(); + $dom = new \DOMDocument(); - # http://stackoverflow.com/q/11309194/200145 + // http://stackoverflow.com/q/11309194/200145 $elementMarkup = \mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); - # http://stackoverflow.com/q/4879946/200145 - $DOMDocument->loadHTML($elementMarkup); - $DOMDocument->removeChild($DOMDocument->doctype); - $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild); + // http://stackoverflow.com/q/4879946/200145 + $dom->loadHTML($elementMarkup); + $dom->removeChild($dom->doctype); + $dom->replaceChild($dom->firstChild->firstChild->firstChild, $dom->firstChild); $elementText = ''; - if ($DOMDocument->documentElement->getAttribute('markdown') === '1') - { - foreach ($DOMDocument->documentElement->childNodes as $Node) - { - $elementText .= $DOMDocument->saveHTML($Node); + if ($dom->documentElement->getAttribute('markdown') === '1') { + foreach ($dom->documentElement->childNodes as $node) { + $elementText .= $dom->saveHTML($node); } - $DOMDocument->documentElement->removeAttribute('markdown'); + $dom->documentElement->removeAttribute('markdown'); - $elementText = "\n".$this->text($elementText)."\n"; - } - else - { - foreach ($DOMDocument->documentElement->childNodes as $Node) - { - $nodeMarkup = $DOMDocument->saveHTML($Node); - - if ($Node instanceof \DOMElement && ! \in_array($Node->nodeName, $this->textLevelElements)) - { - $elementText .= $this->processTag($nodeMarkup); - } - else - { - $elementText .= $nodeMarkup; - } + $elementText = "\n" . $this->text($elementText) . "\n"; + } else { + foreach ($dom->documentElement->childNodes as $node) { + $nodeMarkup = $dom->saveHTML($node); + $elementText .= $node instanceof \DOMElement && !\in_array($node->nodeName, $this->textLevelElements) + ? $this->processTag($nodeMarkup) + : $nodeMarkup; } } - # because we don't want for markup to get encoded - $DOMDocument->documentElement->nodeValue = 'placeholder\x1A'; + // because we don't want for markup to get encoded + $dom->documentElement->nodeValue = 'placeholder\x1A'; - $markup = $DOMDocument->saveHTML($DOMDocument->documentElement); + $markup = $dom->saveHTML($dom->documentElement); return \str_replace('placeholder\x1A', $elementText, $markup); } - # ~ - - protected function sortFootnotes($A, $B) # callback + /** + * Footnote sort function + * + * @param array $a First element + * @param array $b Second element + * + * @return int + * + * @since 1.0.0 + */ + protected function sortFootnotes(array $a, array $b) : int { - return $A['number'] - $B['number']; + return $a['number'] <=> $b['number']; } - # - # Fields - # - - protected $regexAttribute = '(?:[#.][-\w]+[ ]*)'; - - protected function textElements($text) + /** + * Parse text elements to lines and then handle lines. + * + * @param string $text Text to parse + * + * @return array + * + * @since 1.0.0 + */ + protected function textElements(string $text) : array { - # make sure no definitions are set - $this->DefinitionData = []; + // make sure no definitions are set + $this->definitionData = []; - # standardize line breaks + // standardize line breaks $text = \str_replace(["\r\n", "\r"], "\n", $text); - # remove surrounding line breaks + // remove surrounding line breaks $text = \trim($text, "\n"); - # split text into lines + // split text into lines $lines = \explode("\n", $text); - # iterate through lines to identify blocks + // iterate through lines to identify blocks return $this->linesElements($lines); } - # - # Setters - # - - public function setBreaksEnabled($breaksEnabled) + /** + * Handle lines of elements + * + * @param string[] $lines Lines to parse + * + * @return array + * + * @since 1.0.0 + */ + protected function linesElements(array $lines) : array { - $this->breaksEnabled = $breaksEnabled; + $elements = []; + $currentBlock = null; - return $this; - } - - protected $breaksEnabled; - - public function setMarkupEscaped($markupEscaped) - { - $this->markupEscaped = $markupEscaped; - - return $this; - } - - protected $markupEscaped; - - public function setUrlsLinked($urlsLinked) - { - $this->urlsLinked = $urlsLinked; - - return $this; - } - - protected $urlsLinked = true; - - public function setSafeMode($safeMode) - { - $this->safeMode = (bool) $safeMode; - - return $this; - } - - protected $safeMode; - - public function setStrictMode($strictMode) - { - $this->strictMode = (bool) $strictMode; - - return $this; - } - - protected $strictMode; - - protected $safeLinksWhitelist = [ - 'http://', - 'https://', - 'ftp://', - 'ftps://', - 'mailto:', - 'tel:', - 'data:image/png;base64,', - 'data:image/gif;base64,', - 'data:image/jpeg;base64,', - 'irc:', - 'ircs:', - 'git:', - 'ssh:', - 'news:', - 'steam:', - ]; - - # - # Lines - # - - protected $BlockTypes = [ - '#' => ['Header'], - '*' => ['Rule', 'List', 'Abbreviation'], - '+' => ['List'], - '-' => ['SetextHeader', 'Table', 'Rule', 'List'], - '0' => ['List'], - '1' => ['List'], - '2' => ['List'], - '3' => ['List'], - '4' => ['List'], - '5' => ['List'], - '6' => ['List'], - '7' => ['List'], - '8' => ['List'], - '9' => ['List'], - ':' => ['Table', 'DefinitionList'], - '<' => ['Comment', 'Markup'], - '=' => ['SetextHeader'], - '>' => ['Quote'], - '[' => ['Footnote', 'Reference'], - '_' => ['Rule'], - '`' => ['FencedCode'], - '|' => ['Table'], - '~' => ['FencedCode'], - ]; - - # ~ - - protected $unmarkedBlockTypes = [ - 'Code', - ]; - - # - # Blocks - # - - protected function lines(array $lines) - { - return $this->elements($this->linesElements($lines)); - } - - protected function linesElements(array $lines) - { - $Elements = []; - $CurrentBlock = null; - - foreach ($lines as $line) - { - if (\rtrim($line) === '') - { - if (isset($CurrentBlock)) - { - $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) - ? $CurrentBlock['interrupted'] + 1 : 1 + foreach ($lines as $line) { + if (\rtrim($line) === '') { + if (isset($currentBlock)) { + $currentBlock['interrupted'] = (isset($currentBlock['interrupted']) + ? $currentBlock['interrupted'] + 1 : 1 ); } continue; } - while (($beforeTab = \strstr($line, "\t", true)) !== false) - { + while (($beforeTab = \strstr($line, "\t", true)) !== false) { $shortage = 4 - \mb_strlen($beforeTab, 'utf-8') % 4; $line = $beforeTab . \str_repeat(' ', $shortage) - . \substr($line, \strlen($beforeTab) + 1) - ; + . \substr($line, \strlen($beforeTab) + 1); } $indent = \strspn($line, ' '); + $text = $indent > 0 ? \substr($line, $indent) : $line; + $line = ['body' => $line, 'indent' => $indent, 'text' => $text]; - $text = $indent > 0 ? \substr($line, $indent) : $line; + if (isset($currentBlock['continuable'])) { + $methodName = 'block' . $currentBlock['type'] . 'Continue'; + $block = $this->{$methodName}($line, $currentBlock); - # ~ - - $Line = ['body' => $line, 'indent' => $indent, 'text' => $text]; - - # ~ - - if (isset($CurrentBlock['continuable'])) - { - $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; - $Block = $this->{$methodName}($Line, $CurrentBlock); - - if (isset($Block)) { - $CurrentBlock = $Block; + if (isset($block)) { + $currentBlock = $block; continue; - } elseif ($this->isBlockCompletable($CurrentBlock['type'])) { - $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; - $CurrentBlock = $this->{$methodName}($CurrentBlock); + } elseif (\in_array($currentBlock['type'], self::COMPLETABLE)) { + $methodName = 'block' . $currentBlock['type'] . 'Complete'; + $currentBlock = $this->{$methodName}($currentBlock); } } - # ~ - - $marker = $text[0]; - - # ~ - + $marker = $text[0]; $blockTypes = $this->unmarkedBlockTypes; - if (isset($this->BlockTypes[$marker])) - { - foreach ($this->BlockTypes[$marker] as $blockType) - { - $blockTypes []= $blockType; + if (isset($this->blockTypes[$marker])) { + foreach ($this->blockTypes[$marker] as $blockType) { + $blockTypes [] = $blockType; } } - # - # ~ + foreach ($blockTypes as $blockType) { + $block = $this->{"block{$blockType}"}($line, $currentBlock); - foreach ($blockTypes as $blockType) - { - $Block = $this->{"block{$blockType}"}($Line, $CurrentBlock); + if (isset($block)) { + $block['type'] = $blockType; - if (isset($Block)) - { - $Block['type'] = $blockType; - - if (!isset($Block['identified'])) - { - if (isset($CurrentBlock)) - { - $Elements[] = $this->extractElement($CurrentBlock); + if (!isset($block['identified'])) { + if (isset($currentBlock)) { + $elements[] = $this->extractElement($currentBlock); } - $Block['identified'] = true; + $block['identified'] = true; } - if ($this->isBlockContinuable($blockType)) - { - $Block['continuable'] = true; + if (\in_array($blockType, self::CONTINUABLE)) { + $block['continuable'] = true; } - $CurrentBlock = $Block; + $currentBlock = $block; continue 2; } } - # ~ - - if (isset($CurrentBlock) && $CurrentBlock['type'] === 'Paragraph') - { - $Block = $this->paragraphContinue($Line, $CurrentBlock); + if (isset($currentBlock) && $currentBlock['type'] === 'Paragraph') { + $block = $this->paragraphContinue($line, $currentBlock); } - if (isset($Block)) - { - $CurrentBlock = $Block; - } - else - { - if (isset($CurrentBlock)) - { - $Elements[] = $this->extractElement($CurrentBlock); + if (isset($block)) { + $currentBlock = $block; + } else { + if (isset($currentBlock)) { + $elements[] = $this->extractElement($currentBlock); } - $CurrentBlock = $this->paragraph($Line); - - $CurrentBlock['identified'] = true; - } - } - - # ~ - - if (isset($CurrentBlock['continuable']) && $this->isBlockCompletable($CurrentBlock['type'])) - { - $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; - $CurrentBlock = $this->{$methodName}($CurrentBlock); - } - - # ~ - - if (isset($CurrentBlock)) - { - $Elements[] = $this->extractElement($CurrentBlock); - } - - # ~ - - return $Elements; - } - - protected function extractElement(array $Component) - { - if (!isset($Component['element'])) - { - if (isset($Component['markup'])) - { - $Component['element'] = ['rawHtml' => $Component['markup']]; - } - elseif (isset($Component['hidden'])) - { - $Component['element'] = []; - } - } - - return $Component['element']; - } - - protected function isBlockContinuable($Type) : bool - { - return \method_exists($this, 'block' . $Type . 'Continue'); - } - - protected function isBlockCompletable($Type) : bool - { - return \method_exists($this, 'block' . $Type . 'Complete'); - } - - # - # Code - - protected function blockCodeBase($Line, $Block = null) - { - if (isset($Block) && $Block['type'] === 'Paragraph' && !isset($Block['interrupted'])) - { - return; - } - - if ($Line['indent'] >= 4) - { - $text = \substr($Line['body'], 4); - - return [ - 'element' => [ - 'name' => 'pre', + $currentBlock = [ + 'type' => 'Paragraph', 'element' => [ - 'name' => 'code', - 'text' => $text, + 'name' => 'p', + 'handler' => [ + 'function' => 'lineElements', + 'argument' => $line['text'], + 'destination' => 'elements', + ], ], - ], - ]; - } - } + ]; - protected function blockCodeContinue($Line, $Block) - { - if ($Line['indent'] >= 4) - { - if (isset($Block['interrupted'])) - { - $Block['element']['element']['text'] .= \str_repeat("\n", $Block['interrupted']); - - unset($Block['interrupted']); + $currentBlock['identified'] = true; } - - $Block['element']['element']['text'] .= "\n"; - - $text = \substr($Line['body'], 4); - - $Block['element']['element']['text'] .= $text; - - return $Block; - } - } - - protected function blockCodeComplete($Block) - { - return $Block; - } - - # - # Comment - - protected function blockCommentBase($Line) - { - if ($this->markupEscaped || $this->safeMode) - { - return; } - if (\str_starts_with($Line['text'], '') !== false) - { - $Block['closed'] = true; + if (isset($currentBlock)) { + $elements[] = $this->extractElement($currentBlock); + } + + return $elements; + } + + /** + * Extract element from block + * + * @param array $block Block + * + * @return array + * + * @since 1.0.0 + */ + protected function extractElement(array $block) : array + { + if (!isset($block['element'])) { + if (isset($block['markup'])) { + $block['element'] = ['rawHtml' => $block['markup']]; + } elseif (isset($block['hidden'])) { + $block['element'] = []; } - - return $Block; } + + return $block['element']; } - protected function blockCommentContinue($Line, array $Block) + /** + * Continue block code + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockCodeContinue(array $line, array $block) : ?array { - if (isset($Block['closed'])) - { - return; - } - - $Block['element']['rawHtml'] .= "\n" . $Line['body']; - - if (\strpos($Line['text'], '-->') !== false) - { - $Block['closed'] = true; - } - - return $Block; - } - - # - # Fenced Code - - protected function blockFencedCodeBase($Line) - { - $marker = $Line['text'][0]; - - $openerLength = \strspn($Line['text'], $marker); - - if ($openerLength < 3) - { - return; - } - - $infostring = \trim(\substr($Line['text'], $openerLength), "\t "); - - if (\strpos($infostring, '`') !== false) - { - return; - } - - $Element = [ - 'name' => 'code', - 'text' => '', - ]; - - if ($infostring !== '') - { - /** - * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes - * Every HTML element may have a class attribute specified. - * The attribute, if specified, must have a value that is a set - * of space-separated tokens representing the various classes - * that the element belongs to. - * [...] - * The space characters, for the purposes of this specification, - * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), - * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and - * U+000D CARRIAGE RETURN (CR). - */ - $language = \substr($infostring, 0, \strcspn($infostring, " \t\n\f\r")); - - $Element['attributes'] = ['class' => "language-{$language}"]; - } - - return [ - 'char' => $marker, - 'openerLength' => $openerLength, - 'element' => [ - 'name' => 'pre', - 'element' => $Element, - ], - ]; - } - - protected function blockFencedCodeContinue($Line, $Block) - { - if (isset($Block['complete'])) - { - return; - } - - if (isset($Block['interrupted'])) - { - $Block['element']['element']['text'] .= \str_repeat("\n", $Block['interrupted']); - - unset($Block['interrupted']); - } - - if (($len = \strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] - && \rtrim(\substr($Line['text'], $len), ' ') === '' - ) { - $Block['element']['element']['text'] = \substr($Block['element']['element']['text'], 1); - - $Block['complete'] = true; - - return $Block; - } - - $Block['element']['element']['text'] .= "\n" . $Line['body']; - - return $Block; - } - - protected function blockFencedCodeComplete($Block) - { - return $Block; - } - - # - # Header - - protected function blockHeaderParent($Line) - { - $level = \strspn($Line['text'], '#'); - - if ($level > 6) - { - return; - } - - $text = \trim($Line['text'], '#'); - - if ($this->strictMode && isset($text[0]) && $text[0] !== ' ') - { - return; - } - - $text = \trim($text, ' '); - - return [ - 'element' => [ - 'name' => 'h' . $level, - 'handler' => [ - 'function' => 'lineElements', - 'argument' => $text, - 'destination' => 'elements', - ], - ], - ]; - } - - # - # List - - protected function blockListBase($Line, array $CurrentBlock = null) - { - list($name, $pattern) = $Line['text'][0] <= '-' ? ['ul', '[*+-]'] : ['ol', '[0-9]{1,9}+[.\)]']; - - if (\preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) - { - $contentIndent = \strlen($matches[2]); - - if ($contentIndent >= 5) - { - --$contentIndent; - $matches[1] = \substr($matches[1], 0, -$contentIndent); - $matches[3] = \str_repeat(' ', $contentIndent) . $matches[3]; - } - elseif ($contentIndent === 0) - { - $matches[1] .= ' '; - } - - $markerWithoutWhitespace = \strstr($matches[1], ' ', true); - - $Block = [ - 'indent' => $Line['indent'], - 'pattern' => $pattern, - 'data' => [ - 'type' => $name, - 'marker' => $matches[1], - 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : \substr($markerWithoutWhitespace, -1)), - ], - 'element' => [ - 'name' => $name, - 'elements' => [], - ], - ]; - $Block['data']['markerTypeRegex'] = \preg_quote($Block['data']['markerType'], '/'); - - if ($name === 'ol') - { - $listStart = \ltrim(\strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; - - if ($listStart !== '1') - { - if ( - isset($CurrentBlock) - && $CurrentBlock['type'] === 'Paragraph' - && !isset($CurrentBlock['interrupted']) - ) { - return; - } - - $Block['element']['attributes'] = ['start' => $listStart]; - } - } - - $Block['li'] = [ - 'name' => 'li', - 'handler' => [ - 'function' => 'li', - 'argument' => empty($matches[3]) ? [] : [$matches[3]], - 'destination' => 'elements', - ], - ]; - - $Block['element']['elements'] []= & $Block['li']; - - return $Block; - } - } - - protected function blockListContinue($Line, array $Block) - { - if (isset($Block['interrupted']) && empty($Block['li']['handler']['argument'])) - { + if ($line['indent'] < 4) { return null; } - $requiredIndent = ($Block['indent'] + \strlen($Block['data']['marker'])); + if (isset($block['interrupted'])) { + $block['element']['element']['text'] .= \str_repeat("\n", $block['interrupted']); - if ($Line['indent'] < $requiredIndent - && ( - ( - $Block['data']['type'] === 'ol' - && \preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) - ) || ( - $Block['data']['type'] === 'ul' - && \preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) - ) - ) + unset($block['interrupted']); + } + + $block['element']['element']['text'] .= "\n"; + + $text = \substr($line['body'], 4); + + $block['element']['element']['text'] .= $text; + + return $block; + } + + /** + * Complete block code + * + * @param array $block Current block + * + * @return array + * + * @since 1.0.0 + */ + protected function blockCodeComplete(array $block) : array + { + return $block; + } + + /** + * Continue block code + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockCommentContinue(array $line, array $block) : ?array + { + if (isset($block['closed'])) { + return null; + } + + $block['element']['rawHtml'] .= "\n" . $line['body']; + + if (\strpos($line['text'], '-->') !== false) { + $block['closed'] = true; + } + + return $block; + } + + /** + * Continue block fenced code + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockFencedCodeContinue(array $line, array $block) : ?array + { + if (isset($block['complete'])) { + return null; + } + + if (isset($block['interrupted'])) { + $block['element']['element']['text'] .= \str_repeat("\n", $block['interrupted']); + + unset($block['interrupted']); + } + + if (($len = \strspn($line['text'], $block['char'])) >= $block['openerLength'] + && \rtrim(\substr($line['text'], $len), ' ') === '' ) { - if (isset($Block['interrupted'])) - { - $Block['li']['handler']['argument'] []= ''; + $block['element']['element']['text'] = \substr($block['element']['element']['text'], 1); - $Block['loose'] = true; + $block['complete'] = true; - unset($Block['interrupted']); + return $block; + } + + $block['element']['element']['text'] .= "\n" . $line['body']; + + return $block; + } + + /** + * Complete block fenced code + * + * @param array $block Current block + * + * @return array + * + * @since 1.0.0 + */ + protected function blockFencedCodeComplete(array $block) : array + { + return $block; + } + + /** + * Continue block spoiler + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockSpoilerContinue(array $line, array $block) : ?array + { + if (isset($block['complete'])) { + return null; + } + + if (isset($block['interrupted'])) { + $block['element']['element']['text'] .= \str_repeat("\n", $block['interrupted']); + + unset($block['interrupted']); + } + + if (($len = \strspn($line['text'], $block['char'])) >= $block['openerLength'] + && \rtrim(\substr($line['text'], $len), ' ') === '' + ) { + $block['element']['element']['text'] = \substr($block['element']['element']['text'], 1); + + $block['complete'] = true; + + return $block; + } + + $block['element']['element']['elements'][1]['text'] .= "\n" . $line['body']; + + return $block; + } + + /** + * Complete block spoiler + * + * @param array $block Current block + * + * @return array + * + * @since 1.0.0 + */ + protected function blockSpoilerComplete(array $block) : array + { + return $block; + } + + /** + * Continue block list + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockListContinue(array $line, array $block) : ?array + { + if (isset($block['interrupted']) && empty($block['li']['handler']['argument'])) { + return null; + } + + $requiredIndent = ($block['indent'] + \strlen($block['data']['marker'])); + + if ($line['indent'] < $requiredIndent + && (($block['data']['type'] === 'ol' + && \preg_match('/^[0-9]++' . $block['data']['markerTypeRegex'] . '(?:[ ]++(.*)|$)/', $line['text'], $matches) + ) || ($block['data']['type'] === 'ul' + && \preg_match('/^' . $block['data']['markerTypeRegex'] . '(?:[ ]++(.*)|$)/', $line['text'], $matches) + )) + ) { + if (isset($block['interrupted'])) { + $block['li']['handler']['argument'][] = ''; + + $block['loose'] = true; + + unset($block['interrupted']); } - unset($Block['li']); + unset($block['li']); - $text = isset($matches[1]) ? $matches[1] : ''; + $text = $matches[1] ?? ''; - $Block['indent'] = $Line['indent']; + $block['indent'] = $line['indent']; - $Block['li'] = [ + $block['li'] = [ 'name' => 'li', 'handler' => [ 'function' => 'li', @@ -3168,533 +4139,192 @@ class Markdown ], ]; - $Block['element']['elements'] []= & $Block['li']; + $block['element']['elements'][] = &$block['li']; - return $Block; - } - elseif ($Line['indent'] < $requiredIndent && $this->blockList($Line)) - { + return $block; + } elseif ($line['indent'] < $requiredIndent && $this->blockList($line)) { return null; } - if ($Line['text'][0] === '[' && $this->blockReference($Line)) - { - return $Block; + if ($line['text'][0] === '[' && $this->blockReference($line)) { + return $block; } - if ($Line['indent'] >= $requiredIndent) - { - if (isset($Block['interrupted'])) - { - $Block['li']['handler']['argument'] []= ''; + if ($line['indent'] >= $requiredIndent) { + if (isset($block['interrupted'])) { + $block['li']['handler']['argument'][] = ''; - $Block['loose'] = true; + $block['loose'] = true; - unset($Block['interrupted']); + unset($block['interrupted']); } - $text = \substr($Line['body'], $requiredIndent); + $text = \substr($line['body'], $requiredIndent); - $Block['li']['handler']['argument'] []= $text; + $block['li']['handler']['argument'][] = $text; - return $Block; + return $block; } - if (!isset($Block['interrupted'])) - { - $text = \preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + if (!isset($block['interrupted'])) { + $text = \preg_replace('/^[ ]{0,' . $requiredIndent . '}+/', '', $line['body']); - $Block['li']['handler']['argument'] []= $text; + $block['li']['handler']['argument'][] = $text; - return $Block; + return $block; } + + return null; } - protected function blockListComplete(array $Block) + /** + * Complete block list + * + * @param array $block Current block + * + * @return array + * + * @since 1.0.0 + */ + protected function blockListComplete(array $block) : array { - if (isset($Block['loose'])) - { - foreach ($Block['element']['elements'] as &$li) - { - if (\end($li['handler']['argument']) !== '') - { - $li['handler']['argument'] []= ''; - } + if (!isset($block['loose'])) { + return $block; + } + + foreach ($block['element']['elements'] as &$li) { + if (\end($li['handler']['argument']) !== '') { + $li['handler']['argument'][] = ''; } } - return $Block; + return $block; } - # - # Quote - - protected function blockQuoteBase($Line) + /** + * Continue block quote + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockQuoteContinue(array $line, array $block) : ?array { - if (\preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) - { - return [ - 'element' => [ - 'name' => 'blockquote', - 'handler' => [ - 'function' => 'linesElements', - 'argument' => (array) $matches[1], - 'destination' => 'elements', - ], - ], - ]; + if (isset($block['interrupted'])) { + return null; } + + if ($line['text'][0] === '>' && \preg_match('/^>[ ]?+(.*+)/', $line['text'], $matches)) { + $block['element']['handler']['argument'][] = $matches[1]; + + return $block; + } + + if (!isset($block['interrupted'])) { + $block['element']['handler']['argument'][] = $line['text']; + + return $block; + } + + return null; } - protected function blockQuoteContinue($Line, array $Block) + /** + * Continue block table + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function blockTableContinue(array $line, array $block) : ?array { - if (isset($Block['interrupted'])) - { - return; - } - - if ($Line['text'][0] === '>' && \preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) - { - $Block['element']['handler']['argument'] []= $matches[1]; - - return $Block; - } - - if (!isset($Block['interrupted'])) - { - $Block['element']['handler']['argument'] []= $Line['text']; - - return $Block; - } - } - - # - # Rule - - protected function blockRuleBase($Line) - { - $marker = $Line['text'][0]; - - if (\substr_count($Line['text'], $marker) >= 3 && \rtrim($Line['text'], " {$marker}") === '') - { - return [ - 'element' => [ - 'name' => 'hr', - ], - ]; - } - } - - # - # Setext - - protected function blockSetextHeaderParent($Line, array $Block = null) - { - if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted'])) - { - return; - } - - if ($Line['indent'] < 4 && \rtrim(\rtrim($Line['text'], ' '), $Line['text'][0]) === '') - { - $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; - - return $Block; - } - } - - # - # Reference - - protected function blockReferenceBase($Line) - { - if (\strpos($Line['text'], ']') !== false - && \preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + if (isset($block['interrupted']) + || (\count($block['alignments']) !== 1 && $line['text'][0] !== '|' && \strpos($line['text'], '|') === false) ) { - $id = \strtolower($matches[1]); - - $Data = [ - 'url' => UriFactory::build($matches[2]), - 'title' => isset($matches[3]) ? $matches[3] : null, - ]; - - $this->DefinitionData['Reference'][$id] = $Data; - - return [ - 'element' => [], - ]; - } - } - - # - # Table - - protected function blockTableBase($Line, array $Block = null) - { - if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted'])) - { - return; + return null; } - if ( - \strpos($Block['element']['handler']['argument'], '|') === false - && \strpos($Line['text'], '|') === false - && \strpos($Line['text'], ':') === false - || \strpos($Block['element']['handler']['argument'], "\n") !== false - ) { - return; - } + $elements = []; - if (\rtrim($Line['text'], ' -:|') !== '') - { - return; - } + $row = $line['text']; - $alignments = []; + $row = \trim($row); + $row = \trim($row, '|'); - $divider = $Line['text']; + \preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); - $divider = \trim($divider); - $divider = \trim($divider, '|'); + $cells = \array_slice($matches[0], 0, \count($block['alignments'])); - $dividerCells = \explode('|', $divider); + foreach ($cells as $index => $cell) { + $cell = \trim($cell); - foreach ($dividerCells as $dividerCell) - { - $dividerCell = \trim($dividerCell); - - if ($dividerCell === '') - { - return; - } - - $alignment = null; - - if ($dividerCell[0] === ':') - { - $alignment = 'left'; - } - - if (\substr($dividerCell, - 1) === ':') - { - $alignment = $alignment === 'left' ? 'center' : 'right'; - } - - $alignments []= $alignment; - } - - # ~ - - $HeaderElements = []; - - $header = $Block['element']['handler']['argument']; - - $header = \trim($header); - $header = \trim($header, '|'); - - $headerCells = \explode('|', $header); - - if (\count($headerCells) !== \count($alignments)) - { - return; - } - - foreach ($headerCells as $index => $headerCell) - { - $headerCell = \trim($headerCell); - - $HeaderElement = [ - 'name' => 'th', + $element = [ + 'name' => 'td', 'handler' => [ 'function' => 'lineElements', - 'argument' => $headerCell, + 'argument' => $cell, 'destination' => 'elements', ], ]; - if (isset($alignments[$index])) - { - $alignment = $alignments[$index]; - - $HeaderElement['attributes'] = [ - 'style' => "text-align: {$alignment};", + if (isset($block['alignments'][$index])) { + $element['attributes'] = [ + 'style' => 'text-align: ' . $block['alignments'][$index] . ';', ]; } - $HeaderElements []= $HeaderElement; + $elements [] = $element; } - # ~ - - $Block = [ - 'alignments' => $alignments, - 'identified' => true, - 'element' => [ - 'name' => 'table', - 'elements' => [], - ], - ]; - - $Block['element']['elements'] []= [ - 'name' => 'thead', - ]; - - $Block['element']['elements'] []= [ - 'name' => 'tbody', - 'elements' => [], - ]; - - $Block['element']['elements'][0]['elements'] []= [ + $element = [ 'name' => 'tr', - 'elements' => $HeaderElements, + 'elements' => $elements, ]; - return $Block; + $block['element']['elements'][1]['elements'][] = $element; + + return $block; } - protected function blockTableContinue($Line, array $Block) + /** + * Continue block paragraph + * + * @param array{body:string, indent:int, text:string} $line Line data + * @param array $block Current block + * + * @return null|array + * + * @since 1.0.0 + */ + protected function paragraphContinue(array $line, array $block) : ?array { - if (isset($Block['interrupted'])) - { - return; + if (isset($block['interrupted'])) { + return null; } - if (\count($Block['alignments']) === 1 || $Line['text'][0] === '|' || \strpos($Line['text'], '|')) - { - $Elements = []; + $block['element']['handler']['argument'] .= "\n".$line['text']; - $row = $Line['text']; - - $row = \trim($row); - $row = \trim($row, '|'); - - \preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); - - $cells = \array_slice($matches[0], 0, \count($Block['alignments'])); - - foreach ($cells as $index => $cell) - { - $cell = \trim($cell); - - $Element = [ - 'name' => 'td', - 'handler' => [ - 'function' => 'lineElements', - 'argument' => $cell, - 'destination' => 'elements', - ], - ]; - - if (isset($Block['alignments'][$index])) - { - $Element['attributes'] = [ - 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', - ]; - } - - $Elements []= $Element; - } - - $Element = [ - 'name' => 'tr', - 'elements' => $Elements, - ]; - - $Block['element']['elements'][1]['elements'] []= $Element; - - return $Block; - } + return $block; } - # - # ~ - # - - protected function paragraph($Line) + /** + * Handle link + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array + * + * @since 1.0.0 + */ + protected function inlineLinkParent(array $excerpt) : ?array { - return [ - 'type' => 'Paragraph', - 'element' => [ - 'name' => 'p', - 'handler' => [ - 'function' => 'lineElements', - 'argument' => $Line['text'], - 'destination' => 'elements', - ], - ], - ]; - } - - protected function paragraphContinue($Line, array $Block) - { - if (isset($Block['interrupted'])) - { - return; - } - - $Block['element']['handler']['argument'] .= "\n".$Line['text']; - - return $Block; - } - - # - # Inline Elements - # - - protected $InlineTypes = [ - '!' => ['Image'], - '&' => ['SpecialCharacter'], - '*' => ['Emphasis'], - ':' => ['Url'], - '<' => ['UrlTag', 'EmailTag', 'Markup'], - '[' => ['FootnoteMarker', 'Link'], - '_' => ['Emphasis'], - '`' => ['Code'], - '~' => ['Strikethrough'], - '\\' => ['EscapeSequence'], - ]; - - # ~ - - protected $inlineMarkerList = '!*_&[:<`~\\'; - - # - # ~ - # - - public function line($text, $nonNestables = []) - { - return $this->elements($this->lineElements($text, $nonNestables)); - } - - /* - protected function lineElements($text, $nonNestables = array()) - { - # standardize line breaks - $text = str_replace(array("\r\n", "\r"), "\n", $text); - - $Elements = array(); - - $nonNestables = (empty($nonNestables) - ? array() - : array_combine($nonNestables, $nonNestables) - ); - - # $excerpt is based on the first occurrence of a marker - - while ($excerpt = strpbrk($text, $this->inlineMarkerList)) - { - $marker = $excerpt[0]; - - $markerPosition = strlen($text) - strlen($excerpt); - - $Excerpt = array('text' => $excerpt, 'context' => $text); - - foreach ($this->InlineTypes[$marker] as $inlineType) - { - # check to see if the current inline type is nestable in the current context - - if (isset($nonNestables[$inlineType])) - { - continue; - } - - $Inline = $this->{"inline$inlineType"}($Excerpt); - - if ( !isset($Inline)) - { - continue; - } - - # makes sure that the inline belongs to "our" marker - - if (isset($Inline['position']) and $Inline['position'] > $markerPosition) - { - continue; - } - - # sets a default inline position - - if ( !isset($Inline['position'])) - { - $Inline['position'] = $markerPosition; - } - - # cause the new element to 'inherit' our non nestables - - - $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) - ? array_merge($Inline['element']['nonNestables'], $nonNestables) - : $nonNestables - ; - - # the text that comes before the inline - $unmarkedText = substr($text, 0, $Inline['position']); - - # compile the unmarked text - $InlineText = $this->inlineText($unmarkedText); - $Elements[] = $InlineText['element']; - - # compile the inline - $Elements[] = $this->extractElement($Inline); - - # remove the examined text - $text = substr($text, $Inline['position'] + $Inline['extent']); - - continue 2; - } - - # the marker does not belong to an inline - - $unmarkedText = substr($text, 0, $markerPosition + 1); - - $InlineText = $this->inlineText($unmarkedText); - $Elements[] = $InlineText['element']; - - $text = substr($text, $markerPosition + 1); - } - - $InlineText = $this->inlineText($text); - $Elements[] = $InlineText['element']; - - foreach ($Elements as &$Element) - { - if ( !isset($Element['autobreak'])) - { - $Element['autobreak'] = false; - } - } - - return $Elements; - } - */ - - # - # ~ - # - - protected function inlineTextParent($text) - { - $Inline = [ - 'extent' => \strlen($text), - 'element' => [], - ]; - - $Inline['element']['elements'] = self::pregReplaceElements( - $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', - [ - ['name' => 'br'], - ['text' => "\n"], - ], - $text - ); - - return $Inline; - } - - protected function inlineLinkParent($Excerpt) - { - $Element = [ + $element = [ 'name' => 'a', 'handler' => [ 'function' => 'lineElements', @@ -3708,329 +4338,315 @@ class Markdown ], ]; - $extent = 0; + $extent = 0; + $remainder = $excerpt['text']; - $remainder = $Excerpt['text']; - - if (\preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) - { - $Element['handler']['argument'] = $matches[1]; + if (\preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) { + $element['handler']['argument'] = $matches[1]; $extent += \strlen($matches[0]); $remainder = \substr($remainder, $extent); - } - else - { - return; + } else { + return null; } - if (\preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) - { - $Element['attributes']['href'] = UriFactory::build($matches[1]); + if (\preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) { + $element['attributes']['href'] = UriFactory::build($matches[1]); - if (isset($matches[2])) - { - $Element['attributes']['title'] = \substr($matches[2], 1, - 1); + if (isset($matches[2])) { + $element['attributes']['title'] = \substr($matches[2], 1, - 1); } $extent += \strlen($matches[0]); - } - else - { - if (\preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) - { - $definition = \strlen($matches[1]) !== 0 ? $matches[1] : $Element['handler']['argument']; + } else { + if (\preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) { + $definition = \strlen($matches[1]) !== 0 ? $matches[1] : $element['handler']['argument']; $definition = \strtolower($definition); $extent += \strlen($matches[0]); - } - else - { - $definition = \strtolower($Element['handler']['argument']); + } else { + $definition = \strtolower($element['handler']['argument']); } - if (!isset($this->DefinitionData['Reference'][$definition])) - { - return; + if (!isset($this->definitionData['Reference'][$definition])) { + return null; } - $Definition = $this->DefinitionData['Reference'][$definition]; + $definition = $this->definitionData['Reference'][$definition]; - $Element['attributes']['href'] = $Definition['url']; - $Element['attributes']['title'] = $Definition['title']; + $element['attributes']['href'] = $definition['url']; + $element['attributes']['title'] = $definition['title']; } return [ 'extent' => $extent, - 'element' => $Element, + 'element' => $element, ]; } - protected function inlineSpecialCharacter($Excerpt) + /** + * Handle special character + * + * @param array{text:string, context:string, before:string} $excerpt Inline data + * + * @return null|array + * + * @since 1.0.0 + */ + protected function inlineSpecialCharacter(array $excerpt) : ?array { - if (\substr($Excerpt['text'], 1, 1) !== ' ' && \strpos($Excerpt['text'], ';') !== false - && \preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + if (\substr($excerpt['text'], 1, 1) !== ' ' && \strpos($excerpt['text'], ';') !== false + && \preg_match('/^&(#?+[0-9a-zA-Z]++);/', $excerpt['text'], $matches) ) { return [ 'element' => ['rawHtml' => '&' . $matches[1] . ';'], 'extent' => \strlen($matches[0]), ]; } + + return null; } - # ~ - - protected function unmarkedText($text) + /** + * Handle "handler" + * + * @param array $element Element to handle + * + * @return array + * + * @since 1.0.0 + */ + protected function handle(array $element) : array { - $Inline = $this->inlineText($text); - return $this->element($Inline['element']); + if (!isset($element['handler'])) { + return $element; + } + + if (!isset($element['nonNestables'])) { + $element['nonNestables'] = []; + } + + if (\is_string($element['handler'])) { + $function = $element['handler']; + $argument = $element['text']; + unset($element['text']); + $destination = 'rawHtml'; + } else { + $function = $element['handler']['function']; + $argument = $element['handler']['argument']; + $destination = $element['handler']['destination']; + } + + $element[$destination] = $this->{$function}($argument, $element['nonNestables']); + + if ($destination === 'handler') { + $element = $this->handle($element); + } + + unset($element['handler']); + + return $element; } - # - # Handlers - # - - protected function handle(array $Element) + /** + * Handle element recursively + * + * @param string|\Closure $closure Closure for handling element + * @param array $element Element to handle + * + * @return array + * + * @since 1.0.0 + */ + protected function elementApplyRecursive(string|\Closure $closure, array $element) : array { - if (isset($Element['handler'])) - { - if (!isset($Element['nonNestables'])) - { - $Element['nonNestables'] = []; + $element = \is_string($closure) ? $this->{$closure}($element) : $closure($element); + + if (isset($element['elements'])) { + foreach ($element['elements'] as &$e) { + $e = $this->elementApplyRecursive($closure, $e); } + } elseif (isset($element['element'])) { + $element['element'] = $this->elementApplyRecursive($closure, $element['element']); + } - if (\is_string($Element['handler'])) - { - $function = $Element['handler']; - $argument = $Element['text']; - unset($Element['text']); - $destination = 'rawHtml'; + return $element; + } + + /** + * Handle element recursively + * + * @param string|\Closure $closure Closure for handling element + * @param array $element Element to handle + * + * @return array + * + * @since 1.0.0 + */ + protected function elementApplyRecursiveDepthFirst(string|\Closure $closure, array $element) : array + { + if (isset($element['elements'])) { + foreach ($element['elements'] as &$e) { + $e = $this->elementApplyRecursiveDepthFirst($closure, $e); } - else - { - $function = $Element['handler']['function']; - $argument = $Element['handler']['argument']; - $destination = $Element['handler']['destination']; + } elseif (isset($element['element'])) { + foreach ($element['element'] as &$e) { + $e = $this->elementApplyRecursiveDepthFirst($closure, $e); } - - $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); - - if ($destination === 'handler') - { - $Element = $this->handle($Element); - } - - unset($Element['handler']); } - return $Element; + return \is_string($closure) ? $this->{$closure}($element) : $closure($element); } - protected function handleElementRecursive(array $Element) + /** + * Render element + * + * @param array $element Element to render + * + * @return : string + * + * @since 1.0.0 + */ + protected function element(array $element) : string { - return $this->elementApplyRecursive([$this, 'handle'], $Element); - } - - protected function handleElementsRecursive(array $Elements) - { - return $this->elementsApplyRecursive([$this, 'handle'], $Elements); - } - - protected function elementApplyRecursive($closure, array $Element) - { - $Element = $closure($Element); - - if (isset($Element['elements'])) - { - $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); - } - elseif (isset($Element['element'])) - { - $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + if ($this->safeMode) { + $element = $this->sanitizeElement($element); } - return $Element; - } + // identity map if element has no handler + $element = $this->handle($element); + $hasName = isset($element['name']); + $markup = ''; - protected function elementApplyRecursiveDepthFirst($closure, array $Element) - { - if (isset($Element['elements'])) - { - $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); - } - elseif (isset($Element['element'])) - { - $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); - } + if ($hasName) { + $markup .= '<' . $element['name']; - return $closure($Element); - } - - protected function elementsApplyRecursive($closure, array $Elements) - { - foreach ($Elements as &$Element) - { - $Element = $this->elementApplyRecursive($closure, $Element); - } - - return $Elements; - } - - protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) - { - foreach ($Elements as &$Element) - { - $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); - } - - return $Elements; - } - - protected function element(array $Element) - { - if ($this->safeMode) - { - $Element = $this->sanitiseElement($Element); - } - - # identity map if element has no handler - $Element = $this->handle($Element); - - $hasName = isset($Element['name']); - - $markup = ''; - - if ($hasName) - { - $markup .= '<' . $Element['name']; - - if (isset($Element['attributes'])) - { - foreach ($Element['attributes'] as $name => $value) - { - if ($value === null) - { + if (isset($element['attributes'])) { + foreach ($element['attributes'] as $name => $value) { + if ($value === null) { continue; } - $markup .= " {$name}=\"".self::escape($value).'"'; + $markup .= ' ' . $name . '="' . \htmlspecialchars((string) $value, \ENT_QUOTES, 'UTF-8') . '"'; } } } $permitRawHtml = false; - if (isset($Element['text'])) - { - $text = $Element['text']; - } - // very strongly consider an alternative if you're writing an - // extension - elseif (isset($Element['rawHtml'])) - { - $text = $Element['rawHtml']; + if (isset($element['text'])) { + $text = $element['text']; + } elseif (isset($element['rawHtml'])) { + // very strongly consider an alternative if you're writing an extension + $text = $element['rawHtml']; - $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; - $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + $permitRawHtml = !$this->safeMode || ($element['allowRawHtmlInSafeMode'] ?? false); } - $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + $hasContent = isset($text) || isset($element['element']) || isset($element['elements']); - if ($hasContent) - { + if ($hasContent) { $markup .= $hasName ? '>' : ''; - if (isset($Element['elements'])) - { - $markup .= $this->elements($Element['elements']); - } - elseif (isset($Element['element'])) - { - $markup .= $this->element($Element['element']); + if (isset($element['elements'])) { + $markup .= $this->elements($element['elements']); + } elseif (isset($element['element'])) { + $markup .= $this->element($element['element']); } elseif (!$permitRawHtml) { - $markup .= self::escape((string) $text, true); - } - else - { + $markup .= \htmlspecialchars((string) $text, \ENT_NOQUOTES, 'UTF-8'); + } else { $markup .= $text; } - $markup .= $hasName ? '' : ''; - } - elseif ($hasName) - { + $markup .= $hasName ? '' : ''; + } elseif ($hasName) { $markup .= ' />'; } return $markup; } - protected function elements(array $Elements) : string + /** + * Render elements + * + * @param array $elements Elements to render + * + * @return string + * + * @since 1.0.0 + */ + protected function elements(array $elements) : string { - $markup = ''; - + $markup = ''; $autoBreak = true; - foreach ($Elements as $Element) - { - if (empty($Element)) - { + foreach ($elements as $element) { + if (empty($element)) { continue; } - $autoBreakNext = (isset($Element['autobreak']) - ? $Element['autobreak'] : isset($Element['name']) + $autoBreakNext = (isset($element['autobreak']) + ? $element['autobreak'] : isset($element['name']) ); + // (autobreak === false) covers both sides of an element $autoBreak = $autoBreak ? $autoBreakNext : $autoBreak; - $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $markup .= ($autoBreak ? "\n" : '') . $this->element($element); $autoBreak = $autoBreakNext; } return $markup . ($autoBreak ? "\n" : ''); } - # ~ - - protected function li($lines) + /** + * Handle list + * + * @param string[] $lines Lines + * + * @return array + * + * @since 1.0.0 + */ + protected function li(array $lines) : array { - $Elements = $this->linesElements($lines); + $elements = $this->linesElements($lines); - if (! \in_array('', $lines) - && isset($Elements[0], $Elements[0]['name']) - && $Elements[0]['name'] === 'p' + if (!\in_array('', $lines) + && isset($elements[0], $elements[0]['name']) + && $elements[0]['name'] === 'p' ) { - unset($Elements[0]['name']); + unset($elements[0]['name']); } - return $Elements; + return $elements; } - # - # AST Convenience - # - /** - * Replace occurrences $regexp with $Elements in $text. Return an array of - * elements representing the replacement. + * Replace occurrences $regexp with $elements in $text. + * + * @param string $regexp Regex + * @param array $elements Elements to replace + * @param string $text Text to match against regex + * + * @return array + * + * @since 1.0.0 */ - protected static function pregReplaceElements($regexp, $Elements, $text) + protected static function pregReplaceElements(string $regexp, array $elements, string $text) : array { $newElements = []; - while (\preg_match($regexp, $text, $matches, \PREG_OFFSET_CAPTURE)) - { - $offset = $matches[0][1]; + while (\preg_match($regexp, $text, $matches, \PREG_OFFSET_CAPTURE)) { + $offset = (int) $matches[0][1]; $before = \substr($text, 0, $offset); $after = \substr($text, $offset + \strlen($matches[0][0])); $newElements[] = ['text' => $before]; - foreach ($Elements as $Element) - { - $newElements[] = $Element; + foreach ($elements as $element) { + $newElements[] = $element; } $text = $after; @@ -4041,165 +4657,68 @@ class Markdown return $newElements; } - # - # Deprecated Methods - # - - public static function parse($text) + /** + * Sanitize element + * + * @param array $element Element to sanitize + * + * @return array + * + * @since 1.0.0 + */ + protected function sanitizeElement(array $element) : array { - $parsedown = new self(); - - return $parsedown->text($text); - } - - protected function sanitiseElement(array $Element) - { - static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; - static $safeUrlNameToAtt = [ + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = [ 'a' => 'href', 'img' => 'src', ]; - if (!isset($Element['name'])) - { - unset($Element['attributes']); - return $Element; + if (!isset($element['name'])) { + unset($element['attributes']); + + return $element; } - if (isset($safeUrlNameToAtt[$Element['name']])) - { - $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + if (isset($safeUrlNameToAtt[$element['name']])) { + $element = $this->filterUnsafeUrlInAttribute($element, $safeUrlNameToAtt[$element['name']]); } - if (! empty($Element['attributes'])) - { - foreach ($Element['attributes'] as $att => $val) - { - # filter out badly parsed attribute - if (! \preg_match($goodAttribute, $att)) - { - unset($Element['attributes'][$att]); - } - # dump onevent attribute - elseif (self::striAtStart($att, 'on')) - { - unset($Element['attributes'][$att]); + if (!empty($element['attributes'])) { + foreach ($element['attributes'] as $att => $_) { + if (!\preg_match($goodAttribute, $att)) { + // filter out badly parsed attribute + unset($element['attributes'][$att]); + } elseif (\str_starts_with($att, 'on')) { + // dump onevent attribute + unset($element['attributes'][$att]); } } } - return $Element; + return $element; } - protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + /** + * Sanitize url in attribute + * + * @param array $element Element to sanitize + * @param string $attribute Attribute to sanitize + * + * @return array + * + * @since 1.0.0 + */ + protected function filterUnsafeUrlInAttribute(array $element, string $attribute) : array { - foreach ($this->safeLinksWhitelist as $scheme) - { - if (self::striAtStart($Element['attributes'][$attribute], $scheme)) - { - return $Element; + foreach ($this->safeLinksWhitelist as $scheme) { + if (\str_starts_with($element['attributes'][$attribute], $scheme)) { + return $element; } } - $Element['attributes'][$attribute] = \str_replace(':', '%3A', $Element['attributes'][$attribute]); + $element['attributes'][$attribute] = \str_replace(':', '%3A', $element['attributes'][$attribute]); - return $Element; + return $element; } - - # - # Static Methods - # - - protected static function escape(string $text, bool $allowQuotes = false) : string - { - return \htmlspecialchars($text, $allowQuotes ? \ENT_NOQUOTES : \ENT_QUOTES, 'UTF-8'); - } - - protected static function striAtStart($string, $needle) - { - $len = \strlen($needle); - - if ($len > \strlen($string)) - { - return false; - } - else - { - return \strtolower(\substr($string, 0, $len)) === \strtolower($needle); - } - } - - public static function instance($name = 'default') - { - if (isset(self::$instances[$name])) - { - return self::$instances[$name]; - } - - $instance = new static(); - - self::$instances[$name] = $instance; - - return $instance; - } - - private static $instances = []; - - # - # Fields - # - - protected $DefinitionData; - - public const ID_ATTRIBUTE_DEFAULT = 'toc'; - - protected $tagToc = '[toc]'; - - protected $contentsListArray = []; - - protected $contentsListString = ''; - - protected $firstHeadLevel = 0; - - protected $isBlacklistInitialized = false; - - protected $anchorDuplicates = []; - - # - # Read-Only - - protected $specialCharacters = [ - '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '?', '"', "'", '<', - ]; - - protected $StrongRegex = [ - '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', - ]; - - protected $UnderlineRegex = [ - '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', - ]; - - protected $EmRegex = [ - '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', - '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', - ]; - - protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; - - protected $voidElements = [ - 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', - ]; - - protected $textLevelElements = [ - 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', - 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', - 'i', 'rp', 'del', 'code', 'strike', 'marquee', - 'q', 'rt', 'ins', 'font', 'strong', - 's', 'tt', 'kbd', 'mark', - 'u', 'xm', 'sub', 'nobr', - 'sup', 'ruby', - 'var', 'span', - 'wbr', 'time', - ]; } diff --git a/Utils/Parser/Pdf/PdfParser.php b/Utils/Parser/Pdf/PdfParser.php index 6750541d2..8f6876562 100755 --- a/Utils/Parser/Pdf/PdfParser.php +++ b/Utils/Parser/Pdf/PdfParser.php @@ -26,7 +26,7 @@ use phpOMS\Utils\StringUtils; * @link https://jingga.app * @since 1.0.0 */ -class PdfParser +final class PdfParser { /** * PDFToText path. @@ -44,6 +44,30 @@ class PdfParser */ public static string $pdftoppm = '/usr/bin/pdftoppm'; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /** + * Html to string + * + * @param string $path Path + * + * @return string + * + * @since 1.0.0 + */ + public static function parsePdf(string $path, string $output = 'html') : string + { + return self::pdf2text($path); + } + /** * Pdf to text * @@ -63,6 +87,8 @@ class PdfParser return ''; } + // Try to read pdf directly + // Important: not all PDFs are searchable, some behave like an image if (\is_file(self::$pdftotext)) { try { SystemUtils::runProc( @@ -88,6 +114,7 @@ class PdfParser return $text; } + // Couldn't read text from pdf -> transform to image and run OCR on image $out = \tempnam($tmpDir, 'oms_pdf_'); if ($out === false) { return ''; @@ -117,6 +144,7 @@ class PdfParser foreach ($files as $file) { if (!StringUtils::endsWith($file, '.jpg') + && !StringUtils::endsWith($file, '.jpeg') && !StringUtils::endsWith($file, '.png') && !StringUtils::endsWith($file, '.gif') ) { diff --git a/Utils/Parser/Php/ArrayParser.php b/Utils/Parser/Php/ArrayParser.php index 092aa81e6..eb66bef1f 100755 --- a/Utils/Parser/Php/ArrayParser.php +++ b/Utils/Parser/Php/ArrayParser.php @@ -26,8 +26,18 @@ use phpOMS\Contract\SerializableInterface; * @link https://jingga.app * @since 1.0.0 */ -class ArrayParser +final class ArrayParser { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Serializing array (recursively). * diff --git a/Utils/Parser/Presentation/PresentationParser.php b/Utils/Parser/Presentation/PresentationParser.php index 8a75ec8e1..6bc0ed368 100755 --- a/Utils/Parser/Presentation/PresentationParser.php +++ b/Utils/Parser/Presentation/PresentationParser.php @@ -24,8 +24,18 @@ use PhpOffice\PhpPresentation\IOFactory; * @link https://jingga.app * @since 1.0.0 */ -class PresentationParser +final class PresentationParser { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Presentation to string * @@ -45,6 +55,24 @@ class PresentationParser $oTree = new PresentationWriter($presentation); return $oTree->renderHtml(); + } elseif ($output === 'txt') { + $presentation = IOFactory::load($path); + $oTree = new PresentationWriter($presentation); + $html = $oTree->renderHtml(); + + $doc = new \DOMDocument(); + $html = \preg_replace( + ['~~', '~~'], + ['', ''], + $html + ); + + $doc->loadHTMLFile($path); + + $body = $doc->getElementsByTagName('body'); + $node = $body->item(0); + + return empty($node->textContent) ? '' : $node->textContent; } return ''; diff --git a/Utils/Parser/Presentation/PresentationWriter.php b/Utils/Parser/Presentation/PresentationWriter.php index 7feccb979..96c25e2e6 100755 --- a/Utils/Parser/Presentation/PresentationWriter.php +++ b/Utils/Parser/Presentation/PresentationWriter.php @@ -121,20 +121,20 @@ class PresentationWriter */ protected function displayPhpPresentation(PhpPresentation $oPHPPpt) : void { - $this->append('
  • PhpPresentation'); + $this->append('
  • folder_open PhpPresentation'); $this->append('
      '); - $this->append('
    • Info "PhpPresentation"
    • '); + $this->append('
    • info Info "PhpPresentation"
    • '); foreach ($oPHPPpt->getAllSlides() as $oSlide) { - $this->append('
    • Slide'); + $this->append('
    • indeterminate_check_box Slide'); $this->append('
        '); - $this->append('
      • Info "Slide"
      • '); + $this->append('
      • info Info "Slide"
      • '); foreach ($oSlide->getShapeCollection() as $oShape) { if ($oShape instanceof Group) { - $this->append('
      • Shape "Group"'); + $this->append('
      • indeterminate_check_box Shape "Group"'); $this->append('
          '); - // $this->append('
        • Info "Group"
        • '); + // $this->append('
        • info Info "Group"
        • '); foreach ($oShape->getShapeCollection() as $oShapeChild) { $this->displayShape($oShapeChild); } diff --git a/Utils/Parser/Spreadsheet/SpreadsheetParser.php b/Utils/Parser/Spreadsheet/SpreadsheetParser.php index 07847522b..62ae0664d 100755 --- a/Utils/Parser/Spreadsheet/SpreadsheetParser.php +++ b/Utils/Parser/Spreadsheet/SpreadsheetParser.php @@ -25,8 +25,18 @@ use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; * @link https://jingga.app * @since 1.0.0 */ -class SpreadsheetParser +final class SpreadsheetParser { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Spreadsheet to string * @@ -38,9 +48,9 @@ class SpreadsheetParser */ public static function parseSpreadsheet(string $path, string $output = 'json') : string { - if ($output === 'json') { - $spreadsheet = IOFactory::load($path); + $spreadsheet = IOFactory::load($path); + if ($output === 'json') { $sheetCount = $spreadsheet->getSheetCount(); $csv = []; @@ -52,8 +62,6 @@ class SpreadsheetParser return $json === false ? '' : $json; } elseif ($output === 'pdf') { - $spreadsheet = IOFactory::load($path); - $spreadsheet->getActiveSheet()->setShowGridLines(false); $spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); @@ -64,13 +72,31 @@ class SpreadsheetParser return $writer->toPdfString(); } elseif ($output === 'html') { - $spreadsheet = IOFactory::load($path); - IOFactory::registerWriter('custom', \phpOMS\Utils\Parser\Spreadsheet\SpreadsheetWriter::class); /** @var \phpOMS\Utils\Parser\Spreadsheet\SpreadsheetWriter $writer */ $writer = IOFactory::createWriter($spreadsheet, 'custom'); return $writer->generateHtmlAll(); + } elseif ($output === 'txt') { + IOFactory::registerWriter('custom', \phpOMS\Utils\Parser\Spreadsheet\SpreadsheetWriter::class); + + /** @var \phpOMS\Utils\Parser\Spreadsheet\SpreadsheetWriter $writer */ + $writer = IOFactory::createWriter($spreadsheet, 'custom'); + $html = $writer->generateHtmlAll(); + + $doc = new \DOMDocument(); + $html = \preg_replace( + ['~~', '~~'], + ['', ''], + $html + ); + + $doc->loadHTMLFile($path); + + $body = $doc->getElementsByTagName('body'); + $node = $body->item(0); + + return empty($node->textContent) ? '' : $node->textContent; } return ''; diff --git a/Utils/Parser/Xml/XmlParser.php b/Utils/Parser/Xml/XmlParser.php new file mode 100644 index 000000000..c5622d114 --- /dev/null +++ b/Utils/Parser/Xml/XmlParser.php @@ -0,0 +1,83 @@ +preserveWhiteSpace = true; + $doc->formatOutput = true; + + $xml = \file_get_contents($path); + $xml = \preg_replace( + ['~~', '~~'], + ['', ''], + $xml + ); + + $doc->loadXML($path); + + if (empty($xpath)) { + return $doc->loadXML($xml); + } + + $content = ''; + $xNode = new \DOMXpath($doc); + $elements = $xNode->query($xpath); + + if ($elements === false) { + return $content; + } + + foreach ($elements as $element) { + $nodes = $element->childNodes; + + foreach ($nodes as $node) { + $content .= $node->textContent . "\n"; + } + } + + return $content; + } +} diff --git a/Utils/Permutation.php b/Utils/Permutation.php index 6f4bc3532..d2f62106e 100755 --- a/Utils/Permutation.php +++ b/Utils/Permutation.php @@ -37,14 +37,14 @@ final class Permutation /** * Create all permutations. * - * @param array $toPermute data to permutate + * @param array $toPermute data to permute * @param array $result existing permutations * * @return array * * @since 1.0.0 */ - public static function permut(array $toPermute, array $result = [], bool $concat = true) : array + public static function permuteAll(array $toPermute, array $result = [], bool $concat = true) : array { $permutations = []; @@ -58,7 +58,7 @@ final class Permutation unset($newArr[$key]); - $permutations = \array_merge($permutations, self::permut($newArr, $newres, $concat)); + $permutations = \array_merge($permutations, self::permuteAll($newArr, $newres, $concat)); } } @@ -98,9 +98,9 @@ final class Permutation } /** - * Permutate based on transposition key. + * Permute based on transposition key. * - * @param array|string $toPermute To permutate + * @param array|string $toPermute To permute * @param int[] $key Permutation keys * * @return string|array @@ -109,7 +109,7 @@ final class Permutation * * @since 1.0.0 */ - public static function permutate(string | array $toPermute, array $key) : string | array + public static function permuteByKey(string | array $toPermute, array $key) : string | array { $length = \is_array($toPermute) ? \count($toPermute) : \strlen($toPermute); diff --git a/Utils/RnG/ArrayRandomize.php b/Utils/RnG/ArrayRandomize.php index adfb5fbda..6619e3b11 100755 --- a/Utils/RnG/ArrayRandomize.php +++ b/Utils/RnG/ArrayRandomize.php @@ -22,8 +22,18 @@ namespace phpOMS\Utils\RnG; * @link https://jingga.app * @since 1.0.0 */ -class ArrayRandomize +final class ArrayRandomize { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Yates array shuffler. * diff --git a/Utils/RnG/DateTime.php b/Utils/RnG/DateTime.php index c288b10ff..704a8e39f 100755 --- a/Utils/RnG/DateTime.php +++ b/Utils/RnG/DateTime.php @@ -22,8 +22,18 @@ namespace phpOMS\Utils\RnG; * @link https://jingga.app * @since 1.0.0 */ -class DateTime +final class DateTime { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get a random \DateTime. * diff --git a/Utils/RnG/Email.php b/Utils/RnG/Email.php index 4ae5ab4cf..50a71dcca 100755 --- a/Utils/RnG/Email.php +++ b/Utils/RnG/Email.php @@ -22,8 +22,18 @@ namespace phpOMS\Utils\RnG; * @link https://jingga.app * @since 1.0.0 */ -class Email +final class Email { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get a random email. * diff --git a/Utils/RnG/File.php b/Utils/RnG/File.php index 22b951659..87288bf29 100755 --- a/Utils/RnG/File.php +++ b/Utils/RnG/File.php @@ -22,7 +22,7 @@ namespace phpOMS\Utils\RnG; * @link https://jingga.app * @since 1.0.0 */ -class File +final class File { /** * Extensions. @@ -44,6 +44,16 @@ class File ['flv'], ['fla'], ['deb'], ['py'], ['pl'], ]; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get a random file extension. * @@ -53,11 +63,9 @@ class File * * @since 1.0.0 */ - public static function generateExtension(array $source = null) : string + public static function generateExtension(?array $source = null) : string { - if ($source === null) { - $source = self::$extensions; - } + $source ??= self::$extensions; $key = \array_rand($source, 1); diff --git a/Utils/RnG/LinearCongruentialGenerator.php b/Utils/RnG/LinearCongruentialGenerator.php index bfc60986e..c10e42258 100755 --- a/Utils/RnG/LinearCongruentialGenerator.php +++ b/Utils/RnG/LinearCongruentialGenerator.php @@ -22,7 +22,7 @@ namespace phpOMS\Utils\RnG; * @link https://jingga.app * @since 1.0.0 */ -class LinearCongruentialGenerator +final class LinearCongruentialGenerator { /** * BSD seed value. @@ -40,6 +40,16 @@ class LinearCongruentialGenerator */ private static $msvcrtSeed = 0; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * BSD random number * diff --git a/Utils/RnG/Name.php b/Utils/RnG/Name.php index f02a9faec..dff549c46 100755 --- a/Utils/RnG/Name.php +++ b/Utils/RnG/Name.php @@ -22,7 +22,7 @@ namespace phpOMS\Utils\RnG; * @link https://jingga.app * @since 1.0.0 */ -class Name +final class Name { private static array $names = [ 'western' => [ @@ -216,7 +216,7 @@ class Name 'Zula', 'Catrina', 'Hazeline', 'Lillian', 'Reanne', 'Zuri', 'Catriona', 'Heather', 'Lillie', 'Rebecca', 'Zyana', 'Cayla', 'Heaven', ], - 'male' => [ + 'male' => [ 'Aaron', 'Clay', 'Gino', 'Laurie', 'Richard', 'Abdul', 'Clayton', 'Giorgio', 'Lawrence', 'Richie', 'Abdullah', 'Clement', 'Giovanni', 'Lawson', 'Rick', 'Abe', 'Cliff', 'Glen', 'Layne', 'Rickey', 'Abel', 'Clifford', 'Glenn', 'Layton', 'Rickie', 'Abraham', 'Clifton', 'Glyndwr', 'Leaf', 'Ricky', 'Abram', @@ -481,6 +481,16 @@ class Name ], ]; + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get a random string. * diff --git a/Utils/RnG/Number.php b/Utils/RnG/Number.php new file mode 100644 index 000000000..60012d14c --- /dev/null +++ b/Utils/RnG/Number.php @@ -0,0 +1,51 @@ + 49, 'us' => 1]; - } + $countries ??= ['de' => 49, 'us' => 1]; $numberString = \str_replace( '$1', diff --git a/Utils/RnG/StringUtils.php b/Utils/RnG/StringUtils.php index 55e67dd6b..1cc6c73d0 100755 --- a/Utils/RnG/StringUtils.php +++ b/Utils/RnG/StringUtils.php @@ -22,8 +22,18 @@ namespace phpOMS\Utils\RnG; * @link https://jingga.app * @since 1.0.0 */ -class StringUtils +final class StringUtils { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get a random string. * diff --git a/Utils/RnG/Text.php b/Utils/RnG/Text.php index 2b8b8a87b..d07cc0236 100755 --- a/Utils/RnG/Text.php +++ b/Utils/RnG/Text.php @@ -22,7 +22,7 @@ namespace phpOMS\Utils\RnG; * @link https://jingga.app * @since 1.0.0 */ -class Text +final class Text { /** * Vocabulary. @@ -97,15 +97,13 @@ class Text * * @since 1.0.0 */ - public function generateText(int $length, array $words = null) : string + public function generateText(int $length, ?array $words = null) : string { if ($length === 0) { return ''; } - if ($words === null) { - $words = self::LOREM_IPSUM; - } + $words ??= self::LOREM_IPSUM; $punctuation = $this->generatePunctuation($length); $punctuationCount = \array_count_values( @@ -249,7 +247,7 @@ class Text $paragraphLength = $length - $i; } - $i += $paragraphLength; + $i += $paragraphLength; $paragraph[] = $i; } diff --git a/Utils/RnG/UUID.php b/Utils/RnG/UUID.php index 3dcf94611..a7661145f 100755 --- a/Utils/RnG/UUID.php +++ b/Utils/RnG/UUID.php @@ -24,6 +24,16 @@ namespace phpOMS\Utils\RnG; */ final class UUID { + /** + * Constructor. + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + /** * Get default random UUID * diff --git a/Utils/StringUtils.php b/Utils/StringUtils.php index 31d23d291..e82295714 100755 --- a/Utils/StringUtils.php +++ b/Utils/StringUtils.php @@ -131,7 +131,7 @@ final class StringUtils } /** - * Count occurences of character at the beginning of a string. + * Count occurrences of character at the beginning of a string. * * @param string $string string to analyze * @param string $character character to count at the beginning of the string @@ -139,7 +139,7 @@ final class StringUtils * @example StringUtils::countCharacterFromStart(' Test string', ' '); // 4 * @example StringUtils::countCharacterFromStart(' Test string', 's'); // 0 * - * @return int the amount of repeating occurences at the beginning of the string + * @return int the amount of repeating occurrences at the beginning of the string * * @since 1.0.0 */ @@ -178,7 +178,7 @@ final class StringUtils /** @var int $v */ foreach ($countChars as $v) { - $p = $v / $size; + $p = $v / $size; $entropy -= $p * \log($p) / \log(2); } @@ -265,37 +265,30 @@ final class StringUtils for ($i = 0; $i < $n; ++$i) { $mc = $diff['mask'][$i]; - if ($mc !== 0) { - switch ($mc) { - case -1: - $result .= '' . $diff['values'][$i] . '' . $delim; - break; - case 1: - $result .= '' . $diff['values'][$i] . '' . $delim; - break; - } - } else { - $result .= $diff['values'][$i] . $delim; + $previousMC = $diff['mask'][$i - 1] ?? 0; + $nextMC = $diff['mask'][$i + 1] ?? 0; + + switch ($mc) { + case -1: + $result .= ($previousMC === -1 ? '' : '') + . $diff['values'][$i] + . ($nextMC === -1 ? '' : '') + . $delim; + + break; + case 1: + $result .= ($previousMC === 1 ? '' : '') + . $diff['values'][$i] + . ($nextMC === 1 ? '' : '') + . $delim; + + break; + default: + $result .= $diff['values'][$i] . $delim; } } - $result = \rtrim($result, $delim); - - switch ($mc) { - case -1: - $result .= ''; - break; - case 1: - $result .= ''; - break; - } - - // @todo: This should not be necessary but the algorithm above allows for weird combinations. - return \str_replace( - ['', '', '', '', '', '', ' ', ' '], - ['', '', '', '', '', '', '', ''], - $result - ); + return \rtrim($result, $delim); } /** @@ -389,36 +382,6 @@ final class StringUtils return (int) $res; } - /** - * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. - * - * @param string $string String to check - * - * @return bool - * - * @since 1.0.0 - */ - public static function isShellSafe(string $string) : bool - { - if (\escapeshellcmd($string) !== $string - || !\in_array(\escapeshellarg($string), ["'{$string}'", "\"{$string}\""]) - ) { - return false; - } - - $length = \strlen($string); - - for ($i = 0; $i < $length; ++$i) { - $c = $string[$i]; - - if (!\ctype_alnum($c) && \strpos('@_-.', $c) === false) { - return false; - } - } - - return true; - } - /** * Turn ints into spreadsheet column names * diff --git a/Utils/TaskSchedule/Cron.php b/Utils/TaskSchedule/Cron.php index 13c2ad9b1..e3b98a063 100755 --- a/Utils/TaskSchedule/Cron.php +++ b/Utils/TaskSchedule/Cron.php @@ -206,8 +206,10 @@ class Cron extends SchedulerAbstract $elements = \array_merge($elements, $interval); $elements[] = \trim(\substr($line, $len = (\strlen(\implode(' ', $interval)) + 1), $comment - $len - 1)); - $jobs[] = $job = CronJob::createWith($elements); - $job->setStatus($line[0] === '#' ? TaskStatus::INACTIVE : TaskStatus::ACTIVE); + $job = CronJob::createWith($elements); + $job->status = $line[0] === '#' ? TaskStatus::INACTIVE : TaskStatus::ACTIVE; + + $jobs[] = $job; } $line = \fgets($fp); diff --git a/Utils/TaskSchedule/Interval.php b/Utils/TaskSchedule/Interval.php index c7f5f0e70..775f061f4 100755 --- a/Utils/TaskSchedule/Interval.php +++ b/Utils/TaskSchedule/Interval.php @@ -110,7 +110,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function __construct(\DateTime $start = null, string $interval = null) + public function __construct(?\DateTime $start = null, ?string $interval = null) { $this->start = $start ?? new \DateTime('now'); @@ -225,7 +225,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function setMinute(int $index, int $start = null, int $end = null, int $step = null) : void + public function setMinute(int $index, ?int $start = null, ?int $end = null, ?int $step = null) : void { $this->minute[$index] = [ 'start' => $start, @@ -245,7 +245,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function addMinute(int $start = null, int $end = null, int $step = null) : void + public function addMinute(?int $start = null, ?int $end = null, ?int $step = null) : void { $this->minute[] = [ 'start' => $start, @@ -278,7 +278,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function setHour(int $index, int $start = null, int $end = null, int $step = null) : void + public function setHour(int $index, ?int $start = null, ?int $end = null, ?int $step = null) : void { $this->hour[$index] = [ 'start' => $start, @@ -298,7 +298,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function addHour(int $start = null, int $end = null, int $step = null) : void + public function addHour(?int $start = null, ?int $end = null, ?int $step = null) : void { $this->hour[] = [ 'start' => $start, @@ -331,7 +331,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function setDayOfMonth(int $index, int $start = null, int $end = null, int $step = null) : void + public function setDayOfMonth(int $index, ?int $start = null, ?int $end = null, ?int $step = null) : void { $this->dayOfMonth[$index] = [ 'start' => $start, @@ -351,7 +351,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function addDayOfMonth(int $start = null, int $end = null, int $step = null) : void + public function addDayOfMonth(?int $start = null, ?int $end = null, ?int $step = null) : void { $this->dayOfMonth[] = [ 'start' => $start, @@ -384,7 +384,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function setDayOfWeek(int $index, int $start = null, int $end = null, int $step = null) : void + public function setDayOfWeek(int $index, ?int $start = null, ?int $end = null, ?int $step = null) : void { $this->dayOfWeek[$index] = [ 'start' => $start, @@ -404,7 +404,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function addDayOfWeek(int $start = null, int $end = null, int $step = null) : void + public function addDayOfWeek(?int $start = null, ?int $end = null, ?int $step = null) : void { $this->dayOfWeek[] = [ 'start' => $start, @@ -437,7 +437,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function setMonth(int $index, int $start = null, int $end = null, int $step = null) : void + public function setMonth(int $index, ?int $start = null, ?int $end = null, ?int $step = null) : void { $this->month[$index] = [ 'start' => $start, @@ -457,7 +457,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function addMonth(int $start = null, int $end = null, int $step = null) : void + public function addMonth(?int $start = null, ?int $end = null, ?int $step = null) : void { $this->month[] = [ 'start' => $start, @@ -490,7 +490,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function setYear(int $index, int $start = null, int $end = null, int $step = null) : void + public function setYear(int $index, ?int $start = null, ?int $end = null, ?int $step = null) : void { $this->year[$index] = [ 'start' => $start, @@ -510,7 +510,7 @@ class Interval implements SerializableInterface * * @since 1.0.0 */ - public function addYear(int $start = null, int $end = null, int $step = null) : void + public function addYear(?int $start = null, ?int $end = null, ?int $step = null) : void { $this->year[] = [ 'start' => $start, diff --git a/Utils/TaskSchedule/Schedule.php b/Utils/TaskSchedule/Schedule.php index b00f6b7e5..9991020d8 100755 --- a/Utils/TaskSchedule/Schedule.php +++ b/Utils/TaskSchedule/Schedule.php @@ -23,6 +23,9 @@ use phpOMS\Validation\Base\DateTime; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 + * + * @todo Use `Interval` for scheduler instead of string etc. + * https://github.com/Karaka-Management/phpOMS/issues/257 */ class Schedule extends TaskAbstract { @@ -39,13 +42,9 @@ class Schedule extends TaskAbstract */ public static function createWith(array $jobData) : TaskAbstract { - /** - * @todo Karaka/phpOMS#231 - * Use the interval for generating a schedule - */ $job = new self($jobData[1], $jobData[8], $jobData[7]); - $job->setStatus((int) $jobData[3]); + $job->status = (int) $jobData[3]; if (DateTime::isValid($jobData[2])) { $job->setNextRunTime(new \DateTime($jobData[2])); diff --git a/Utils/TaskSchedule/TaskAbstract.php b/Utils/TaskSchedule/TaskAbstract.php index e1dc82ff2..586720f0b 100755 --- a/Utils/TaskSchedule/TaskAbstract.php +++ b/Utils/TaskSchedule/TaskAbstract.php @@ -54,7 +54,7 @@ abstract class TaskAbstract * @var int * @since 1.0.0 */ - protected int $status = TaskStatus::ACTIVE; + public int $status = TaskStatus::ACTIVE; /** * Next runtime @@ -170,32 +170,6 @@ abstract class TaskAbstract $this->interval = $interval; } - /** - * Get status. - * - * @return int - * - * @since 1.0.0 - */ - public function getStatus() : int - { - return $this->status; - } - - /** - * Set status. - * - * @param int $status Status - * - * @return void - * - * @since 1.0.0 - */ - public function setStatus(int $status) : void - { - $this->status = $status; - } - /** * Get next run time. * diff --git a/Validation/Base/DateTime.php b/Validation/Base/DateTime.php index b0f656058..38ddbcfc9 100755 --- a/Validation/Base/DateTime.php +++ b/Validation/Base/DateTime.php @@ -29,7 +29,7 @@ abstract class DateTime extends ValidatorAbstract /** * {@inheritdoc} */ - public static function isValid(mixed $value, array $constraints = null) : bool + public static function isValid(mixed $value, ?array $constraints = null) : bool { if (!\is_string($value)) { return false; diff --git a/Validation/Base/Json.php b/Validation/Base/Json.php index c0f4d375a..78e285cec 100755 --- a/Validation/Base/Json.php +++ b/Validation/Base/Json.php @@ -30,7 +30,7 @@ abstract class Json extends ValidatorAbstract /** * {@inheritdoc} */ - public static function isValid(mixed $value, array $constraints = null) : bool + public static function isValid(mixed $value, ?array $constraints = null) : bool { if (!\is_string($value)) { return false; diff --git a/Validation/Finance/BIC.php b/Validation/Finance/BIC.php index 986ad0645..d63dc9004 100755 --- a/Validation/Finance/BIC.php +++ b/Validation/Finance/BIC.php @@ -29,7 +29,7 @@ final class BIC extends ValidatorAbstract /** * {@inheritdoc} */ - public static function isValid(mixed $value, array $constraints = null) : bool + public static function isValid(mixed $value, ?array $constraints = null) : bool { if (!\is_string($value)) { return false; diff --git a/Validation/Finance/CreditCard.php b/Validation/Finance/CreditCard.php index 19c5ee050..05a292010 100755 --- a/Validation/Finance/CreditCard.php +++ b/Validation/Finance/CreditCard.php @@ -29,7 +29,7 @@ final class CreditCard extends ValidatorAbstract /** * {@inheritdoc} */ - public static function isValid($value, array $constraints = null) : bool + public static function isValid($value, ?array $constraints = null) : bool { if (!\is_string($value)) { return false; diff --git a/Validation/Finance/EUVat.php b/Validation/Finance/EUVat.php index 1d86aa38b..92e1776e3 100644 --- a/Validation/Finance/EUVat.php +++ b/Validation/Finance/EUVat.php @@ -63,7 +63,7 @@ final class EUVat extends ValidatorAbstract /** * {@inheritdoc} */ - public static function isValid(mixed $value, array $constraints = null) : bool + public static function isValid(mixed $value, ?array $constraints = null) : bool { if (!\is_string($value)) { return false; diff --git a/Validation/Finance/Iban.php b/Validation/Finance/Iban.php index c905bbebf..e30f2b803 100755 --- a/Validation/Finance/Iban.php +++ b/Validation/Finance/Iban.php @@ -29,7 +29,7 @@ final class Iban extends ValidatorAbstract /** * {@inheritdoc} */ - public static function isValid(mixed $value, array $constraints = null) : bool + public static function isValid(mixed $value, ?array $constraints = null) : bool { if (!\is_string($value)) { return false; @@ -42,7 +42,7 @@ final class Iban extends ValidatorAbstract return false; // @codeCoverageIgnore } - $enumName = 'C_' . \strtoupper($temp); + $enumName = '_' . \strtoupper($temp); if (!IbanEnum::isValidName($enumName)) { self::$error = IbanErrorType::INVALID_COUNTRY; @@ -148,14 +148,16 @@ final class Iban extends ValidatorAbstract */ private static function validateChecksum(string $iban) : bool { - $chars = ['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13, 'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17, 'i' => 18, - 'j' => 19, 'k' => 20, 'l' => 21, 'm' => 22, 'n' => 23, 'o' => 24, 'p' => 25, 'q' => 26, 'r' => 27, - 's' => 28, 't' => 29, 'u' => 30, 'v' => 31, 'w' => 32, 'x' => 33, 'y' => 34, 'z' => 35,]; + $chars = [ + 'a' => 10, 'b' => 11, 'c' => 12, 'd' => 13, 'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17, 'i' => 18, + 'j' => 19, 'k' => 20, 'l' => 21, 'm' => 22, 'n' => 23, 'o' => 24, 'p' => 25, 'q' => 26, 'r' => 27, + 's' => 28, 't' => 29, 'u' => 30, 'v' => 31, 'w' => 32, 'x' => 33, 'y' => 34, 'z' => 35, + ]; $moved = \substr($iban, 4) . \substr($iban, 0, 4); $movedArray = (array) \str_split($moved); $new = ''; - foreach ($movedArray as $key => $value) { + foreach ($movedArray as $key => $_) { if (!\is_numeric($movedArray[$key])) { $movedArray[$key] = $chars[$movedArray[$key]]; } diff --git a/Validation/Finance/IbanEnum.php b/Validation/Finance/IbanEnum.php index 50e0bdf16..31c7ec98e 100755 --- a/Validation/Finance/IbanEnum.php +++ b/Validation/Finance/IbanEnum.php @@ -26,167 +26,167 @@ use phpOMS\Stdlib\Base\Enum; */ class IbanEnum extends Enum { - public const C_AL = 'ALkk bbbs sssx cccc cccc cccc cccc'; + public const _AL = 'ALkk bbbs sssx cccc cccc cccc cccc'; - public const C_AD = 'ADkk bbbb ssss cccc cccc cccc'; + public const _AD = 'ADkk bbbb ssss cccc cccc cccc'; - public const C_AT = 'ATkk bbbb bccc cccc cccc'; + public const _AT = 'ATkk bbbb bccc cccc cccc'; - public const C_AZ = 'AZkk bbbb cccc cccc cccc cccc cccc '; + public const _AZ = 'AZkk bbbb cccc cccc cccc cccc cccc '; - public const C_BH = 'BHkk bbbb cccc cccc cccc cc'; + public const _BH = 'BHkk bbbb cccc cccc cccc cc'; - public const C_BE = 'BEkk bbbc cccc ccxx'; + public const _BE = 'BEkk bbbc cccc ccxx'; - public const C_BA = 'BAkk bbbs sscc cccc ccxx'; + public const _BA = 'BAkk bbbs sscc cccc ccxx'; - public const C_BR = 'BRkk bbbb bbbb ssss sccc cccc ccct n'; + public const _BR = 'BRkk bbbb bbbb ssss sccc cccc ccct n'; - public const C_BG = 'BGkk bbbb ssss ttcc cccc cc'; + public const _BG = 'BGkk bbbb ssss ttcc cccc cc'; - public const C_CR = 'CRkk bbbc cccc cccc cccc c'; + public const _CR = 'CRkk bbbc cccc cccc cccc c'; - public const C_HR = 'HRkk bbbb bbbc cccc cccc c'; + public const _HR = 'HRkk bbbb bbbc cccc cccc c'; - public const C_CY = 'CYkk bbbs ssss cccc cccc cccc cccc'; + public const _CY = 'CYkk bbbs ssss cccc cccc cccc cccc'; - public const C_CZ = 'CZkk bbbb ssss sscc cccc cccc'; + public const _CZ = 'CZkk bbbb ssss sscc cccc cccc'; - public const C_DK = 'DKkk bbbb cccc cccc cc'; + public const _DK = 'DKkk bbbb cccc cccc cc'; - public const C_DO = 'DOkk bbbb cccc cccc cccc cccc cccc'; + public const _DO = 'DOkk bbbb cccc cccc cccc cccc cccc'; - public const C_TL = 'TLkk bbbc cccc cccc cccc cxx'; + public const _TL = 'TLkk bbbc cccc cccc cccc cxx'; - public const C_EE = 'EEkk bbss cccc cccc cccx'; + public const _EE = 'EEkk bbss cccc cccc cccx'; - public const C_FO = 'FOkk bbbb cccc cccc cx'; + public const _FO = 'FOkk bbbb cccc cccc cx'; - public const C_FI = 'FIkk bbbb bbcc cccc cx'; + public const _FI = 'FIkk bbbb bbcc cccc cx'; - public const C_FR = 'FRkk bbbb bsss sscc cccc cccc cxx'; + public const _FR = 'FRkk bbbb bsss sscc cccc cccc cxx'; - public const C_GE = 'GEkk bbcc cccc cccc cccc cc'; + public const _GE = 'GEkk bbcc cccc cccc cccc cc'; - public const C_DE = 'DEkk bbbb bbbb cccc cccc cc'; + public const _DE = 'DEkk bbbb bbbb cccc cccc cc'; - public const C_GI = 'GIkk bbbb cccc cccc cccc ccc'; + public const _GI = 'GIkk bbbb cccc cccc cccc ccc'; - public const C_GR = 'GRkk bbbs sssc cccc cccc cccc ccc'; + public const _GR = 'GRkk bbbs sssc cccc cccc cccc ccc'; - public const C_GL = 'GLkk bbbb cccc cccc cc'; + public const _GL = 'GLkk bbbb cccc cccc cc'; - public const C_GT = 'GTkk bbbb mmtt cccc cccc cccc cccc'; + public const _GT = 'GTkk bbbb mmtt cccc cccc cccc cccc'; - public const C_HU = 'HUkk bbbs sssx cccc cccc cccc cccx'; + public const _HU = 'HUkk bbbs sssx cccc cccc cccc cccx'; - public const C_IS = 'ISkk bbbb sscc cccc iiii iiii ii'; + public const _IS = 'ISkk bbbb sscc cccc iiii iiii ii'; - public const C_IE = 'IEkk aaaa bbbb bbcc cccc cc'; + public const _IE = 'IEkk aaaa bbbb bbcc cccc cc'; - public const C_IL = 'ILkk bbbn nncc cccc cccc ccc'; + public const _IL = 'ILkk bbbn nncc cccc cccc ccc'; - public const C_IT = 'ITkk xbbb bbss sssc cccc cccc ccc'; + public const _IT = 'ITkk xbbb bbss sssc cccc cccc ccc'; - public const C_JO = 'JOkk bbbb ssss cccc cccc cccc cccc cc'; + public const _JO = 'JOkk bbbb ssss cccc cccc cccc cccc cc'; - public const C_KZ = 'KZkk bbbc cccc cccc cccc'; + public const _KZ = 'KZkk bbbc cccc cccc cccc'; - public const C_XK = 'XKkk bbbb cccc cccc cccc'; + public const _XK = 'XKkk bbbb cccc cccc cccc'; - public const C_KW = 'KWkk bbbb cccc cccc cccc cccc cccc cc'; + public const _KW = 'KWkk bbbb cccc cccc cccc cccc cccc cc'; - public const C_LV = 'LVkk bbbb cccc cccc cccc c'; + public const _LV = 'LVkk bbbb cccc cccc cccc c'; - public const C_LB = 'LBkk bbbb cccc cccc cccc cccc cccc'; + public const _LB = 'LBkk bbbb cccc cccc cccc cccc cccc'; - public const C_LI = 'LIkk bbbb bccc cccc cccc c'; + public const _LI = 'LIkk bbbb bccc cccc cccc c'; - public const C_LT = 'LTkk bbbb bccc cccc cccc'; + public const _LT = 'LTkk bbbb bccc cccc cccc'; - public const C_LU = 'LUkk bbbc cccc cccc cccc'; + public const _LU = 'LUkk bbbc cccc cccc cccc'; - public const C_MK = 'MKkk bbbc cccc cccc cxx'; + public const _MK = 'MKkk bbbc cccc cccc cxx'; - public const C_MT = 'MTkk bbbb ssss sccc cccc cccc cccc ccc'; + public const _MT = 'MTkk bbbb ssss sccc cccc cccc cccc ccc'; - public const C_MR = 'MRkk bbbb bsss sscc cccc cccc cxx'; + public const _MR = 'MRkk bbbb bsss sscc cccc cccc cxx'; - public const C_MU = 'MUkk bbbb bbss cccc cccc cccc 000m mm'; + public const _MU = 'MUkk bbbb bbss cccc cccc cccc 000m mm'; - public const C_MC = 'MCkk bbbb bsss sscc cccc cccc cxx'; + public const _MC = 'MCkk bbbb bsss sscc cccc cccc cxx'; - public const C_MD = 'MDkk bbcc cccc cccc cccc cccc'; + public const _MD = 'MDkk bbcc cccc cccc cccc cccc'; - public const C_ME = 'MEkk bbbc cccc cccc cccc xx'; + public const _ME = 'MEkk bbbc cccc cccc cccc xx'; - public const C_NL = 'NLkk bbbb cccc cccc cc'; + public const _NL = 'NLkk bbbb cccc cccc cc'; - public const C_NO = 'NOkk bbbb cccc ccx'; + public const _NO = 'NOkk bbbb cccc ccx'; - public const C_PK = 'PKkk bbbb cccc cccc cccc cccc'; + public const _PK = 'PKkk bbbb cccc cccc cccc cccc'; - public const C_PS = 'PSkk bbbb xxxx xxxx xccc cccc cccc c'; + public const _PS = 'PSkk bbbb xxxx xxxx xccc cccc cccc c'; - public const C_PL = 'PLkk bbbs sssx cccc cccc cccc cccc'; + public const _PL = 'PLkk bbbs sssx cccc cccc cccc cccc'; - public const C_PT = 'PTkk bbbb ssss cccc cccc cccx x'; + public const _PT = 'PTkk bbbb ssss cccc cccc cccx x'; - public const C_QA = 'QAkk bbbb cccc cccc cccc cccc cccc c'; + public const _QA = 'QAkk bbbb cccc cccc cccc cccc cccc c'; - public const C_RO = 'ROkk bbbb cccc cccc cccc cccc'; + public const _RO = 'ROkk bbbb cccc cccc cccc cccc'; - public const C_SM = 'SMkk xbbb bbss sssc cccc cccc ccc'; + public const _SM = 'SMkk xbbb bbss sssc cccc cccc ccc'; - public const C_SA = 'SAkk bbcc cccc cccc cccc cccc'; + public const _SA = 'SAkk bbcc cccc cccc cccc cccc'; - public const C_RS = 'RSkk bbbc cccc cccc cccc xx'; + public const _RS = 'RSkk bbbc cccc cccc cccc xx'; - public const C_SK = 'SKkk bbbb ssss sscc cccc cccc'; + public const _SK = 'SKkk bbbb ssss sscc cccc cccc'; - public const C_SI = 'SIkk bbss sccc cccc cxx'; + public const _SI = 'SIkk bbss sccc cccc cxx'; - public const C_ES = 'ESkk bbbb ssss xxcc cccc cccc'; + public const _ES = 'ESkk bbbb ssss xxcc cccc cccc'; - public const C_SE = 'SEkk bbbc cccc cccc cccc cccc'; + public const _SE = 'SEkk bbbc cccc cccc cccc cccc'; - public const C_CH = 'CHkk bbbb bccc cccc cccc c'; + public const _CH = 'CHkk bbbb bccc cccc cccc c'; - public const C_TN = 'TNkk bbss sccc cccc cccc cccc'; + public const _TN = 'TNkk bbss sccc cccc cccc cccc'; - public const C_TR = 'TRkk bbbb bxcc cccc cccc cccc cc'; + public const _TR = 'TRkk bbbb bxcc cccc cccc cccc cc'; - public const C_UA = 'UAkk bbbb bbcc cccc cccc cccc cccc c'; + public const _UA = 'UAkk bbbb bbcc cccc cccc cccc cccc c'; - public const C_AE = 'AEkk bbbc cccc cccc cccc ccc'; + public const _AE = 'AEkk bbbc cccc cccc cccc ccc'; - public const C_GB = 'GBkk bbbb ssss sscc cccc cc'; + public const _GB = 'GBkk bbbb ssss sscc cccc cc'; - public const C_VG = 'VGkk bbbb cccc cccc cccc cccc'; + public const _VG = 'VGkk bbbb cccc cccc cccc cccc'; - public const C_SN = 'SNkk annn nnnn nnnn nnnn nnnn nnnn'; + public const _SN = 'SNkk annn nnnn nnnn nnnn nnnn nnnn'; - public const C_MZ = 'MZkk nnnn nnnn nnnn nnnn nnnn n'; + public const _MZ = 'MZkk nnnn nnnn nnnn nnnn nnnn n'; - public const C_ML = 'MLkk annn nnnn nnnn nnnn nnnn nnnn'; + public const _ML = 'MLkk annn nnnn nnnn nnnn nnnn nnnn'; - public const C_MG = 'MGkk nnnn nnnn nnnn nnnn nnnn nnn'; + public const _MG = 'MGkk nnnn nnnn nnnn nnnn nnnn nnn'; - public const C_CI = 'CIkk annn nnnn nnnn nnnn nnnn nnnn'; + public const _CI = 'CIkk annn nnnn nnnn nnnn nnnn nnnn'; - public const C_IR = 'IRkk nnnn nnnn nnnn nnnn nnnn nn'; + public const _IR = 'IRkk nnnn nnnn nnnn nnnn nnnn nn'; - public const C_CV = 'CVkk nnnn nnnn nnnn nnnn nnnn n'; + public const _CV = 'CVkk nnnn nnnn nnnn nnnn nnnn n'; - public const C_CM = 'CMkk nnnn nnnn nnnn nnnn nnnn nnn'; + public const _CM = 'CMkk nnnn nnnn nnnn nnnn nnnn nnn'; - public const C_BI = 'BIkk nnnn nnnn nnnn'; + public const _BI = 'BIkk nnnn nnnn nnnn'; - public const C_BF = 'BFkk nnnn nnnn nnnn nnnn nnnn nnn'; + public const _BF = 'BFkk nnnn nnnn nnnn nnnn nnnn nnn'; - public const C_BJ = 'BJkk annn nnnn nnnn nnnn nnnn nnnn'; + public const _BJ = 'BJkk annn nnnn nnnn nnnn nnnn nnnn'; - public const C_AO = 'AOkk nnnn nnnn nnnn nnnn nnnn n'; + public const _AO = 'AOkk nnnn nnnn nnnn nnnn nnnn n'; - public const C_DZ = 'DZkk nnnn nnnn nnnn nnnn nnnn'; + public const _DZ = 'DZkk nnnn nnnn nnnn nnnn nnnn'; } diff --git a/Validation/Network/Email.php b/Validation/Network/Email.php index edb68fb2f..81a6dc209 100755 --- a/Validation/Network/Email.php +++ b/Validation/Network/Email.php @@ -39,7 +39,7 @@ abstract class Email extends ValidatorAbstract /** * {@inheritdoc} */ - public static function isValid(mixed $value, array $constraints = null) : bool + public static function isValid(mixed $value, ?array $constraints = null) : bool { if (\filter_var($value, \FILTER_VALIDATE_EMAIL) === false) { self::$msg = 'Invalid Email by filter_var standards'; diff --git a/Validation/Network/Hostname.php b/Validation/Network/Hostname.php index 215c7bdc7..7b6aa25a5 100755 --- a/Validation/Network/Hostname.php +++ b/Validation/Network/Hostname.php @@ -39,9 +39,9 @@ abstract class Hostname extends ValidatorAbstract /** * {@inheritdoc} * - * A IPv6 string MUST be in [...] to be sucessfully validated + * A IPv6 string MUST be in [...] to be successfully validated */ - public static function isValid(mixed $value, array $constraints = null) : bool + public static function isValid(mixed $value, ?array $constraints = null) : bool { //return \filter_var(\gethostbyname($value), \FILTER_VALIDATE_IP) !== false; diff --git a/Validation/Network/Ip.php b/Validation/Network/Ip.php index b8a358f53..dafffce06 100755 --- a/Validation/Network/Ip.php +++ b/Validation/Network/Ip.php @@ -39,7 +39,7 @@ abstract class Ip extends ValidatorAbstract /** * {@inheritdoc} */ - public static function isValid(mixed $value, array $constraints = null) : bool + public static function isValid(mixed $value, ?array $constraints = null) : bool { return \filter_var($value, \FILTER_VALIDATE_IP) !== false; } diff --git a/Validation/Validator.php b/Validation/Validator.php index bbc88455a..80d879cf7 100755 --- a/Validation/Validator.php +++ b/Validation/Validator.php @@ -38,7 +38,7 @@ final class Validator extends ValidatorAbstract * * @since 1.0.0 */ - public static function isValid(mixed $var, array $constraints = null) : bool + public static function isValid(mixed $var, ?array $constraints = null) : bool { if ($constraints === null) { return true; diff --git a/Validation/ValidatorInterface.php b/Validation/ValidatorInterface.php index 955cfa258..48b419fab 100755 --- a/Validation/ValidatorInterface.php +++ b/Validation/ValidatorInterface.php @@ -34,7 +34,7 @@ interface ValidatorInterface * * @since 1.0.0 */ - public static function isValid(mixed $value, array $constraints = null) : bool; + public static function isValid(mixed $value, ?array $constraints = null) : bool; /** * Get most recent error string. diff --git a/Views/View.php b/Views/View.php index f9b743c0a..15863d165 100755 --- a/Views/View.php +++ b/Views/View.php @@ -98,7 +98,7 @@ class View extends ViewAbstract * * @since 1.0.0 */ - public function __construct(L11nManager $l11n = null, RequestAbstract $request = null, ResponseAbstract $response = null) + public function __construct(?L11nManager $l11n = null, ?RequestAbstract $request = null, ?ResponseAbstract $response = null) { $this->l11nManager = $l11n ?? new L11nManager(); $this->request = $request; @@ -201,7 +201,7 @@ class View extends ViewAbstract * * @since 1.0.0 */ - public function getText(string $translation, string $module = null, string $theme = null) : string + public function getText(string $translation, ?string $module = null, ?string $theme = null) : string { if ($module === null && $this->module === null) { $this->setModuleDynamically(); @@ -302,7 +302,7 @@ class View extends ViewAbstract * * @since 1.0.0 */ - public function getHtml(string $translation, string $module = null, string $theme = null) : string + public function getHtml(string $translation, ?string $module = null, ?string $theme = null) : string { return \htmlspecialchars($this->getText($translation, $module, $theme)); } @@ -317,7 +317,7 @@ class View extends ViewAbstract * * @since 1.0.0 */ - public function getNumeric(int | float | FloatInt $numeric, string $format = null) : string + public function getNumeric(int | float | FloatInt $numeric, ?string $format = null) : string { return $this->l11nManager->getNumeric($this->l11n, $numeric, $format); } @@ -325,14 +325,14 @@ class View extends ViewAbstract /** * Print a percentage value * - * @param float $percentage Percentage value to print - * @param null|string $format Format type to use + * @param float|FloatInt $percentage Percentage value to print + * @param null|string $format Format type to use * * @return string * * @since 1.0.0 */ - public function getPercentage(float $percentage, string $format = null) : string + public function getPercentage(float | FloatInt $percentage, ?string $format = null) : string { return $this->l11nManager->getPercentage($this->l11n, $percentage, $format); } @@ -351,8 +351,8 @@ class View extends ViewAbstract */ public function getCurrency( int | float | Money | FloatInt $currency, - string $symbol = null, - string $format = null, + ?string $symbol = null, + ?string $format = null, int $divide = 1 ) : string { @@ -369,7 +369,7 @@ class View extends ViewAbstract * * @since 1.0.0 */ - public function getDateTime(\DateTimeInterface $datetime = null, string $format = null) : string + public function getDateTime(?\DateTimeInterface $datetime = null, ?string $format = null) : string { return $this->l11nManager->getDateTime($this->l11n, $datetime, $format); } diff --git a/Views/ViewAbstract.php b/Views/ViewAbstract.php index d39ca6b64..f218c0274 100755 --- a/Views/ViewAbstract.php +++ b/Views/ViewAbstract.php @@ -32,7 +32,7 @@ abstract class ViewAbstract implements RenderableInterface * @var string * @since 1.0.0 */ - protected const BASE_PATH = __DIR__ . '/../..'; + public const BASE_PATH = __DIR__ . '/../..'; /** * Output is buffered @@ -230,7 +230,7 @@ abstract class ViewAbstract implements RenderableInterface } /** - * Arrayify view and it's subviews. + * Arrayify view and it's sub-views. * * @return array * diff --git a/preload.php b/preload.php index 6f719dcad..9d94e5a8e 100755 --- a/preload.php +++ b/preload.php @@ -4,7 +4,7 @@ * * PHP Version 8.1 * - * @package Karaka + * @package Jingga * @copyright Dennis Eichhorn * @license OMS License 2.0 * @version 1.0.0 diff --git a/tests/Account/AccountManagerTest.php b/tests/Account/AccountManagerTest.php index bc1e19090..63c6ea074 100755 --- a/tests/Account/AccountManagerTest.php +++ b/tests/Account/AccountManagerTest.php @@ -73,7 +73,7 @@ final class AccountManagerTest extends \PHPUnit\Framework\TestCase public function testRetrieveAccount() : void { $this->manager->add($this->account); - self::assertEquals($this->account, $this->manager->get($this->account->getId())); + self::assertEquals($this->account, $this->manager->get($this->account->id)); } /** @@ -87,7 +87,7 @@ final class AccountManagerTest extends \PHPUnit\Framework\TestCase $added = $this->manager->add($this->account); self::assertFalse($added); - self::assertTrue($this->manager->remove($this->account->getId())); + self::assertTrue($this->manager->remove($this->account->id)); self::assertFalse($this->manager->remove(-1)); self::assertEquals(0, $this->manager->count()); } @@ -100,7 +100,7 @@ final class AccountManagerTest extends \PHPUnit\Framework\TestCase public function testRemoveAccount() : void { $this->manager->add($this->account); - self::assertTrue($this->manager->remove($this->account->getId())); + self::assertTrue($this->manager->remove($this->account->id)); self::assertEquals(0, $this->manager->count()); self::assertFalse($this->manager->remove(-1)); } @@ -115,7 +115,7 @@ final class AccountManagerTest extends \PHPUnit\Framework\TestCase $this->manager->add($this->account); self::assertFalse($this->manager->remove(-1)); self::assertEquals(1, $this->manager->count()); - self::assertTrue($this->manager->remove($this->account->getId())); + self::assertTrue($this->manager->remove($this->account->id)); self::assertEquals(0, $this->manager->count()); } } diff --git a/tests/Account/AccountTest.php b/tests/Account/AccountTest.php index 130d2c571..99891fb9e 100755 --- a/tests/Account/AccountTest.php +++ b/tests/Account/AccountTest.php @@ -53,8 +53,8 @@ final class AccountTest extends \PHPUnit\Framework\TestCase $account = new Account(); /* Testing default values */ - self::assertIsInt($account->getId()); - self::assertEquals(0, $account->getId()); + self::assertIsInt($account->id); + self::assertEquals(0, $account->id); self::assertInstanceOf('\phpOMS\Localization\Localization', $account->l11n); @@ -74,11 +74,11 @@ final class AccountTest extends \PHPUnit\Framework\TestCase self::assertIsString($account->getEmail()); self::assertEquals('', $account->getEmail()); - self::assertIsInt($account->getStatus()); - self::assertEquals(AccountStatus::INACTIVE, $account->getStatus()); + self::assertIsInt($account->status); + self::assertEquals(AccountStatus::INACTIVE, $account->status); - self::assertIsInt($account->getType()); - self::assertEquals(AccountType::USER, $account->getType()); + self::assertIsInt($account->type); + self::assertEquals(AccountType::USER, $account->type); self::assertEquals([], $account->getPermissions()); self::assertFalse($account->hasGroup(2)); @@ -145,34 +145,6 @@ final class AccountTest extends \PHPUnit\Framework\TestCase self::assertEquals('d.duck@duckburg.com', $account->getEmail()); } - /** - * @testdox The default status of the account can be changed to a different valid status - * @covers phpOMS\Account\Account - * @group framework - */ - public function testChangeStatus() : void - { - $account = new Account(); - $account->generatePassword('abcd'); - - $account->setStatus(AccountStatus::ACTIVE); - self::assertEquals(AccountStatus::ACTIVE, $account->getStatus()); - } - - /** - * @testdox The default type of the account can be changed to a different valid type - * @covers phpOMS\Account\Account - * @group framework - */ - public function testChangeType() : void - { - $account = new Account(); - $account->generatePassword('abcd'); - - $account->setType(AccountType::GROUP); - self::assertEquals(AccountType::GROUP, $account->getType()); - } - /** * @testdox Account permissions can be added * @covers phpOMS\Account\Account @@ -293,40 +265,4 @@ final class AccountTest extends \PHPUnit\Framework\TestCase $account = new Account(); $account->setEmail('d.duck!@#%@duckburg'); } - - /** - * @testdox An account can only have valid account status - * @group framework - */ - public function testStatusException() : void - { - $this->expectException(\phpOMS\Stdlib\Base\Exception\InvalidEnumValue::class); - - $account = new Account(); - - $rand = 0; - do { - $rand = \mt_rand(\PHP_INT_MIN, \PHP_INT_MAX); - } while (AccountStatus::isValidValue($rand)); - - $account->setStatus($rand); - } - - /** - * @testdox An account can only have valid account types - * @group framework - */ - public function testTypeException() : void - { - $this->expectException(\phpOMS\Stdlib\Base\Exception\InvalidEnumValue::class); - - $account = new Account(); - - $rand = 0; - do { - $rand = \mt_rand(\PHP_INT_MIN, \PHP_INT_MAX); - } while (AccountType::isValidValue($rand)); - - $account->setType($rand); - } } diff --git a/tests/Account/GroupTest.php b/tests/Account/GroupTest.php index f55bbd427..a6d4c36c7 100755 --- a/tests/Account/GroupTest.php +++ b/tests/Account/GroupTest.php @@ -38,14 +38,14 @@ final class GroupTest extends \PHPUnit\Framework\TestCase $group = new Group(); /* Testing default values */ - self::assertIsInt($group->getId()); - self::assertEquals(0, $group->getId()); + self::assertIsInt($group->id); + self::assertEquals(0, $group->id); self::assertIsString($group->name); self::assertEquals('', $group->name); - self::assertIsInt($group->getStatus()); - self::assertEquals(GroupStatus::INACTIVE, $group->getStatus()); + self::assertIsInt($group->status); + self::assertEquals(GroupStatus::INACTIVE, $group->status); self::assertIsString($group->description); self::assertEquals('', $group->description); @@ -138,36 +138,4 @@ final class GroupTest extends \PHPUnit\Framework\TestCase $group->removePermission($perm); self::assertCount(0, $group->getPermissions()); } - - /** - * @testdox The default status of the group can be changed to a different valid status - * @covers phpOMS\Account\Group - * @group framework - */ - public function testChangeStatus() : void - { - $group = new Group(); - - $group->setStatus(GroupStatus::ACTIVE); - self::assertEquals(GroupStatus::ACTIVE, $group->getStatus()); - } - - /** - * @testdox A group can only have valid group status - * @covers phpOMS\Account\Group - * @group framework - */ - public function testStatusException() : void - { - $this->expectException(\phpOMS\Stdlib\Base\Exception\InvalidEnumValue::class); - - $group = new Group(); - - $rand = 0; - do { - $rand = \mt_rand(\PHP_INT_MIN, \PHP_INT_MAX); - } while (GroupStatus::isValidValue($rand)); - - $group->setStatus($rand); - } } diff --git a/tests/Account/NullAccountTest.php b/tests/Account/NullAccountTest.php index f3c5e5470..e08f6c07e 100755 --- a/tests/Account/NullAccountTest.php +++ b/tests/Account/NullAccountTest.php @@ -42,7 +42,7 @@ final class NullAccountTest extends \PHPUnit\Framework\TestCase public function testId() : void { $null = new NullAccount(2); - self::assertEquals(2, $null->getId()); + self::assertEquals(2, $null->id); } public function testJsonSerialization() : void diff --git a/tests/Account/NullGroupTest.php b/tests/Account/NullGroupTest.php index 39694ba93..03e4738e3 100755 --- a/tests/Account/NullGroupTest.php +++ b/tests/Account/NullGroupTest.php @@ -42,7 +42,7 @@ final class NullGroupTest extends \PHPUnit\Framework\TestCase public function testId() : void { $null = new NullGroup(2); - self::assertEquals(2, $null->getId()); + self::assertEquals(2, $null->id); } public function testJsonSerialization() : void diff --git a/tests/Algorithm/Frequency/AprioriTest.php b/tests/Algorithm/Frequency/AprioriTest.php new file mode 100644 index 000000000..fe998058b --- /dev/null +++ b/tests/Algorithm/Frequency/AprioriTest.php @@ -0,0 +1,115 @@ + 2, + 'epsilon' => 2, + 'epsilon:theta' => 0, + 'beta' => 4, + 'beta:theta' => 2, + 'beta:epsilon' => 2, + 'beta:epsilon:theta' => 0, + 'alpha' => 4, + 'alpha:theta' => 2, + 'alpha:epsilon' => 2, + 'alpha:epsilon:theta' => 0, + 'alpha:beta' => 4, + 'alpha:beta:theta' => 2, + 'alpha:beta:epsilon' => 2, + 'alpha:beta:epsilon:theta' => 0, + ], + Apriori::apriori([ + ['alpha', 'beta', 'epsilon'], + ['alpha', 'beta', 'theta'], + ['alpha', 'beta', 'epsilon'], + ['alpha', 'beta', 'theta'], + ]) + ); + + self::assertEquals( + [ + '4' => 5, + '3' => 3, + '3:4' => 3, + '2' => 5, + '2:4' => 4, + '2:3' => 2, + '2:3:4' => 2, + '1' => 3, + '1:4' => 2, + '1:3' => 1, + '1:3:4' => 1, + '1:2' => 3, + '1:2:4' => 2, + '1:2:3' => 1, + '1:2:3:4' => 1, + ], + Apriori::apriori([ + ['1', '2', '3', '4'], + ['1', '2', '4'], + ['1', '2'], + ['2', '3', '4'], + ['3', '4'], + ['2', '4'], + ]) + ); + } + + public function testAprioriSubset() : void + { + self::assertEquals( + ['beta:theta' => 2], + Apriori::apriori( + [ + ['alpha', 'beta', 'epsilon'], + ['alpha', 'beta', 'theta'], + ['alpha', 'beta', 'epsilon'], + ['alpha', 'beta', 'theta'], + ], + ['beta', 'theta'] + ) + ); + + self::assertEquals( + ['2:3' => 2], + Apriori::apriori( + [ + ['1', '2', '3', '4'], + ['1', '2', '4'], + ['1', '2'], + ['2', '3', '4'], + ['3', '4'], + ['2', '4'], + ], + ['2', '3'] + ) + ); + } +} diff --git a/tests/Algorithm/Graph/DependencyResolverTest.php b/tests/Algorithm/Graph/DependencyResolverTest.php new file mode 100644 index 000000000..eeab355b6 --- /dev/null +++ b/tests/Algorithm/Graph/DependencyResolverTest.php @@ -0,0 +1,50 @@ + [1, 2], 1 => [0, 2], 2 => []]) + ); + } + + /** + * @covers phpOMS\Algorithm\Graph\DependencyResolver + * @group framework + */ + public function testResolve() : void + { + self::assertEquals( + [2, 3, 1, 0], + DependencyResolver::resolve([0 => [1, 2], 1 => [2, 3], 2 => [], 3 => []]) + ); + } +} diff --git a/tests/Algorithm/Graph/MarkovChainTest.php b/tests/Algorithm/Graph/MarkovChainTest.php new file mode 100644 index 000000000..0521dc2d5 --- /dev/null +++ b/tests/Algorithm/Graph/MarkovChainTest.php @@ -0,0 +1,105 @@ +setTraining( + [ + 'A' => ['A' => 0.1, 'C' => 0.6, 'E' => 0.3], + 'C' => ['A' => 0.25, 'C' => 0.05, 'E' => 0.7], + 'E' => ['A' => 0.7, 'C' => 0.3, 'E' => 0.0], + ] + ); + + self::assertEquals(3, \count($markov->generate(3, ['A']))); + } + + /** + * @covers phpOMS\Algorithm\Graph\MarkovChain + * @group framework + */ + public function testTrainingGenerate() : void + { + $markov = new MarkovChain(); + $markov->train(['A', 'C', 'E', 'A', 'C', 'E', 'E', 'C', 'A', 'A', 'E', 'A']); + + self::assertEquals(5, \count($markov->generate(5, ['A']))); + } + + /** + * @covers phpOMS\Algorithm\Graph\MarkovChain + * @group framework + */ + public function testStepProbability() : void + { + $markov = new MarkovChain(2); + $markov->setTraining( + [ + 'A A' => ['A' => 0.18, 'D' => 0.6, 'G' => 0.22], + 'A D' => ['A' => 0.5, 'D' => 0.5, 'G' => 0.0], + 'A G' => ['A' => 0.15, 'D' => 0.75, 'G' => 0.1], + 'D D' => ['A' => 0.0, 'D' => 0.0, 'G' => 1.0], + 'D A' => ['A' => 0.25, 'D' => 0.0, 'G' => 0.75], + 'D G' => ['A' => 0.9, 'D' => 0.1, 'G' => 0.0], + 'G G' => ['A' => 0.4, 'D' => 0.4, 'G' => 0.2], + 'G A' => ['A' => 0.5, 'D' => 0.25, 'G' => 0.25], + 'G D' => ['A' => 1.0, 'D' => 0.0, 'G' => 0.0], + ] + ); + + self::assertEquals(0.1, $markov->stepProbability(['D', 'G'], 'D')); + } + + /** + * @covers phpOMS\Algorithm\Graph\MarkovChain + * @group framework + */ + public function testPathProbability() : void + { + $markov = new MarkovChain(2); + $markov->setTraining( + [ + 'A A' => ['A' => 0.18, 'D' => 0.6, 'G' => 0.22], + 'A D' => ['A' => 0.5, 'D' => 0.5, 'G' => 0.0], + 'A G' => ['A' => 0.15, 'D' => 0.75, 'G' => 0.1], + 'D D' => ['A' => 0.0, 'D' => 0.0, 'G' => 1.0], + 'D A' => ['A' => 0.25, 'D' => 0.0, 'G' => 0.75], + 'D G' => ['A' => 0.9, 'D' => 0.1, 'G' => 0.0], + 'G G' => ['A' => 0.4, 'D' => 0.4, 'G' => 0.2], + 'G A' => ['A' => 0.5, 'D' => 0.25, 'G' => 0.25], + 'G D' => ['A' => 1.0, 'D' => 0.0, 'G' => 0.0], + ] + ); + + self::assertEquals(0.9 * 0.5 * 0.6, $markov->pathProbability(['D', 'G', 'A', 'A', 'D'])); + } +} diff --git a/tests/Algorithm/JobScheduling/WeightedTest.php b/tests/Algorithm/JobScheduling/WeightedTest.php index fa7bd0412..69ce8908f 100755 --- a/tests/Algorithm/JobScheduling/WeightedTest.php +++ b/tests/Algorithm/JobScheduling/WeightedTest.php @@ -46,7 +46,7 @@ final class WeightedTest extends \PHPUnit\Framework\TestCase $names = []; foreach ($filtered as $job) { - $value += $job->getValue(); + $value += $job->getValue(); $names[] = $job->name; } diff --git a/tests/Api/Geocoding/NominatimTest.php b/tests/Api/Geocoding/NominatimTest.php new file mode 100644 index 000000000..2d12e42fc --- /dev/null +++ b/tests/Api/Geocoding/NominatimTest.php @@ -0,0 +1,46 @@ + 50.3050738, + 'lon' => 8.688465172531158, + ], + Nominatim::geocoding('de', 'Rosbach', 'Kirchstraße 33'), + 0.01 + ); + + self::assertEqualsWithDelta( + [ + 'lat' => 50.3050738, + 'lon' => 8.688465172531158, + ], + Nominatim::geocoding('de', 'Rosbach', 'Kirchstraße 33', '61191'), + 0.01 + ); + } +} diff --git a/tests/Application/ApplicationInfoTest.php b/tests/Application/ApplicationInfoTest.php index 427fe5a3e..6af83db67 100755 --- a/tests/Application/ApplicationInfoTest.php +++ b/tests/Application/ApplicationInfoTest.php @@ -111,7 +111,7 @@ final class ApplicationInfoTest extends \PHPUnit\Framework\TestCase $info = new ApplicationInfo(__DIR__ . '/info-test.json'); $info->load(); - $testObj = new class() { + $testObj = new class() { public $test = 1; public function test() : void diff --git a/tests/Application/ApplicationManagerTest.php b/tests/Application/ApplicationManagerTest.php index 6c594c4e1..aac7e3318 100755 --- a/tests/Application/ApplicationManagerTest.php +++ b/tests/Application/ApplicationManagerTest.php @@ -49,7 +49,7 @@ final class ApplicationManagerTest extends \PHPUnit\Framework\TestCase */ protected function setUp() : void { - $app = new class() extends ApplicationAbstract { + $app = new class() extends ApplicationAbstract { protected string $appName = 'Api'; }; @@ -62,12 +62,12 @@ final class ApplicationManagerTest extends \PHPUnit\Framework\TestCase public function get( mixed $ids = null, - string | array $names = null, - int $unit = null, - int $app = null, - string $module = null, - int $group = null, - int $account = null + string | array|null $names = null, + ?int $unit = null, + ?int $app = null, + ?string $module = null, + ?int $group = null, + ?int $account = null ) : mixed { return ''; } @@ -172,7 +172,7 @@ final class ApplicationManagerTest extends \PHPUnit\Framework\TestCase } /** - * @testdox A invalid application path results in no uninstallation + * @testdox A invalid application path results in no uninstall * @covers phpOMS\Application\ApplicationManager * @group framework */ @@ -183,7 +183,7 @@ final class ApplicationManagerTest extends \PHPUnit\Framework\TestCase } /** - * @testdox A missing uninstallation file results in no uninstallation + * @testdox A missing uninstall file results in no uninstall * @covers phpOMS\Application\ApplicationManager * @group framework */ diff --git a/tests/Application/InstallerAbstractTest.php b/tests/Application/InstallerAbstractTest.php index d06211f77..0cdba868d 100755 --- a/tests/Application/InstallerAbstractTest.php +++ b/tests/Application/InstallerAbstractTest.php @@ -32,7 +32,7 @@ final class InstallerAbstractTest extends \PHPUnit\Framework\TestCase */ protected function setUp() : void { - $this->installer = new class() extends InstallerAbstract { + $this->installer = new class() extends InstallerAbstract { public const PATH = __DIR__ . '/Invalid'; }; } diff --git a/tests/Application/Testapp/Admin/Install/Application/Routes.php b/tests/Application/Testapp/Admin/Install/Application/Routes.php index 8bfccbeab..8ca42bfe7 100755 --- a/tests/Application/Testapp/Admin/Install/Application/Routes.php +++ b/tests/Application/Testapp/Admin/Install/Application/Routes.php @@ -15,13 +15,13 @@ declare(strict_types=1); use phpOMS\Router\RouteVerb; return [ - '^.*/testapp.*$' => [ + '^.*/testapp(\?.*$|$)' => [ [ 'dest' => '\phpOMS\tess\Application\Apps\Testapp\Controller\Controller:testEndpoint', 'verb' => RouteVerb::GET, 'permission' => [ - 'type' => 1, - 'state' => 2, + 'type' => 1, + 'state' => 2, ], ], ], diff --git a/tests/Application/Testapp/info.json b/tests/Application/Testapp/info.json index d079e1e00..96bc7c4f2 100755 --- a/tests/Application/Testapp/info.json +++ b/tests/Application/Testapp/info.json @@ -14,7 +14,6 @@ "name": "Jingga", "website": "jingga.app" }, - "description": "The administration module.", "directory": "Admin", "providing": { "Navigation": "*", diff --git a/tests/Application/info-test.json b/tests/Application/info-test.json index bb4cc9cba..8b8fe302f 100755 --- a/tests/Application/info-test.json +++ b/tests/Application/info-test.json @@ -14,7 +14,6 @@ "name": "Jingga", "website": "jingga.app" }, - "description": "The administration module.", "directory": "Admin", "providing": { "Navigation": "*" diff --git a/tests/Autoloader.php b/tests/Autoloader.php index 6e249662d..95dd1b74c 100755 --- a/tests/Autoloader.php +++ b/tests/Autoloader.php @@ -75,8 +75,8 @@ class Autoloader */ public static function defaultAutoloader(string $class) : void { - $class = \ltrim($class, '\\'); - $class = \strtr($class, '_\\', '//'); + $class = \ltrim($class, '\\'); + $class = \strtr($class, '_\\', '//'); if (\stripos($class, 'Web/Backend') !== false || \stripos($class, 'Web/Api') !== false) { $class = \is_dir(__DIR__ . '/Web') ? $class : \str_replace('Web/', 'MainRepository/Web/', $class); diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php index 1b48ed57b..44c8a78a9 100755 --- a/tests/Bootstrap.php +++ b/tests/Bootstrap.php @@ -1,4 +1,15 @@ [ + 'db' => [ 'core' => [ 'masters' => [ - 'admin' => [ + 'admin' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -80,7 +91,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'insert' => [ + 'insert' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -90,7 +101,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'select' => [ + 'select' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -100,7 +111,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'update' => [ + 'update' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -110,7 +121,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'delete' => [ + 'delete' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -120,7 +131,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'schema' => [ + 'schema' => [ 'db' => 'mysql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '3306', /* db host port */ @@ -132,7 +143,7 @@ $CONFIG = [ ], ], 'postgresql' => [ - 'admin' => [ + 'admin' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -142,7 +153,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'insert' => [ + 'insert' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -152,7 +163,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'select' => [ + 'select' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -162,7 +173,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'update' => [ + 'update' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -172,7 +183,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'delete' => [ + 'delete' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -182,7 +193,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'schema' => [ + 'schema' => [ 'db' => 'pgsql', /* db type */ 'host' => '127.0.0.1', /* db host address */ 'port' => '5432', /* db host port */ @@ -194,37 +205,37 @@ $CONFIG = [ ], ], 'sqlite' => [ - 'admin' => [ + 'admin' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'insert' => [ + 'insert' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'select' => [ + 'select' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'update' => [ + 'update' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'delete' => [ + 'delete' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'schema' => [ + 'schema' => [ 'db' => 'sqlite', /* db type */ 'database' => __DIR__ . '/../Localization/Defaults/localization.sqlite', /* db name */ 'weight' => 1000, /* db table prefix */ @@ -232,7 +243,7 @@ $CONFIG = [ ], ], 'mssql' => [ - 'admin' => [ + 'admin' => [ 'db' => 'mssql', /* db type */ 'host' => 'localhost', /* db host address */ 'port' => '1433', /* db host port */ @@ -242,7 +253,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'insert' => [ + 'insert' => [ 'db' => 'mssql', /* db type */ 'host' => 'localhost', /* db host address */ 'port' => '1433', /* db host port */ @@ -252,7 +263,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'select' => [ + 'select' => [ 'db' => 'mssql', /* db type */ 'host' => 'localhost', /* db host address */ 'port' => '1433', /* db host port */ @@ -262,7 +273,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'update' => [ + 'update' => [ 'db' => 'mssql', /* db type */ 'host' => 'localhost', /* db host address */ 'port' => '1433', /* db host port */ @@ -272,7 +283,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'delete' => [ + 'delete' => [ 'db' => 'mssql', /* db type */ 'host' => 'localhost', /* db host address */ 'port' => '1433', /* db host port */ @@ -282,7 +293,7 @@ $CONFIG = [ 'weight' => 1000, /* db table prefix */ 'datetimeformat' => 'Y-m-d H:i:s', ], - 'schema' => [ + 'schema' => [ 'db' => 'mssql', /* db type */ 'host' => 'localhost', /* db host address */ 'port' => '1433', /* db host port */ @@ -322,16 +333,16 @@ $CONFIG = [ 'password' => '123456', ], ], - 'log' => [ + 'log' => [ 'file' => [ 'path' => __DIR__ . '/Logs', ], ], - 'page' => [ + 'page' => [ 'root' => '/', 'https' => false, ], - 'app' => [ + 'app' => [ 'path' => __DIR__, 'default' => [ 'app' => 'Backend', @@ -350,7 +361,7 @@ $CONFIG = [ ], ], ], - 'socket' => [ + 'socket' => [ 'master' => [ 'host' => '127.0.0.1', 'limit' => 300, @@ -360,7 +371,7 @@ $CONFIG = [ 'language' => [ 'en', ], - 'apis' => [ + 'apis' => [ ], ]; diff --git a/tests/Business/Finance/FinanceFormulasTest.php b/tests/Business/Finance/FinanceFormulasTest.php index 54d2b1b59..dcf12e886 100755 --- a/tests/Business/Finance/FinanceFormulasTest.php +++ b/tests/Business/Finance/FinanceFormulasTest.php @@ -121,13 +121,13 @@ final class FinanceFormulasTest extends \PHPUnit\Framework\TestCase * @covers phpOMS\Business\Finance\FinanceFormulas * @group framework */ - public function testAnnutiyPaymentFactorPV() : void + public function testAnnuityPaymentFactorPV() : void { $expected = 0.21216; $r = 0.02; $n = 5; - $p = FinanceFormulas::getAnnutiyPaymentFactorPV($r, $n); + $p = FinanceFormulas::getAnnuityPaymentFactorPV($r, $n); self::assertEqualsWithDelta(\round($expected, 5), \round($p, 5), 0.01); self::assertEqualsWithDelta($n, FinanceFormulas::getNumberOfAPFPV($p, $r), 0.01); @@ -314,8 +314,8 @@ final class FinanceFormulasTest extends \PHPUnit\Framework\TestCase $C = \round(FinanceFormulas::getCompoundInterest($P, $r, $t), 2); self::assertEqualsWithDelta(\round($expected, 2), $C, 0.01); - self::assertEqualsWithDelta($P, FinanceFormulas::getPrincipalOfCompundInterest($C, $r, $t), 0.1); - self::assertEqualsWithDelta($t, (int) \round(FinanceFormulas::getPeriodsOfCompundInterest($P, $C, $r), 0), 0.01); + self::assertEqualsWithDelta($P, FinanceFormulas::getPrincipalOfCompoundInterest($C, $r, $t), 0.1); + self::assertEqualsWithDelta($t, (int) \round(FinanceFormulas::getPeriodsOfCompoundInterest($P, $C, $r), 0), 0.01); } /** diff --git a/tests/Business/Recommendation/BayesianPersonalizedRankingTest.php b/tests/Business/Recommendation/BayesianPersonalizedRankingTest.php new file mode 100644 index 000000000..f8ed76a2e --- /dev/null +++ b/tests/Business/Recommendation/BayesianPersonalizedRankingTest.php @@ -0,0 +1,24 @@ + [1.0, 2.0], + 'B' => [2.0, 4.0], + 'C' => [2.5, 4.0], + 'D' => [4.5, 5.0], + ]); + + self::assertEquals( + ['B', 'C'], + $memory->bestMatch([2.2, 4.1], 2) + ); + } +} diff --git a/tests/Business/Recommendation/ModelCFTest.php b/tests/Business/Recommendation/ModelCFTest.php new file mode 100644 index 000000000..4e10ac810 --- /dev/null +++ b/tests/Business/Recommendation/ModelCFTest.php @@ -0,0 +1,48 @@ +cache->get('test')); self::assertEquals( [ - 'status' => CacheStatus::OK, - 'count' => 0, - 'size' => 0, + 'status' => CacheStatus::OK, + 'count' => 0, + 'size' => 0, ], $this->cache->stats() ); @@ -237,7 +237,7 @@ final class FileCacheTest extends \PHPUnit\Framework\TestCase self::assertEquals([], $this->cache->getLike('key\d')); } - public function testExpiredDelteLike() : void + public function testExpiredDeleteLike() : void { $this->cache->set('key1', 'testVal1', 2); $this->cache->set('key2', 'testVal2', 2); @@ -339,9 +339,9 @@ final class FileCacheTest extends \PHPUnit\Framework\TestCase self::assertEquals( [ - 'status' => CacheStatus::OK, - 'count' => 2, - 'size' => 17, + 'status' => CacheStatus::OK, + 'count' => 2, + 'size' => 17, ], $this->cache->stats() ); @@ -365,9 +365,9 @@ final class FileCacheTest extends \PHPUnit\Framework\TestCase self::assertEquals( [ - 'status' => CacheStatus::OK, - 'count' => 0, - 'size' => 0, + 'status' => CacheStatus::OK, + 'count' => 0, + 'size' => 0, ], $this->cache->stats() ); diff --git a/tests/DataStorage/Cache/Connection/MemCachedTest.php b/tests/DataStorage/Cache/Connection/MemCachedTest.php index fe8614030..db3ff6c14 100755 --- a/tests/DataStorage/Cache/Connection/MemCachedTest.php +++ b/tests/DataStorage/Cache/Connection/MemCachedTest.php @@ -63,9 +63,9 @@ final class MemCachedTest extends \PHPUnit\Framework\TestCase self::assertEquals(11211, $this->cache->getPort()); self::assertEquals( [ - 'status' => CacheStatus::OK, - 'count' => 0, - 'size' => 0, + 'status' => CacheStatus::OK, + 'count' => 0, + 'size' => 0, ], $this->cache->stats() ); @@ -321,7 +321,7 @@ final class MemCachedTest extends \PHPUnit\Framework\TestCase self::assertTrue($this->cache->flushAll()); self::assertNull($this->cache->get('key5')); - // Carefull memcached is dumb and keeps expired elements which were not acessed after flushing in stat + // Careful memcached is dumb and keeps expired elements which were not acessed after flushing in stat self::assertGreaterThanOrEqual(0, $this->cache->stats()['count']); self::assertGreaterThanOrEqual(0, $this->cache->stats()['size']); } diff --git a/tests/DataStorage/Database/DataMapperAbstractTest.php b/tests/DataStorage/Database/DataMapperAbstractTest.php index 1e34165fd..ceb2b0873 100755 --- a/tests/DataStorage/Database/DataMapperAbstractTest.php +++ b/tests/DataStorage/Database/DataMapperAbstractTest.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace phpOMS\tests\DataStorage\Database; +include_once __DIR__ . '/../../Autoloader.php'; + use phpOMS\DataStorage\Database\Query\OrderType; use phpOMS\tests\DataStorage\Database\TestModel\BaseModel; use phpOMS\tests\DataStorage\Database\TestModel\BaseModelMapper; @@ -42,21 +44,27 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase { $this->model = new BaseModel(); + \phpOMS\Log\FileLogger::getInstance()->verbose = true; + $GLOBALS['dbpool']->get()->con->prepare( 'CREATE TABLE `test_base` ( `test_base_id` int(11) NOT NULL AUTO_INCREMENT, `test_base_string` varchar(254) NOT NULL, + `test_base_compress` BLOB NOT NULL, + `test_base_pstring` varchar(254) NOT NULL, `test_base_int` int(11) NOT NULL, `test_base_bool` tinyint(1) DEFAULT NULL, `test_base_null` int(11) DEFAULT NULL, `test_base_float` decimal(5, 4) DEFAULT NULL, `test_base_belongs_to_one` int(11) DEFAULT NULL, + `test_base_belongs_top_one` int(11) DEFAULT NULL, `test_base_owns_one_self` int(11) DEFAULT NULL, + `test_base_owns_onep_self` int(11) DEFAULT NULL, `test_base_json` varchar(254) DEFAULT NULL, `test_base_json_serializable` varchar(254) DEFAULT NULL, `test_base_serializable` varchar(254) DEFAULT NULL, `test_base_datetime` datetime DEFAULT NULL, - `test_base_datetime_null` datetime DEFAULT NULL, /* There was a bug where it returned the current date because new \DateTime(null) === current date which is wrong, we want null as value! */ + `test_base_datetime_null` datetime DEFAULT NULL, PRIMARY KEY (`test_base_id`) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' )->execute(); @@ -112,6 +120,49 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase PRIMARY KEY (`test_has_many_rel_relations_id`) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' )->execute(); + + // private + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_has_many_directp` ( + `test_has_many_directp_id` int(11) NOT NULL AUTO_INCREMENT, + `test_has_many_directp_string` varchar(254) NOT NULL, + `test_has_many_directp_to` int(11) NOT NULL, + PRIMARY KEY (`test_has_many_directp_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); + + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_has_many_relp` ( + `test_has_many_relp_id` int(11) NOT NULL AUTO_INCREMENT, + `test_has_many_relp_string` varchar(254) NOT NULL, + PRIMARY KEY (`test_has_many_relp_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); + + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_has_many_rel_relationsp` ( + `test_has_many_rel_relationsp_id` int(11) NOT NULL AUTO_INCREMENT, + `test_has_many_rel_relationsp_src` int(11) NOT NULL, + `test_has_many_rel_relationsp_dest` int(11) NOT NULL, + PRIMARY KEY (`test_has_many_rel_relationsp_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); + + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_belongs_to_onep` ( + `test_belongs_to_onep_id` int(11) NOT NULL AUTO_INCREMENT, + `test_belongs_to_onep_string` varchar(254) NOT NULL, + PRIMARY KEY (`test_belongs_to_onep_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); + + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_owns_onep` ( + `test_owns_onep_id` int(11) NOT NULL AUTO_INCREMENT, + `test_owns_onep_string` varchar(254) NOT NULL, + PRIMARY KEY (`test_owns_onep_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); } protected function tearDown() : void @@ -123,6 +174,14 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_direct')->execute(); $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_rel')->execute(); $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_rel_relations')->execute(); + + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_directp')->execute(); + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_relp')->execute(); + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_rel_relationsp')->execute(); + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_belongs_to_onep')->execute(); + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_owns_onep')->execute(); + + \phpOMS\Log\FileLogger::getInstance()->verbose = false; } /** @@ -138,7 +197,7 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase public function testCreate() : void { self::assertGreaterThan(0, BaseModelMapper::create()->execute($this->model)); - self::assertGreaterThan(0, $this->model->getId()); + self::assertGreaterThan(0, $this->model->id); } public function testCreateNullModel() : void @@ -153,9 +212,9 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase public function testCreateAlreadyCreatedModel() : void { self::assertGreaterThan(0, $id = BaseModelMapper::create()->execute($this->model)); - self::assertGreaterThan(0, $this->model->getId()); + self::assertGreaterThan(0, $this->model->id); self::assertEquals($id, BaseModelMapper::create()->execute($this->model)); - self::assertEquals($id, $this->model->getId()); + self::assertEquals($id, $this->model->id); } public function testCreateHasManyRelation() : void @@ -197,8 +256,10 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase ->where('id', $id) ->execute(); - self::assertEquals($this->model->getId(), $modelR->getId()); + self::assertEquals($this->model->id, $modelR->id); self::assertEquals($this->model->string, $modelR->string); + self::assertEquals($this->model->compress, $modelR->compress); + self::assertEquals($this->model->getPString(), $modelR->getPString()); self::assertEquals($this->model->int, $modelR->int); self::assertEquals($this->model->bool, $modelR->bool); self::assertEquals($this->model->float, $modelR->float); @@ -218,12 +279,38 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase self::assertEquals($this->model->belongsToOne->string, $modelR->belongsToOne->string); } + public function testGetRaw() : void + { + $id = BaseModelMapper::create()->execute($this->model); + + /** @var BaseModel $modelR */ + $modelR = BaseModelMapper::getRaw() + ->with('belongsToOne') + ->with('ownsOneSelf') + ->with('hasManyDirect') + ->with('hasMnayRelations') + ->with('conditional') + ->where('id', $id) + ->execute(); + + self::assertTrue(\is_array($modelR)); + } + public function testGetAll() : void { BaseModelMapper::create()->execute($this->model); self::assertCount(1, BaseModelMapper::getAll()->execute()); } + public function testGetYield() : void + { + BaseModelMapper::create()->execute($this->model); + + foreach (BaseModelMapper::yield()->execute() as $model) { + self::assertGreaterThan(0, $model->id); + } + } + public function testGetFor() : void { $id = BaseModelMapper::create()->execute($this->model); @@ -247,7 +334,7 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase $id2 = BaseModelMapper::create()->execute($model2); $by = BaseModelMapper::get()->where('string', '456')->execute(); - self::assertEquals($model2->getId(), $by->getId()); + self::assertEquals($model2->id, $by->id); } public function testGetNewest() : void @@ -255,13 +342,14 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase $model1 = new BaseModel(); $model1->datetime = new \DateTime('now'); $id1 = BaseModelMapper::create()->execute($model1); + \sleep(1); $model2 = new BaseModel(); $model2->datetime = new \DateTime('now'); $id2 = BaseModelMapper::create()->execute($model2); $newest = BaseModelMapper::getAll()->sort('id', OrderType::DESC)->limit(1)->execute(); - self::assertEquals($id2, \reset($newest)->getId()); + self::assertEquals($id2, \reset($newest)->id); } public function testGetNullModel() : void @@ -269,6 +357,38 @@ final class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase self::assertEquals(NullBaseModel::class, \get_class(BaseModelMapper::get()->where('id', 99)->execute())); } + public function testCount() : void + { + BaseModelMapper::create()->execute($this->model); + self::assertEquals(1, BaseModelMapper::count()->execute()); + } + + public function testSum() : void + { + BaseModelMapper::create()->execute($this->model); + self::assertEquals(11, BaseModelMapper::sum()->columns(['test_base_int'])->execute()); + } + + public function testExists() : void + { + $id = BaseModelMapper::create()->execute($this->model); + self::assertTrue(BaseModelMApper::exists()->where('id', $id)->execute()); + self::assertFalse(BaseModelMApper::exists()->where('id', $id + 1)->execute()); + } + + public function testHas() : void + { + $id = BaseModelMapper::create()->execute($this->model); + self::assertTrue(BaseModelMApper::has()->with('hasManyRelations')->where('id', $id)->execute()); + self::assertTrue(BaseModelMApper::has()->with('hasManyDirect')->where('id', $id)->execute()); + } + + public function testRandom() : void + { + $id = BaseModelMapper::create()->execute($this->model); + self::assertEquals($id, BaseModelMApper::getRandom()->limit(1)->execute()->id); + } + /** * @covers phpOMS\DataStorage\Database\Mapper\DataMapperAbstract * @covers phpOMS\DataStorage\Database\Mapper\DataMapperFactory diff --git a/tests/DataStorage/Database/Query/BuilderTest.php b/tests/DataStorage/Database/Query/BuilderTest.php index fb61096a5..0defa7035 100755 --- a/tests/DataStorage/Database/Query/BuilderTest.php +++ b/tests/DataStorage/Database/Query/BuilderTest.php @@ -85,7 +85,7 @@ final class BuilderTest extends \PHPUnit\Framework\TestCase $datetime = new \DateTime('now'); $sql = 'SELECT [a].[test], [b].[test] FROM [a], [b] WHERE [a].[test] = \'' . $datetime->format('Y-m-d H:i:s') . '\';'; - $sql = \strtr($sql, '[]', $iS . $iE); + $sql = \strtr($sql, '[]', $iS . $iE); self::assertEquals($sql, $query->select('a.test', 'b.test')->from('a', 'b')->where('a.test', '=', $datetime)->toSql()); $query = new Builder($con); @@ -121,7 +121,7 @@ final class BuilderTest extends \PHPUnit\Framework\TestCase $iE = $con->getGrammar()->systemIdentifierEnd; $query = new Builder($con); - $sql = 'SELECT [a].[test] FROM [a] as b WHERE [a].[test] = 1 ORDER BY \rand() LIMIT 1;'; + $sql = 'SELECT [a].[test] FROM [a] as b WHERE [a].[test] = 1 ORDER BY RAND() LIMIT 1;'; $sql = \strtr($sql, '[]', $iS . $iE); self::assertEquals($sql, $query->random('a.test')->fromAs('a', 'b')->where('a.test', '=', 1)->toSql()); } @@ -535,19 +535,19 @@ final class BuilderTest extends \PHPUnit\Framework\TestCase $iE = $con->getGrammar()->systemIdentifierEnd; $query = new Builder($con); - $sql = 'UPDATE [a] SET [a].[test] = 1, [a].[test2] = 2 WHERE [a].[test] = 1;'; + $sql = 'UPDATE [a] SET [test] = 1, [test2] = 2 WHERE [a].[test] = 1;'; $sql = \strtr($sql, '[]', $iS . $iE); - self::assertEquals($sql, $query->update('a')->set(['a.test' => 1])->set(['a.test2' => 2])->where('a.test', '=', 1)->toSql()); + self::assertEquals($sql, $query->update('a')->set(['test' => 1])->set(['test2' => 2])->where('a.test', '=', 1)->toSql()); $query = new Builder($con); - $sql = 'UPDATE [a] SET [a].[test] = 1, [a].[test2] = 2 WHERE [a].[test] = 1;'; + $sql = 'UPDATE [a] SET [test] = 1, [test2] = 2 WHERE [a].[test] = 1;'; $sql = \strtr($sql, '[]', $iS . $iE); - self::assertEquals($sql, $query->update('a')->sets('a.test', 1)->sets('a.test2', 2)->where('a.test', '=', 1)->toSql()); + self::assertEquals($sql, $query->update('a')->sets('test', 1)->sets('test2', 2)->where('a.test', '=', 1)->toSql()); $query = new Builder($con); - $sql = 'UPDATE [a] SET [a].[test] = 1, [a].[test2] = :test2 WHERE [a].[test] = :test3;'; + $sql = 'UPDATE [a] SET [test] = 1, [test2] = :test2 WHERE [a].[test] = :test3;'; $sql = \strtr($sql, '[]', $iS . $iE); - self::assertEquals($sql, $query->update('a')->set(['a.test' => 1])->set(['a.test2' => new Parameter('test2')])->where('a.test', '=', new Parameter('test3'))->toSql()); + self::assertEquals($sql, $query->update('a')->set(['test' => 1])->set(['test2' => new Parameter('test2')])->where('a.test', '=', new Parameter('test3'))->toSql()); } /** @@ -744,28 +744,6 @@ final class BuilderTest extends \PHPUnit\Framework\TestCase $query->delete(); } - /** - * @testdox Invalid select types throw a InvalidArgumentException - * @group framework - * @dataProvider dbConnectionProvider - */ - public function testInvalidSelectParameter($con) : void - { - if (!$con->isInitialized()) { - self::markTestSkipped(); - - return; - } - - $iS = $con->getGrammar()->systemIdentifierStart; - $iE = $con->getGrammar()->systemIdentifierEnd; - - $this->expectException(\InvalidArgumentException::class); - - $query = new Builder($con, true); - $query->select(false); - } - /** * @testdox Invalid from types throw a InvalidArgumentException * @group framework diff --git a/tests/DataStorage/Database/Query/ColumnTest.php b/tests/DataStorage/Database/Query/ColumnTest.php deleted file mode 100755 index 51a14aa20..000000000 --- a/tests/DataStorage/Database/Query/ColumnTest.php +++ /dev/null @@ -1,32 +0,0 @@ -get())); - } -} diff --git a/tests/DataStorage/Database/Query/CountTest.php b/tests/DataStorage/Database/Query/CountTest.php deleted file mode 100755 index d13cfd7a8..000000000 --- a/tests/DataStorage/Database/Query/CountTest.php +++ /dev/null @@ -1,32 +0,0 @@ -get())); - } -} diff --git a/tests/DataStorage/Database/Query/ExpressionTest.php b/tests/DataStorage/Database/Query/ExpressionTest.php deleted file mode 100755 index 0743230fc..000000000 --- a/tests/DataStorage/Database/Query/ExpressionTest.php +++ /dev/null @@ -1,32 +0,0 @@ -get())); - } -} diff --git a/tests/DataStorage/Database/Query/FromTest.php b/tests/DataStorage/Database/Query/FromTest.php deleted file mode 100755 index f1a7111d9..000000000 --- a/tests/DataStorage/Database/Query/FromTest.php +++ /dev/null @@ -1,32 +0,0 @@ -get())); - } -} diff --git a/tests/DataStorage/Database/Query/IntoTest.php b/tests/DataStorage/Database/Query/IntoTest.php deleted file mode 100755 index fd3daafaa..000000000 --- a/tests/DataStorage/Database/Query/IntoTest.php +++ /dev/null @@ -1,32 +0,0 @@ -get())); - } -} diff --git a/tests/DataStorage/Database/Query/SelectTest.php b/tests/DataStorage/Database/Query/SelectTest.php deleted file mode 100755 index 83a46661e..000000000 --- a/tests/DataStorage/Database/Query/SelectTest.php +++ /dev/null @@ -1,32 +0,0 @@ -get())); - } -} diff --git a/tests/DataStorage/Database/Schema/BuilderTest.php b/tests/DataStorage/Database/Schema/BuilderTest.php index b988c99c0..f5a70693e 100755 --- a/tests/DataStorage/Database/Schema/BuilderTest.php +++ b/tests/DataStorage/Database/Schema/BuilderTest.php @@ -170,7 +170,6 @@ final class BuilderTest extends \PHPUnit\Framework\TestCase $iS = $con->getGrammar()->systemIdentifierStart; $iE = $con->getGrammar()->systemIdentifierEnd; - // @todo: fix, this is not correct for sqlite $query = new Builder($con); $sql = ''; @@ -181,7 +180,7 @@ final class BuilderTest extends \PHPUnit\Framework\TestCase } elseif ($con instanceof SqlServerConnection) { $sql = 'CREATE TABLE IF NOT EXISTS [user_roles] ([user_id] INT AUTO_INCREMENT, [role_id] VARCHAR(10) DEFAULT \'1\' NULL, PRIMARY KEY ([user_id]), FOREIGN KEY ([user_id]) REFERENCES [users] ([ext1_id]), FOREIGN KEY ([role_id]) REFERENCES [roles] ([ext2_id]));'; } elseif ($con instanceof SQLiteConnection) { - $sql = 'CREATE TABLE IF NOT EXISTS [user_roles] ([user_id] INT AUTO_INCREMENT, [role_id] VARCHAR(10) DEFAULT \'1\' NULL, PRIMARY KEY ([user_id]), FOREIGN KEY ([user_id]) REFERENCES [users] ([ext1_id]), FOREIGN KEY ([role_id]) REFERENCES [roles] ([ext2_id]));'; + $sql = 'CREATE TABLE [user_roles] ([user_id] INTEGER PRIMARY KEY AUTOINCREMENT, [role_id] TEXT DEFAULT \'1\' NULL, PRIMARY KEY ([user_id]), FOREIGN KEY ([user_id]) REFERENCES [users] ([ext1_id]), FOREIGN KEY ([role_id]) REFERENCES [roles] ([ext2_id]));'; } $sql = \strtr($sql, '[]', $iS . $iE); diff --git a/tests/DataStorage/Database/Schema/Grammar/MysqlGrammarTest.php b/tests/DataStorage/Database/Schema/Grammar/MysqlGrammarTest.php index dc43f93ef..bc1315f34 100755 --- a/tests/DataStorage/Database/Schema/Grammar/MysqlGrammarTest.php +++ b/tests/DataStorage/Database/Schema/Grammar/MysqlGrammarTest.php @@ -78,7 +78,7 @@ final class MysqlGrammarTest extends \PHPUnit\Framework\TestCase ); } - $delete = new Builder($this->con); + $delete = new Builder($this->con); $delete->dropTable('test') ->dropTable('test_foreign') ->execute(); @@ -101,7 +101,7 @@ final class MysqlGrammarTest extends \PHPUnit\Framework\TestCase self::assertContains('test', $tables); self::assertContains('test_foreign', $tables); - $delete = new Builder($this->con); + $delete = new Builder($this->con); $delete->dropTable('test') ->dropTable('test_foreign') ->execute(); diff --git a/tests/DataStorage/Database/TestModel/BaseModel.php b/tests/DataStorage/Database/TestModel/BaseModel.php index 24b892d34..7c6a6c117 100755 --- a/tests/DataStorage/Database/TestModel/BaseModel.php +++ b/tests/DataStorage/Database/TestModel/BaseModel.php @@ -22,6 +22,10 @@ class BaseModel public string $string = 'Base'; + public string $compress = 'Uncompressed'; + + private string $pstring = 'Private'; + public string $conditional = ''; public int $int = 11; @@ -40,10 +44,18 @@ class BaseModel public array $hasManyRelations = []; + private array $hasManyDirectPrivate = []; + + private array $hasManyRelationsPrivate = []; + public $ownsOneSelf = 0; public $belongsToOne = 0; + private $ownsOneSelfPrivate = 0; + + private $belongsToOnePrivate = 0; + public ?object $serializable = null; public array $json = [1, 2, 3]; @@ -64,11 +76,21 @@ class BaseModel new ManyToManyRelModel(), ]; + $this->hasManyDirectPrivate = [ + new ManyToManyDirectModel(), + new ManyToManyDirectModel(), + ]; + + $this->hasManyRelationsPrivate = [ + new ManyToManyRelModel(), + new ManyToManyRelModel(), + ]; + $this->ownsOneSelf = new OwnsOneModel(); $this->belongsToOne = new BelongsToModel(); $this->serializable = new class() implements SerializableInterface { - public $value = ''; + public $value = ''; public function serialize() : string { @@ -89,6 +111,11 @@ class BaseModel }; } + public function getPString() : string + { + return $this->pstring; + } + public function getId() : int { return $this->id; diff --git a/tests/DataStorage/Database/TestModel/BaseModelMapper.php b/tests/DataStorage/Database/TestModel/BaseModelMapper.php index 37d2146fe..6809192b3 100755 --- a/tests/DataStorage/Database/TestModel/BaseModelMapper.php +++ b/tests/DataStorage/Database/TestModel/BaseModelMapper.php @@ -27,6 +27,8 @@ class BaseModelMapper extends DataMapperFactory public const COLUMNS = [ 'test_base_id' => ['name' => 'test_base_id', 'type' => 'int', 'internal' => 'id'], 'test_base_string' => ['name' => 'test_base_string', 'type' => 'string', 'internal' => 'string', 'autocomplete' => true], + 'test_base_compress' => ['name' => 'test_base_compress', 'type' => 'compress', 'internal' => 'compress',], + 'test_base_pstring' => ['name' => 'test_base_pstring', 'type' => 'pstring', 'internal' => 'pstring', 'private' => true], 'test_base_int' => ['name' => 'test_base_int', 'type' => 'int', 'internal' => 'int'], 'test_base_bool' => ['name' => 'test_base_bool', 'type' => 'bool', 'internal' => 'bool'], 'test_base_null' => ['name' => 'test_base_null', 'type' => 'int', 'internal' => 'null'], @@ -38,6 +40,8 @@ class BaseModelMapper extends DataMapperFactory 'test_base_datetime_null' => ['name' => 'test_base_datetime_null', 'type' => 'DateTime', 'internal' => 'datetime_null'], 'test_base_owns_one_self' => ['name' => 'test_base_owns_one_self', 'type' => 'int', 'internal' => 'ownsOneSelf'], 'test_base_belongs_to_one' => ['name' => 'test_base_belongs_to_one', 'type' => 'int', 'internal' => 'belongsToOne'], + 'test_base_owns_onep_self' => ['name' => 'test_base_owns_onep_self', 'type' => 'int', 'internal' => 'ownsOneSelfPrivate', 'private' => true], + 'test_base_belongs_top_one' => ['name' => 'test_base_belongs_top_one', 'type' => 'int', 'internal' => 'belongsToOnePrivate', 'private' => true], ]; /** @@ -50,6 +54,12 @@ class BaseModelMapper extends DataMapperFactory 'belongsToOne' => [ 'mapper' => BelongsToModelMapper::class, 'external' => 'test_base_belongs_to_one', + 'private' => true, + ], + 'belongsToOnePrivate' => [ + 'mapper' => BelongsToModelPrivateMapper::class, + 'external' => 'test_base_belongs_top_one', + 'private' => true, ], ]; @@ -58,6 +68,10 @@ class BaseModelMapper extends DataMapperFactory 'mapper' => OwnsOneModelMapper::class, 'external' => 'test_base_owns_one_self', ], + 'ownsOneSelfPrivate' => [ + 'mapper' => OwnsOneModelPrivateMapper::class, + 'external' => 'test_base_owns_onep_self', + ], ]; /** @@ -86,6 +100,20 @@ class BaseModelMapper extends DataMapperFactory 'column' => 'title', 'external' => null, ], + 'hasManyDirectPrivate' => [ + 'mapper' => ManyToManyDirectModelPrivateMapper::class, + 'table' => 'test_has_many_directp', + 'self' => 'test_has_many_directp_to', + 'external' => null, + 'private' => true, + ], + 'hasManyRelationsPrivate' => [ + 'mapper' => ManyToManyRelModelPrivateMapper::class, + 'table' => 'test_has_many_rel_relationsp', + 'external' => 'test_has_many_rel_relationsp_src', + 'self' => 'test_has_many_rel_relationsp_dest', + 'private' => true, + ], ]; /** diff --git a/tests/DataStorage/Database/TestModel/BelongsToModelPrivateMapper.php b/tests/DataStorage/Database/TestModel/BelongsToModelPrivateMapper.php new file mode 100644 index 000000000..4ed89646e --- /dev/null +++ b/tests/DataStorage/Database/TestModel/BelongsToModelPrivateMapper.php @@ -0,0 +1,49 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'test_belongs_to_onep_id' => ['name' => 'test_belongs_to_onep_id', 'type' => 'int', 'internal' => 'id'], + 'test_belongs_to_onep_string' => ['name' => 'test_belongs_to_onep_string', 'type' => 'string', 'internal' => 'string'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'test_belongs_to_onep'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'test_belongs_to_onep_id'; + + public const MODEL = BelongsToModel::class; +} diff --git a/tests/DataStorage/Database/TestModel/ManyToManyDirectModelPrivateMapper.php b/tests/DataStorage/Database/TestModel/ManyToManyDirectModelPrivateMapper.php new file mode 100644 index 000000000..e1de0fd8a --- /dev/null +++ b/tests/DataStorage/Database/TestModel/ManyToManyDirectModelPrivateMapper.php @@ -0,0 +1,50 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'test_has_many_directp_id' => ['name' => 'test_has_many_directp_id', 'type' => 'int', 'internal' => 'id'], + 'test_has_many_directp_string' => ['name' => 'test_has_many_directp_string', 'type' => 'string', 'internal' => 'string'], + 'test_has_many_directp_to' => ['name' => 'test_has_many_directp_to', 'type' => 'int', 'internal' => 'to'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'test_has_many_directp'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'test_has_many_directp_id'; + + public const MODEL = ManyToManyDirectModel::class; +} diff --git a/tests/DataStorage/Database/TestModel/ManyToManyRelModelPrivateMapper.php b/tests/DataStorage/Database/TestModel/ManyToManyRelModelPrivateMapper.php new file mode 100644 index 000000000..e7df8b66d --- /dev/null +++ b/tests/DataStorage/Database/TestModel/ManyToManyRelModelPrivateMapper.php @@ -0,0 +1,49 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'test_has_many_relp_id' => ['name' => 'test_has_many_relp_id', 'type' => 'int', 'internal' => 'id'], + 'test_has_many_relp_string' => ['name' => 'test_has_many_relp_string', 'type' => 'string', 'internal' => 'string'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'test_has_many_relp'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'test_has_many_relp_id'; + + public const MODEL = ManyToManyRelModel::class; +} diff --git a/tests/DataStorage/Database/TestModel/OwnsOneModelPrivateMapper.php b/tests/DataStorage/Database/TestModel/OwnsOneModelPrivateMapper.php new file mode 100644 index 000000000..08511b3aa --- /dev/null +++ b/tests/DataStorage/Database/TestModel/OwnsOneModelPrivateMapper.php @@ -0,0 +1,49 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'test_owns_onep_id' => ['name' => 'test_owns_onep_id', 'type' => 'int', 'internal' => 'id'], + 'test_owns_onep_string' => ['name' => 'test_owns_onep_string', 'type' => 'string', 'internal' => 'string'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'test_owns_onep'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'test_owns_onep_id'; + + public const MODEL = OwnsOneModel::class; +} diff --git a/tests/Dispatcher/DispatcherTest.php b/tests/Dispatcher/DispatcherTest.php index 9d9937d12..2af67ec07 100755 --- a/tests/Dispatcher/DispatcherTest.php +++ b/tests/Dispatcher/DispatcherTest.php @@ -39,7 +39,7 @@ final class DispatcherTest extends \PHPUnit\Framework\TestCase */ protected function setUp() : void { - $this->app = new class() extends ApplicationAbstract { + $this->app = new class() extends ApplicationAbstract { protected string $appName = 'Api'; }; diff --git a/tests/Localization/Defaults/CityMapperTest.php b/tests/Localization/Defaults/CityMapperTest.php index 647170efb..cf9cb420d 100755 --- a/tests/Localization/Defaults/CityMapperTest.php +++ b/tests/Localization/Defaults/CityMapperTest.php @@ -33,8 +33,8 @@ final class CityMapperTest extends \PHPUnit\Framework\TestCase public static function setUpBeforeClass() : void { self::$con = new SqliteConnection([ - 'db' => 'sqlite', - 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), + 'db' => 'sqlite', + 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), ]); self::$con->connect(); diff --git a/tests/Localization/Defaults/CountryMapperTest.php b/tests/Localization/Defaults/CountryMapperTest.php index 403bfdc65..9ea15f43f 100755 --- a/tests/Localization/Defaults/CountryMapperTest.php +++ b/tests/Localization/Defaults/CountryMapperTest.php @@ -33,8 +33,8 @@ final class CountryMapperTest extends \PHPUnit\Framework\TestCase public static function setUpBeforeClass() : void { self::$con = new SqliteConnection([ - 'db' => 'sqlite', - 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), + 'db' => 'sqlite', + 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), ]); self::$con->connect(); diff --git a/tests/Localization/Defaults/CountryTest.php b/tests/Localization/Defaults/CountryTest.php index 2d678bfb9..ee5c76381 100755 --- a/tests/Localization/Defaults/CountryTest.php +++ b/tests/Localization/Defaults/CountryTest.php @@ -33,7 +33,7 @@ final class CountryTest extends \PHPUnit\Framework\TestCase public function testDefaults() : void { $obj = new Country(); - self::assertEquals(0, $obj->getId()); + self::assertEquals(0, $obj->id); self::assertEquals('', $obj->getName()); self::assertEquals('', $obj->getCode2()); self::assertEquals('', $obj->getCode3()); diff --git a/tests/Localization/Defaults/CurrencyMapperTest.php b/tests/Localization/Defaults/CurrencyMapperTest.php index ce42e7c81..6c576276c 100755 --- a/tests/Localization/Defaults/CurrencyMapperTest.php +++ b/tests/Localization/Defaults/CurrencyMapperTest.php @@ -33,8 +33,8 @@ final class CurrencyMapperTest extends \PHPUnit\Framework\TestCase public static function setUpBeforeClass() : void { self::$con = new SqliteConnection([ - 'db' => 'sqlite', - 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), + 'db' => 'sqlite', + 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), ]); self::$con->connect(); diff --git a/tests/Localization/Defaults/IbanMapperTest.php b/tests/Localization/Defaults/IbanMapperTest.php index 477dcb63d..f4dcecd3b 100755 --- a/tests/Localization/Defaults/IbanMapperTest.php +++ b/tests/Localization/Defaults/IbanMapperTest.php @@ -33,8 +33,8 @@ final class IbanMapperTest extends \PHPUnit\Framework\TestCase public static function setUpBeforeClass() : void { self::$con = new SqliteConnection([ - 'db' => 'sqlite', - 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), + 'db' => 'sqlite', + 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), ]); self::$con->connect(); @@ -51,7 +51,7 @@ final class IbanMapperTest extends \PHPUnit\Framework\TestCase { /** @var Iban $obj */ $obj = IbanMapper::get()->where('id', 22)->execute(); - self::assertEquals('DE', $obj->getCountry()); + self::assertEquals('DE', $obj->country); self::assertEquals(22, $obj->getChars()); self::assertEquals('18n', $obj->getBban()); self::assertEquals('DEkk bbbb bbbb cccc cccc cc', $obj->getFields()); diff --git a/tests/Localization/Defaults/IbanTest.php b/tests/Localization/Defaults/IbanTest.php index 041247d79..bbf92365f 100755 --- a/tests/Localization/Defaults/IbanTest.php +++ b/tests/Localization/Defaults/IbanTest.php @@ -33,7 +33,7 @@ final class IbanTest extends \PHPUnit\Framework\TestCase public function testDefaults() : void { $obj = new Iban(); - self::assertEquals('', $obj->getCountry()); + self::assertEquals('', $obj->country); self::assertEquals(2, $obj->getChars()); self::assertEquals('', $obj->getBban()); self::assertEquals('', $obj->getFields()); diff --git a/tests/Localization/Defaults/LanguageMapperTest.php b/tests/Localization/Defaults/LanguageMapperTest.php index 0b9758858..fd96411f2 100755 --- a/tests/Localization/Defaults/LanguageMapperTest.php +++ b/tests/Localization/Defaults/LanguageMapperTest.php @@ -33,8 +33,8 @@ final class LanguageMapperTest extends \PHPUnit\Framework\TestCase public static function setUpBeforeClass() : void { self::$con = new SqliteConnection([ - 'db' => 'sqlite', - 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), + 'db' => 'sqlite', + 'database' => \realpath(__DIR__ . '/../../../Localization/Defaults/localization.sqlite'), ]); self::$con->connect(); diff --git a/tests/Localization/Defaults/LanguageTest.php b/tests/Localization/Defaults/LanguageTest.php index 000d36ac4..bfafe2464 100755 --- a/tests/Localization/Defaults/LanguageTest.php +++ b/tests/Localization/Defaults/LanguageTest.php @@ -33,7 +33,7 @@ final class LanguageTest extends \PHPUnit\Framework\TestCase public function testDefaults() : void { $obj = new Language(); - self::assertEquals(0, $obj->getId()); + self::assertEquals(0, $obj->id); self::assertEquals('', $obj->getName()); self::assertEquals('', $obj->getNative()); self::assertEquals('', $obj->getCode2()); diff --git a/tests/Localization/LocalizationTest.php b/tests/Localization/LocalizationTest.php index 980f0e9a5..1e670bec7 100755 --- a/tests/Localization/LocalizationTest.php +++ b/tests/Localization/LocalizationTest.php @@ -48,11 +48,11 @@ final class LocalizationTest extends \PHPUnit\Framework\TestCase */ public function testDefault() : void { - self::assertEquals(0, $this->localization->getId()); + self::assertEquals(0, $this->localization->id); self::assertTrue(ISO3166TwoEnum::isValidValue($this->localization->country)); self::assertTrue(TimeZoneEnumArray::isValidValue($this->localization->getTimezone())); self::assertTrue(ISO639x1Enum::isValidValue($this->localization->language)); - self::assertTrue(ISO4217CharEnum::isValidValue($this->localization->getCurrency())); + self::assertTrue(ISO4217CharEnum::isValidValue($this->localization->currency)); self::assertEquals('0', $this->localization->getCurrencyFormat()); self::assertEquals('.', $this->localization->getDecimal()); self::assertEquals(',', $this->localization->getThousands()); @@ -66,18 +66,6 @@ final class LocalizationTest extends \PHPUnit\Framework\TestCase self::assertEquals([], $this->localization->getVolume()); } - /** - * @testdox Setting a invalid language code throws InvalidEnumValue - * @covers phpOMS\Localization\Localization - * @group framework - */ - public function testInvalidLanguage() : void - { - $this->expectException(\phpOMS\Stdlib\Base\Exception\InvalidEnumValue::class); - - $this->localization->setLanguage('abc'); - } - /** * @testdox Setting a invalid country code throws InvalidEnumValue * @covers phpOMS\Localization\Localization @@ -102,18 +90,6 @@ final class LocalizationTest extends \PHPUnit\Framework\TestCase $this->localization->setTimezone('abc'); } - /** - * @testdox Setting a invalid currency code throws InvalidEnumValue - * @covers phpOMS\Localization\Localization - * @group framework - */ - public function testInvalidCurrency() : void - { - $this->expectException(\phpOMS\Stdlib\Base\Exception\InvalidEnumValue::class); - - $this->localization->setCurrency('abc'); - } - /** * @testdox Setting a invalid angle throws InvalidEnumValue * @covers phpOMS\Localization\Localization @@ -160,28 +136,6 @@ final class LocalizationTest extends \PHPUnit\Framework\TestCase self::assertEquals(TimeZoneEnumArray::get(315), $this->localization->getTimezone()); } - /** - * @testdox The language can be set and returned - * @covers phpOMS\Localization\Localization - * @group framework - */ - public function testLanguageInputOutput() : void - { - $this->localization->setLanguage(ISO639x1Enum::_DE); - self::assertEquals(ISO639x1Enum::_DE, $this->localization->language); - } - - /** - * @testdox The currency can be set and returned - * @covers phpOMS\Localization\Localization - * @group framework - */ - public function testCurrencyInputOutput() : void - { - $this->localization->setCurrency(ISO4217CharEnum::_EUR); - self::assertEquals(ISO4217CharEnum::_EUR, $this->localization->getCurrency()); - } - /** * @testdox The datetime can be set and returned * @covers phpOMS\Localization\Localization @@ -322,7 +276,7 @@ final class LocalizationTest extends \PHPUnit\Framework\TestCase public function testLocalizationFromLanguageCode() : void { $l11n = Localization::fromLanguage(ISO639x1Enum::_DE); - self::assertEquals(ISO4217CharEnum::_EUR, $l11n->getCurrency()); + self::assertEquals(ISO4217CharEnum::_EUR, $l11n->currency); } /** @@ -333,7 +287,7 @@ final class LocalizationTest extends \PHPUnit\Framework\TestCase public function testLocalizationLoading() : void { $this->localization->loadFromLanguage(ISO639x1Enum::_DE); - self::assertEquals(ISO4217CharEnum::_EUR, $this->localization->getCurrency()); + self::assertEquals(ISO4217CharEnum::_EUR, $this->localization->currency); } /** @@ -360,7 +314,7 @@ final class LocalizationTest extends \PHPUnit\Framework\TestCase public function testInvalidCountryLocalizationLoading() : void { $this->localization->loadFromLanguage(ISO639x1Enum::_DE, 'ABC'); - self::assertEquals(ISO4217CharEnum::_EUR, $this->localization->getCurrency()); + self::assertEquals(ISO4217CharEnum::_EUR, $this->localization->currency); } /** @@ -371,6 +325,6 @@ final class LocalizationTest extends \PHPUnit\Framework\TestCase public function testMissingLocalizationLoading() : void { $this->localization->loadFromLanguage(ISO639x1Enum::_AA); - self::assertEquals(ISO4217CharEnum::_USD, $this->localization->getCurrency()); + self::assertEquals(ISO4217CharEnum::_USD, $this->localization->currency); } } diff --git a/tests/Math/Functions/AlgebraTest.php b/tests/Math/Functions/AlgebraTest.php new file mode 100644 index 000000000..7455ea3fe --- /dev/null +++ b/tests/Math/Functions/AlgebraTest.php @@ -0,0 +1,91 @@ + 9, 'y' => 0]], GrahamScan::createConvexHull([['x' => 9, 'y' => 0]])); + + $points = []; + for ($i = 0; $i < 10; ++$i) { + for ($j = 0; $j < 10; ++$j) { + $points[] = ['x' => $i, 'y' => $j]; + } + } + + self::assertEquals([ + ['x' => 0, 'y' => 0], + ['x' => 9, 'y' => 0], + ['x' => 9, 'y' => 9], + ['x' => 0, 'y' => 9], + ], + GrahamScan::createConvexHull($points) + ); + } +} diff --git a/tests/Math/Matrix/MatrixTest.php b/tests/Math/Matrix/MatrixTest.php index f418c5790..276867ec5 100755 --- a/tests/Math/Matrix/MatrixTest.php +++ b/tests/Math/Matrix/MatrixTest.php @@ -69,7 +69,7 @@ final class MatrixTest extends \PHPUnit\Framework\TestCase */ public function testMultMatrix() : void { - self::assertEquals([[0, -5], [-6, -7]], $this->C->getMatrix()); + self::assertEquals([[0, -5], [-6, -7]], $this->C->toArray()); } /** @@ -79,7 +79,7 @@ final class MatrixTest extends \PHPUnit\Framework\TestCase */ public function testMultMatrixScalar() : void { - self::assertEquals([[0, -10], [-12, -14]], $this->C->mult(2)->getMatrix()); + self::assertEquals([[0, -10], [-12, -14]], $this->C->mult(2)->toArray()); } /** @@ -291,7 +291,7 @@ final class MatrixTest extends \PHPUnit\Framework\TestCase */ public function testUpperTriangular() : void { - self::assertEquals([[-6, -7], [0, -5]], $this->C->upperTriangular()->getMatrix()); + self::assertEquals([[-6, -7], [0, -5]], $this->C->upperTriangular()->toArray()); } /** @@ -301,8 +301,8 @@ final class MatrixTest extends \PHPUnit\Framework\TestCase public function testLowerTriangular() : void { self::markTestIncomplete(); - //self::assertEquals([], $this->C->lowerTriangular()->getMatrix()); - //self::assertEquals([], $this->C->diagonalize()->getMatrix()); + //self::assertEquals([], $this->C->lowerTriangular()->toArray()); + //self::assertEquals([], $this->C->diagonalize()->toArray()); } /** @@ -492,4 +492,134 @@ final class MatrixTest extends \PHPUnit\Framework\TestCase $A->mult($B); } + + public function testSumAll() : void + { + $m = Matrix::fromArray([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + + self::assertEquals( + 45, + $m->sum(-1) + ); + } + + public function testSumColumns() : void + { + $m = Matrix::fromArray([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + + self::assertEquals( + [12, 15, 18], + $m->sum(0)->toVectorArray() + ); + } + + public function testSumRows() : void + { + $m = Matrix::fromArray([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + + self::assertEquals( + [6, 15, 24], + $m->sum(1)->toVectorArray() + ); + } + + public function testDiaglonal() : void + { + $m = Matrix::fromArray([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + + self::assertFalse($m->isDiagonal()); + + $m = Matrix::fromArray([ + [1, 0, 0], + [0, 5, 0], + [0, 0, -8], + ]); + + self::assertTrue($m->isDiagonal()); + } + + public function testPow() : void + { + $m = Matrix::fromArray([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + + self::assertEquals( + [ + [30, 36, 42], + [66, 81, 96], + [102, 126, 150], + ], + $m->pow(2)->toArray() + ); + + $m = Matrix::fromArray([ + [1.5, 2.5, 3.5], + [4.5, 5.5, 6.5], + [7.5, 8.5, 9.5], + ]); + + self::assertEqualsWithDelta( + [ + [39.75, 47.25, 54.75], + [80.25, 96.75, 113.25], + [120.75, 146.25, 171.75], + ], + $m->pow(2)->toArray(), + 0.1 + ); + + $m = Matrix::fromArray([ + [1, 1, 1], + [1, 2, 3], + [1, 3, 6], + ]); + + self::assertEqualsWithDelta( + [ + [0.8901, 0.5882, 0.3684], + [0.5882, 1.2035, 1.3799], + [0.3684, 1.3799, 3.1167], + ], + $m->pow(2 / 3)->toArray(), + 0.01 + ); + } + + public function testExp() : void + { + $m = Matrix::fromArray([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + + self::assertEqualsWithDelta( + [ + [1118906.6994131860386, 1374815.062935806540981, 1630724.426458427043361], + [2533881.041898971697907, 3113415.03138055427637, 3692947.020862136854833], + [3948856.384384757357213, 4852012.999825302011759, 5755170.615265846666304], + ], + $m->exp(12)->toArray(), + 0.1 + ); + } } diff --git a/tests/Math/Matrix/VectorTest.php b/tests/Math/Matrix/VectorTest.php index afc52c23c..70200aa89 100755 --- a/tests/Math/Matrix/VectorTest.php +++ b/tests/Math/Matrix/VectorTest.php @@ -65,4 +65,20 @@ final class VectorTest extends \PHPUnit\Framework\TestCase self::assertEquals(5, $vec->getM()); } + + public function testCosine() : void + { + $v1 = Vector::fromArray([3, 4, 0]); + $v2 = Vector::fromArray([4, 4, 2]); + + self::assertEqualsWithDelta(14 / 15, $v1->cosine($v2), 0.1); + } + + public function testCross3() : void + { + self::assertEquals( + [-15, -2, 39], + Vector::fromArray([3, -3, 1])->cross3(Vector::fromArray([4, 9, 2]))->toVectorArray() + ); + } } diff --git a/tests/Math/Optimization/SimplexTest.php b/tests/Math/Optimization/SimplexTest.php new file mode 100644 index 000000000..ca9c1be16 --- /dev/null +++ b/tests/Math/Optimization/SimplexTest.php @@ -0,0 +1,108 @@ +solve( + [ + [-1, 1], + [1, 1], + [1, -4], + ], + [8, -3, 2], + [1, 3] + ), + 0.01 + ); + } + + public function testSimplexBasicFeasible() : void + { + $simplex = new Simplex(); + self::assertEqualsWithDelta( + [ + [1.0, 0.0, 0.0, 0.0], + 5.0, + ], + $simplex->solve( + [ + [-1, 1], + [-2, -1], + ], + [1, 2], + [5, -3] + ), + 0.0 + ); + } + + public function testSimplexLPInfeasible() : void + { + $simplex = new Simplex(); + self::assertEquals( + [ + [-2, -2, -2, -2, -2], + \INF, + ], + $simplex->solve( + [ + [-1, -1], + [2, 2], + ], + [2, -10], + [3, -2] + ) + ); + } + + public function testSimplexLPUnbound() : void + { + $simplex = new Simplex(); + self::assertEqualsWithDelta( + [ + [-1, -1, -1, -1], + \INF, + ], + $simplex->solve( + [ + [2, -1], + [1, 2], + ], + [-1, -2], + [1, -1] + ), + 0.01 + ); + } +} diff --git a/tests/Math/Solver/Root/BisectionTest.php b/tests/Math/Solver/Root/BisectionTest.php new file mode 100644 index 000000000..830452251 --- /dev/null +++ b/tests/Math/Solver/Root/BisectionTest.php @@ -0,0 +1,34 @@ + 3, 'y' => 4, 'z' => 0], ['x' => 4, 'y' => 4, 'z' => 2]), + 0.1 + ); + } + /** * @testdox The bray-curtis distance can be calculated * @covers phpOMS\Math\Topology\MetricsND @@ -193,6 +207,18 @@ final class MetricsNDTest extends \PHPUnit\Framework\TestCase MetricsND::canberra([3, 6, 4], [4, 6, 8, 3]); } + /** + * @testdox Different dimension sizes for the coordinates in the cosine metric throw a InvalidDimensionException + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testInvalidCosineDimension() : void + { + $this->expectException(\phpOMS\Math\Matrix\Exception\InvalidDimensionException::class); + + MetricsND::cosine([3, 6, 4], [4, 6, 8, 3]); + } + /** * @testdox Different dimension sizes for the coordinates in the Bray Curtis metric throw a InvalidDimensionException * @covers phpOMS\Math\Topology\MetricsND diff --git a/tests/Message/HeaderAbstractTest.php b/tests/Message/HeaderAbstractTest.php index f2f3c9ea3..c303e4100 100755 --- a/tests/Message/HeaderAbstractTest.php +++ b/tests/Message/HeaderAbstractTest.php @@ -48,7 +48,7 @@ final class HeaderAbstractTest extends \PHPUnit\Framework\TestCase return true; } - public function get(string $key = null) : array + public function get(?string $key = null) : array { return []; } diff --git a/tests/Message/Http/HttpRequestTest.php b/tests/Message/Http/HttpRequestTest.php index f62d2f485..b90fb5d4b 100755 --- a/tests/Message/Http/HttpRequestTest.php +++ b/tests/Message/Http/HttpRequestTest.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace phpOMS\tests\Message\Http; -use phpOMS\Localization\ISO639x1Enum; use phpOMS\Localization\Localization; use phpOMS\Message\Http\BrowserType; use phpOMS\Message\Http\HttpRequest; @@ -150,21 +149,6 @@ final class HttpRequestTest extends \PHPUnit\Framework\TestCase self::assertEquals('http://www.google.com/test/path', $request->__toString()); } - /** - * @testdox The request langauge can be returned - * @covers phpOMS\Message\Http\HttpRequest - * @group framework - */ - public function testLangaugeOutput() : void - { - $request = new HttpRequest(new HttpUri('http://www.google.com/test/path'), $l11n = new Localization()); - - $request->header->l11n = new Localization(); - $request->header->l11n->setLanguage(ISO639x1Enum::_DE); - - self::assertEquals(ISO639x1Enum::_DE, $request->header->l11n->language); - } - /** * @testdox The url hashes for the different paths get correctly generated * @covers phpOMS\Message\Http\HttpRequest @@ -231,7 +215,7 @@ final class HttpRequestTest extends \PHPUnit\Framework\TestCase */ public function testDataJsonRead() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $data = [ 1, 2, 3, @@ -250,7 +234,7 @@ final class HttpRequestTest extends \PHPUnit\Framework\TestCase */ public function testEmptyDataJsonRead() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); self::assertEquals([], $request->getDataJson('def')); } @@ -262,7 +246,7 @@ final class HttpRequestTest extends \PHPUnit\Framework\TestCase */ public function testInvalidDataJsonRead() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $data = [ "0" => 1, "1" => 2, "2" => 3, @@ -281,7 +265,7 @@ final class HttpRequestTest extends \PHPUnit\Framework\TestCase */ public function testDataList() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $data = [ 1, 2, 3, @@ -299,7 +283,7 @@ final class HttpRequestTest extends \PHPUnit\Framework\TestCase */ public function testEmptyDataList() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); self::assertEquals([], $request->getDataList('def')); } @@ -311,7 +295,7 @@ final class HttpRequestTest extends \PHPUnit\Framework\TestCase */ public function testDataLike() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $data = 'this is a test'; @@ -327,7 +311,7 @@ final class HttpRequestTest extends \PHPUnit\Framework\TestCase */ public function testInvalidDataLikeMatch() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $data = 'this is a test'; diff --git a/tests/Message/Http/HttpResponseTest.php b/tests/Message/Http/HttpResponseTest.php index 571c327ad..0352ccf50 100755 --- a/tests/Message/Http/HttpResponseTest.php +++ b/tests/Message/Http/HttpResponseTest.php @@ -14,8 +14,6 @@ declare(strict_types=1); namespace phpOMS\tests\Message\Http; -use phpOMS\Localization\ISO639x1Enum; -use phpOMS\Localization\Localization; use phpOMS\Message\Http\HttpResponse; use phpOMS\System\MimeType; @@ -105,19 +103,6 @@ final class HttpResponseTest extends \PHPUnit\Framework\TestCase self::assertEquals($start, \ob_get_level()); } - /** - * @testdox The response langauge can be returned - * @covers phpOMS\Message\Http\HttpResponse - * @group framework - */ - public function testLangaugeOutput() : void - { - $this->response->header->l11n = new Localization(); - $this->response->header->l11n->setLanguage(ISO639x1Enum::_DE); - - self::assertEquals(ISO639x1Enum::_DE, $this->response->header->l11n->language); - } - /** * @testdox Response data can be turned into an array * @covers phpOMS\Message\Http\HttpResponse diff --git a/tests/Message/Mail/MailHandlerMailTrait.php b/tests/Message/Mail/MailHandlerMailTrait.php index 81b554660..d49c729c3 100755 --- a/tests/Message/Mail/MailHandlerMailTrait.php +++ b/tests/Message/Mail/MailHandlerMailTrait.php @@ -308,10 +308,10 @@ trait MailHandlerMailTrait $mail->confirmationAddress = 'test1@jingga.app'; $mail->setFrom('test1@jingga.app', 'Dennis Eichhorn'); $mail->addTo('test@jingga.app', 'Dennis Eichhorn'); - $mail->subject = 'testSendICalAltWithMail'; - $mail->body = 'Ical test'; - $mail->bodyAlt = 'Ical test'; - $mail->ical = 'BEGIN:VCALENDAR' + $mail->subject = 'testSendICalAltWithMail'; + $mail->body = 'Ical test'; + $mail->bodyAlt = 'Ical test'; + $mail->ical = 'BEGIN:VCALENDAR' . "\r\nVERSION:2.0" . "\r\nPRODID:-//phpOMS//Karaka Calendar//EN" . $methodLine @@ -366,10 +366,10 @@ trait MailHandlerMailTrait $mail->setFrom('test1@jingga.app', 'Dennis Eichhorn'); $mail->addTo('test@jingga.app', 'Dennis Eichhorn'); $mail->addAttachment(__DIR__ . '/files/logo.png', 'logo'); - $mail->subject = 'testSendICalAltAttachmentWithMail'; - $mail->body = 'Ical test'; - $mail->bodyAlt = 'Ical test'; - $mail->ical = 'BEGIN:VCALENDAR' + $mail->subject = 'testSendICalAltAttachmentWithMail'; + $mail->body = 'Ical test'; + $mail->bodyAlt = 'Ical test'; + $mail->ical = 'BEGIN:VCALENDAR' . "\r\nVERSION:2.0" . "\r\nPRODID:-//phpOMS//Karaka Calendar//EN" . $methodLine diff --git a/tests/Message/Mail/MailHandlerSendmailTrait.php b/tests/Message/Mail/MailHandlerSendmailTrait.php index 9e07c1481..737e33bdf 100755 --- a/tests/Message/Mail/MailHandlerSendmailTrait.php +++ b/tests/Message/Mail/MailHandlerSendmailTrait.php @@ -282,10 +282,10 @@ trait MailHandlerSendmailTrait $mail->confirmationAddress = 'test1@jingga.app'; $mail->setFrom('test1@jingga.app', 'Dennis Eichhorn'); $mail->addTo('test@jingga.app', 'Dennis Eichhorn'); - $mail->subject = 'testSendICalAltWithSendmail'; - $mail->body = 'Ical test'; - $mail->bodyAlt = 'Ical test'; - $mail->ical = 'BEGIN:VCALENDAR' + $mail->subject = 'testSendICalAltWithSendmail'; + $mail->body = 'Ical test'; + $mail->bodyAlt = 'Ical test'; + $mail->ical = 'BEGIN:VCALENDAR' . "\r\nVERSION:2.0" . "\r\nPRODID:-//phpOMS//Karaka Calendar//EN" . $methodLine @@ -338,10 +338,10 @@ trait MailHandlerSendmailTrait $mail->setFrom('test1@jingga.app', 'Dennis Eichhorn'); $mail->addTo('test@jingga.app', 'Dennis Eichhorn'); $mail->addAttachment(__DIR__ . '/files/logo.png', 'logo'); - $mail->subject = 'testSendICalAltAttachmentWithSendmail'; - $mail->body = 'Ical test'; - $mail->bodyAlt = 'Ical test'; - $mail->ical = 'BEGIN:VCALENDAR' + $mail->subject = 'testSendICalAltAttachmentWithSendmail'; + $mail->body = 'Ical test'; + $mail->bodyAlt = 'Ical test'; + $mail->ical = 'BEGIN:VCALENDAR' . "\r\nVERSION:2.0" . "\r\nPRODID:-//phpOMS//Karaka Calendar//EN" . $methodLine diff --git a/tests/Message/Mail/MailHandlerSmtpTrait.php b/tests/Message/Mail/MailHandlerSmtpTrait.php index e14d2cff2..5a3e516c4 100755 --- a/tests/Message/Mail/MailHandlerSmtpTrait.php +++ b/tests/Message/Mail/MailHandlerSmtpTrait.php @@ -391,10 +391,10 @@ trait MailHandlerSmtpTrait $mail->confirmationAddress = 'test1@jingga.app'; $mail->setFrom('test1@jingga.app', 'Dennis Eichhorn'); $mail->addTo('test@jingga.app', 'Dennis Eichhorn'); - $mail->subject = 'testSendICalAltWithSmtp'; - $mail->body = 'Ical test'; - $mail->bodyAlt = 'Ical test'; - $mail->ical = 'BEGIN:VCALENDAR' + $mail->subject = 'testSendICalAltWithSmtp'; + $mail->body = 'Ical test'; + $mail->bodyAlt = 'Ical test'; + $mail->ical = 'BEGIN:VCALENDAR' . "\r\nVERSION:2.0" . "\r\nPRODID:-//phpOMS//Karaka Calendar//EN" . $methodLine @@ -456,10 +456,10 @@ trait MailHandlerSmtpTrait $mail->setFrom('test1@jingga.app', 'Dennis Eichhorn'); $mail->addTo('test@jingga.app', 'Dennis Eichhorn'); $mail->addAttachment(__DIR__ . '/files/logo.png', 'logo'); - $mail->subject = 'testSendICalAltAttachmentWithSmtp'; - $mail->body = 'Ical test'; - $mail->bodyAlt = 'Ical test'; - $mail->ical = 'BEGIN:VCALENDAR' + $mail->subject = 'testSendICalAltAttachmentWithSmtp'; + $mail->body = 'Ical test'; + $mail->bodyAlt = 'Ical test'; + $mail->ical = 'BEGIN:VCALENDAR' . "\r\nVERSION:2.0" . "\r\nPRODID:-//phpOMS//Karaka Calendar//EN" . $methodLine diff --git a/tests/Message/Mail/MailHandlerTest.php b/tests/Message/Mail/MailHandlerTest.php index 0e5f40a7d..540ea0c68 100755 --- a/tests/Message/Mail/MailHandlerTest.php +++ b/tests/Message/Mail/MailHandlerTest.php @@ -42,9 +42,9 @@ final class MailHandlerTest extends \PHPUnit\Framework\TestCase 'countryName' => 'DE', 'stateOrProvinceName' => 'Hesse', 'localityName' => 'Frankfurt', - 'organizationName' => 'Karaka', + 'organizationName' => 'Jingga', 'organizationalUnitName' => 'Framework', - 'commonName' => 'Karaka Test', + 'commonName' => 'Jingga Test', 'emailAddress' => 'test@jingga.app', ]; $keyconfig = [ diff --git a/tests/Message/RequestAbstractTest.php b/tests/Message/RequestAbstractTest.php index 7c6fd82fa..79c996aa7 100755 --- a/tests/Message/RequestAbstractTest.php +++ b/tests/Message/RequestAbstractTest.php @@ -96,4 +96,77 @@ final class RequestAbstractTest extends \PHPUnit\Framework\TestCase $this->request->setData('key5', 1); self::assertEquals(1, $this->request->getData('key5', 'invalid')); } + + public function testDataAllInputOutput() : void + { + $this->request->setData('asdf', false); + self::assertEquals(['asdf' => false], $this->request->getData()); + } + + /** + * @group framework + */ + public function testDataJsonInputOutput() : void + { + $this->request->setData('asdf', '[1,2,3]'); + self::assertEquals([1,2,3], $this->request->getDataJson('asdf')); + } + + /** + * @group framework + */ + public function testDataStringInputOutput() : void + { + $this->request->setData('asdf', 1); + self::assertEquals('1', $this->request->getDataString('asdf')); + self::assertEquals('1', $this->request->getData('asdf', 'string')); + } + + /** + * @group framework + */ + public function testDataBoolInputOutput() : void + { + $this->request->setData('asdf', 1); + self::assertTrue($this->request->getDataBool('asdf')); + self::assertTrue($this->request->getData('asdf', 'bool')); + } + + /** + * @group framework + */ + public function testDataFloatInputOutput() : void + { + $this->request->setData('asdf', 1); + self::assertEquals(1.0, $this->request->getDataFloat('asdf')); + self::assertEquals(1.0, $this->request->getData('asdf', 'float')); + } + + /** + * @group framework + */ + public function testDataDateTimeInputOutput() : void + { + $this->request->setData('asdf', '2023-01-01'); + self::assertEquals((new \DateTime('2023-01-01'))->format('Y-m-d'), $this->request->getDataDateTime('asdf')->format('Y-m-d')); + self::assertEquals((new \DateTime('2023-01-01'))->format('Y-m-d'), $this->request->getData('asdf', 'DateTime')->format('Y-m-d')); + } + + public function testDataInvalidTypeInputOutput() : void + { + $this->request->setData('asdf', 1); + self::assertEquals(1, $this->request->getData('asdf', 'invalid')); + } + + /** + * @group framework + */ + public function testInvalidDataTypeInputOutput() : void + { + self::assertNull($this->request->getDataString('a')); + self::assertNull($this->request->getDataBool('a')); + self::assertNull($this->request->getDataInt('a')); + self::assertNull($this->request->getDataFloat('a')); + self::assertNull($this->request->getDataDateTime('a')); + } } diff --git a/tests/Message/ResponseAbstractTest.php b/tests/Message/ResponseAbstractTest.php index f4df8aab2..87530523b 100755 --- a/tests/Message/ResponseAbstractTest.php +++ b/tests/Message/ResponseAbstractTest.php @@ -67,6 +67,12 @@ final class ResponseAbstractTest extends \PHPUnit\Framework\TestCase self::assertEquals([1], $this->response->jsonSerialize()); } + public function testDataAllInputOutput() : void + { + $this->response->set('asdf', false); + self::assertEquals(['asdf' => false], $this->response->getData()); + } + /** * @testdox Data can be set and returned for the response * @covers phpOMS\Message\ResponseAbstract @@ -77,4 +83,81 @@ final class ResponseAbstractTest extends \PHPUnit\Framework\TestCase $this->response->set('asdf', false); self::assertFalse($this->response->getData('asdf')); } + + /** + * @testdox Data can be set and returned for the response + * @covers phpOMS\Message\ResponseAbstract + * @group framework + */ + public function testDataStringInputOutput() : void + { + $this->response->set('asdf', 1); + self::assertEquals('1', $this->response->getDataString('asdf')); + self::assertEquals('1', $this->response->getData('asdf', 'string')); + } + + /** + * @testdox Data can be set and returned for the response + * @covers phpOMS\Message\ResponseAbstract + * @group framework + */ + public function testDataBoolInputOutput() : void + { + $this->response->set('asdf', 1); + self::assertTrue($this->response->getDataBool('asdf')); + self::assertTrue($this->response->getData('asdf', 'bool')); + } + + /** + * @testdox Data can be set and returned for the response + * @covers phpOMS\Message\ResponseAbstract + * @group framework + */ + public function testDataFloatInputOutput() : void + { + $this->response->set('asdf', 1); + self::assertEquals(1.0, $this->response->getDataFloat('asdf')); + self::assertEquals(1.0, $this->response->getData('asdf', 'float')); + } + + /** + * @group framework + */ + public function testDataJsonInputOutput() : void + { + $this->response->set('asdf', '[1,2,3]'); + self::assertEquals([1,2,3], $this->response->getDataJson('asdf')); + } + + /** + * @testdox Data can be set and returned for the response + * @covers phpOMS\Message\ResponseAbstract + * @group framework + */ + public function testDataDateTimeInputOutput() : void + { + $this->response->set('asdf', '2023-01-01'); + self::assertEquals((new \DateTime('2023-01-01'))->format('Y-m-d'), $this->response->getDataDateTime('asdf')->format('Y-m-d')); + self::assertEquals((new \DateTime('2023-01-01'))->format('Y-m-d'), $this->response->getData('asdf', 'DateTime')->format('Y-m-d')); + } + + public function testDataInvalidTypeInputOutput() : void + { + $this->response->set('asdf', 1); + self::assertEquals(1, $this->response->getData('asdf', 'invalid')); + } + + /** + * @testdox Data can be set and returned for the response + * @covers phpOMS\Message\ResponseAbstract + * @group framework + */ + public function testInvalidDataTypeInputOutput() : void + { + self::assertNull($this->response->getDataString('a')); + self::assertNull($this->response->getDataBool('a')); + self::assertNull($this->response->getDataInt('a')); + self::assertNull($this->response->getDataFloat('a')); + self::assertNull($this->response->getDataDateTime('a')); + } } diff --git a/tests/Model/Html/HeadTest.php b/tests/Model/Html/HeadTest.php index 1639c91b1..4aea21376 100755 --- a/tests/Model/Html/HeadTest.php +++ b/tests/Model/Html/HeadTest.php @@ -43,7 +43,7 @@ final class HeadTest extends \PHPUnit\Framework\TestCase { self::assertInstanceOf('\phpOMS\Model\Html\Meta', $this->head->meta); self::assertEquals('', $this->head->title); - self::assertEquals('en', $this->head->getLanguage()); + self::assertEquals('en', $this->head->language); self::assertEquals([], $this->head->getStyleAll()); self::assertEquals([], $this->head->getScriptAll()); self::assertEquals('', $this->head->renderStyle()); @@ -86,17 +86,6 @@ final class HeadTest extends \PHPUnit\Framework\TestCase self::assertEquals(['key' => 'console.log("msg");'], $this->head->getScriptAll()); } - /** - * @testdox The language can be set and returned - * @covers phpOMS\Model\Html\Head - * @group framework - */ - public function testLanguageInputOutput() : void - { - $this->head->setLanguage('en'); - self::assertEquals('en', $this->head->getLanguage()); - } - /** * @testdox The assets can be set and rendered * @covers phpOMS\Model\Html\Head diff --git a/tests/Model/Message/NotifyTest.php b/tests/Model/Message/NotifyTest.php index b6335f910..40a17e6ab 100755 --- a/tests/Model/Message/NotifyTest.php +++ b/tests/Model/Message/NotifyTest.php @@ -48,7 +48,7 @@ final class NotifyTest extends \PHPUnit\Framework\TestCase $obj->delay = 3; $obj->stay = 5; $obj->level = NotificationLevel::ERROR; - $obj->message ='msg'; + $obj->message = 'msg'; $obj->title = 'title'; self::assertEquals([ diff --git a/tests/Module/InstallerAbstractTest.php b/tests/Module/InstallerAbstractTest.php index d66f88ab9..d3e1c4e02 100755 --- a/tests/Module/InstallerAbstractTest.php +++ b/tests/Module/InstallerAbstractTest.php @@ -61,12 +61,12 @@ final class InstallerAbstractTest extends \PHPUnit\Framework\TestCase public function get( mixed $ids = null, - string | array $names = null, - int $unit = null, - int $app = null, - string $module = null, - int $group = null, - int $account = null + string | array|null $names = null, + ?int $unit = null, + ?int $app = null, + ?string $module = null, + ?int $group = null, + ?int $account = null ) : mixed { return null; diff --git a/tests/Module/ModuleAbstractTest.php b/tests/Module/ModuleAbstractTest.php index c586e79bd..063e11a4d 100755 --- a/tests/Module/ModuleAbstractTest.php +++ b/tests/Module/ModuleAbstractTest.php @@ -26,7 +26,6 @@ use phpOMS\tests\DataStorage\Database\TestModel\BaseModel; use phpOMS\tests\DataStorage\Database\TestModel\BaseModelMapper; use phpOMS\tests\DataStorage\Database\TestModel\ManyToManyRelModel; use phpOMS\tests\DataStorage\Database\TestModel\ManyToManyRelModelMapper; -use phpOMS\Uri\HttpUri; /** * @testdox phpOMS\tests\Module\ModuleAbstractTest: Abstract module @@ -102,7 +101,7 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase $model1 = BaseModelMapper::get()->where('id', 1)->execute(); $model2 = ManyToManyRelModelMapper::get()->where('id', 1)->execute(); - $this->createModelRelation(1, $model1->getId(), $model2->id, BaseModelMapper::class, 'hasManyRelations', '', '127.0.0.1'); + $this->createModelRelation(1, $model1->id, $model2->id, BaseModelMapper::class, 'hasManyRelations', '', '127.0.0.1'); } public function deleteRelationDB() : void @@ -110,7 +109,7 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase $model1 = BaseModelMapper::get()->where('id', 1)->execute(); $model2 = ManyToManyRelModelMapper::get()->where('id', 1)->execute(); - $this->deleteModelRelation(1, $model1->getId(), $model2->id, BaseModelMapper::class, 'hasManyRelations', '', '127.0.0.1'); + $this->deleteModelRelation(1, $model1->id, $model2->id, BaseModelMapper::class, 'hasManyRelations', '', '127.0.0.1'); } public function creates() : void @@ -246,7 +245,7 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase */ public function testFillJson() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $response = new HttpResponse(); $this->module->fillJson($request, $response, 'OK', 'Test Title', 'Test Message!', [1, 'test string', 'bool' => true]); @@ -269,7 +268,7 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase */ public function testFillJsonRaw() : void { - $request = new HttpRequest(new HttpUri('')); + $request = new HttpRequest(); $response = new HttpResponse(); $this->module->fillJsonRaw($request, $response, [1, 'test string', 'bool' => true]); @@ -285,21 +284,27 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase */ private function dbSetup() : void { + \phpOMS\Log\FileLogger::getInstance()->verbose = true; + $GLOBALS['dbpool']->get()->con->prepare( 'CREATE TABLE `test_base` ( `test_base_id` int(11) NOT NULL AUTO_INCREMENT, `test_base_string` varchar(254) NOT NULL, + `test_base_compress` BLOB NOT NULL, + `test_base_pstring` varchar(254) NOT NULL, `test_base_int` int(11) NOT NULL, `test_base_bool` tinyint(1) DEFAULT NULL, `test_base_null` int(11) DEFAULT NULL, `test_base_float` decimal(5, 4) DEFAULT NULL, `test_base_belongs_to_one` int(11) DEFAULT NULL, + `test_base_belongs_top_one` int(11) DEFAULT NULL, `test_base_owns_one_self` int(11) DEFAULT NULL, + `test_base_owns_onep_self` int(11) DEFAULT NULL, `test_base_json` varchar(254) DEFAULT NULL, `test_base_json_serializable` varchar(254) DEFAULT NULL, `test_base_serializable` varchar(254) DEFAULT NULL, `test_base_datetime` datetime DEFAULT NULL, - `test_base_datetime_null` datetime DEFAULT NULL, /* There was a bug where it returned the current date because new \DateTime(null) === current date which is wrong, we want null as value! */ + `test_base_datetime_null` datetime DEFAULT NULL, PRIMARY KEY (`test_base_id`) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' )->execute(); @@ -355,6 +360,49 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase PRIMARY KEY (`test_has_many_rel_relations_id`) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' )->execute(); + + // private + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_has_many_directp` ( + `test_has_many_directp_id` int(11) NOT NULL AUTO_INCREMENT, + `test_has_many_directp_string` varchar(254) NOT NULL, + `test_has_many_directp_to` int(11) NOT NULL, + PRIMARY KEY (`test_has_many_directp_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); + + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_has_many_relp` ( + `test_has_many_relp_id` int(11) NOT NULL AUTO_INCREMENT, + `test_has_many_relp_string` varchar(254) NOT NULL, + PRIMARY KEY (`test_has_many_relp_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); + + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_has_many_rel_relationsp` ( + `test_has_many_rel_relationsp_id` int(11) NOT NULL AUTO_INCREMENT, + `test_has_many_rel_relationsp_src` int(11) NOT NULL, + `test_has_many_rel_relationsp_dest` int(11) NOT NULL, + PRIMARY KEY (`test_has_many_rel_relationsp_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); + + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_belongs_to_onep` ( + `test_belongs_to_onep_id` int(11) NOT NULL AUTO_INCREMENT, + `test_belongs_to_onep_string` varchar(254) NOT NULL, + PRIMARY KEY (`test_belongs_to_onep_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); + + $GLOBALS['dbpool']->get()->con->prepare( + 'CREATE TABLE `test_owns_onep` ( + `test_owns_onep_id` int(11) NOT NULL AUTO_INCREMENT, + `test_owns_onep_string` varchar(254) NOT NULL, + PRIMARY KEY (`test_owns_onep_id`) + )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1;' + )->execute(); } /** @@ -369,6 +417,14 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_direct')->execute(); $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_rel')->execute(); $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_rel_relations')->execute(); + + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_directp')->execute(); + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_relp')->execute(); + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_has_many_rel_relationsp')->execute(); + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_belongs_to_onep')->execute(); + $GLOBALS['dbpool']->get()->con->prepare('DROP TABLE test_owns_onep')->execute(); + + \phpOMS\Log\FileLogger::getInstance()->verbose = false; } /** diff --git a/tests/Module/ModuleInfoTest.php b/tests/Module/ModuleInfoTest.php index 23964bfe3..0b749a749 100755 --- a/tests/Module/ModuleInfoTest.php +++ b/tests/Module/ModuleInfoTest.php @@ -112,7 +112,7 @@ final class ModuleInfoTest extends \PHPUnit\Framework\TestCase $info = new ModuleInfo(__DIR__ . '/info-test.json'); $info->load(); - $testObj = new class() { + $testObj = new class() { public $test = 1; public function test() : void diff --git a/tests/Module/ModuleManagerTest.php b/tests/Module/ModuleManagerTest.php index 60adacf9c..79776bf28 100755 --- a/tests/Module/ModuleManagerTest.php +++ b/tests/Module/ModuleManagerTest.php @@ -45,7 +45,7 @@ final class ModuleManagerTest extends \PHPUnit\Framework\TestCase */ protected function setUp() : void { - $this->app = new class() extends ApplicationAbstract { + $this->app = new class() extends ApplicationAbstract { protected string $appName = 'Api'; }; @@ -153,7 +153,7 @@ final class ModuleManagerTest extends \PHPUnit\Framework\TestCase self::assertTrue($this->moduleManager->activate('TestModule')); // this is normally done in the ApiController - $module->setStatus(ModuleStatus::ACTIVE); + $module->status = ModuleStatus::ACTIVE; ModuleMapper::update()->execute($module); $queryLoad = new Builder($this->app->dbPool->get('insert')); diff --git a/tests/Module/info-test.json b/tests/Module/info-test.json index daf851593..92998de85 100755 --- a/tests/Module/info-test.json +++ b/tests/Module/info-test.json @@ -14,7 +14,6 @@ "name": "Jingga", "website": "jingga.app" }, - "description": "The administration module.", "directory": "Admin", "dependencies": [], "providing": { diff --git a/tests/Router/SocketRouterTest.php b/tests/Router/SocketRouterTest.php index 72d7ebcdb..6d29465b9 100755 --- a/tests/Router/SocketRouterTest.php +++ b/tests/Router/SocketRouterTest.php @@ -108,7 +108,7 @@ final class SocketRouterTest extends \PHPUnit\Framework\TestCase */ public function testDynamicRouteAdding() : void { - $this->router->add('^.*backends_admin -settings=general.*$', 'Controller:test'); + $this->router->add('^.*backends_admin -settings=general(\?.*$|$)', 'Controller:test'); self::assertEquals( [['dest' => 'Controller:test']], $this->router->route('backends_admin -settings=general -t 123') @@ -219,7 +219,7 @@ final class SocketRouterTest extends \PHPUnit\Framework\TestCase public function testDataValidation() : void { $this->router->add( - '^.*backends_admin -settings=general.*$', + '^.*backends_admin -settings=general(\?.*$|$)', 'Controller:test', validation: ['test_pattern' => '/^[a-z]*$/'] ); @@ -238,7 +238,7 @@ final class SocketRouterTest extends \PHPUnit\Framework\TestCase public function testInvalidDataValidation() : void { $this->router->add( - '^.*backends_admin -settings=general.*$', + '^.*backends_admin -settings=general(\?.*$|$)', 'Controller:test', validation: ['test_pattern' => '/^[a-z]*$/'] ); @@ -257,7 +257,7 @@ final class SocketRouterTest extends \PHPUnit\Framework\TestCase public function testDataFromPattern() : void { $this->router->add( - '^.*-settings=general.*$', + '^.*-settings=general(\?.*$|$)', 'Controller:test', dataPattern: '/^.*?(settings)=([a-z]*).*?$/' ); diff --git a/tests/Router/WebRouterTest.php b/tests/Router/WebRouterTest.php index ebe3ac000..041d1bd03 100755 --- a/tests/Router/WebRouterTest.php +++ b/tests/Router/WebRouterTest.php @@ -53,7 +53,7 @@ final class WebRouterTest extends \PHPUnit\Framework\TestCase { self::assertEmpty( $this->router->route( - (new HttpRequest(new HttpUri('')))->uri->getRoute() + (new HttpRequest())->uri->getRoute() ) ); } @@ -142,7 +142,7 @@ final class WebRouterTest extends \PHPUnit\Framework\TestCase */ public function testDynamicRouteAdding() : void { - $this->router->add('^.*/backends/admin/settings/general.*$', 'Controller:test', RouteVerb::GET | RouteVerb::SET); + $this->router->add('^.*/backends/admin/settings/general(\?.*$|$)', 'Controller:test', RouteVerb::GET | RouteVerb::SET); self::assertEquals( [['dest' => 'Controller:test']], $this->router->route( @@ -318,7 +318,7 @@ final class WebRouterTest extends \PHPUnit\Framework\TestCase public function testDataValidation() : void { $this->router->add( - '^.*/backends/admin/settings/general.*$', + '^.*/backends/admin/settings/general(\?.*$|$)', 'Controller:test', RouteVerb::GET | RouteVerb::SET, false, @@ -335,14 +335,14 @@ final class WebRouterTest extends \PHPUnit\Framework\TestCase } /** - * @testdox A data validation pattern invalidates missmatches + * @testdox A data validation pattern invalidates miss-matches * @covers phpOMS\Router\WebRouter * @group framework */ public function testInvalidDataValidation() : void { $this->router->add( - '^.*/backends/admin/settings/general.*$', + '^.*/backends/admin/settings/general(\?.*$|$)', 'Controller:test', RouteVerb::GET | RouteVerb::SET, false, @@ -366,7 +366,7 @@ final class WebRouterTest extends \PHPUnit\Framework\TestCase public function testDataFromPattern() : void { $this->router->add( - '^.*/backends/admin.*$', + '^.*/backends/admin(\?.*$|$)', 'Controller:test', RouteVerb::GET | RouteVerb::SET, false, diff --git a/tests/Router/socketRouterTestFile.php b/tests/Router/socketRouterTestFile.php index 9fb5e48d0..3a4b22f4e 100755 --- a/tests/Router/socketRouterTestFile.php +++ b/tests/Router/socketRouterTestFile.php @@ -1,7 +1,7 @@ [ + '^.*backend_admin -settings=general(\?.*$|$)' => [ 0 => [ 'dest' => '\Modules\Admin\Controller:viewSettingsGeneral', ], diff --git a/tests/Router/socketRouterTestFilePermission.php b/tests/Router/socketRouterTestFilePermission.php index 359f24003..6e33a2e90 100755 --- a/tests/Router/socketRouterTestFilePermission.php +++ b/tests/Router/socketRouterTestFilePermission.php @@ -4,13 +4,13 @@ declare(strict_types=1); use phpOMS\Account\PermissionType; return [ - '^.*backend_admin -settings=general.*$' => [ + '^.*backend_admin -settings=general(\?.*$|$)' => [ 0 => [ 'dest' => '\Modules\Admin\Controller:viewSettingsGeneral', 'permission' => [ - 'module' => 'TEST', - 'type' => PermissionType::READ, - 'category' => 1, + 'module' => 'TEST', + 'type' => PermissionType::READ, + 'category' => 1, ], ], ], diff --git a/tests/Router/webRouteTestCsrf.php b/tests/Router/webRouteTestCsrf.php index 693807819..43edd600e 100755 --- a/tests/Router/webRouteTestCsrf.php +++ b/tests/Router/webRouteTestCsrf.php @@ -1,7 +1,7 @@ [ + '^.*/backend/admin/settings/csrf(\?.*$|$)' => [ 0 => [ 'dest' => '\Modules\Admin\Controller:viewCsrf', 'verb' => 1, diff --git a/tests/Router/webRouterTestFile.php b/tests/Router/webRouterTestFile.php index a6945522a..7f4fb925c 100755 --- a/tests/Router/webRouterTestFile.php +++ b/tests/Router/webRouterTestFile.php @@ -1,7 +1,7 @@ [ + '^.*/backend/admin/settings/general(\?.*$|$)' => [ 0 => [ 'dest' => '\Modules\Admin\Controller:viewSettingsGeneral', 'verb' => 1, diff --git a/tests/Router/webRouterTestFilePermission.php b/tests/Router/webRouterTestFilePermission.php index b44c375c5..5441e72bf 100755 --- a/tests/Router/webRouterTestFilePermission.php +++ b/tests/Router/webRouterTestFilePermission.php @@ -5,14 +5,14 @@ use phpOMS\Account\PermissionType; use phpOMS\Router\RouteVerb; return [ - '^.*/backend/admin/settings/general.*$' => [ + '^.*/backend/admin/settings/general(\?.*$|$)' => [ 0 => [ 'dest' => '\Modules\Admin\Controller:viewSettingsGeneral', 'verb' => RouteVerb::GET, 'permission' => [ - 'module' => 'TEST', - 'type' => PermissionType::READ, - 'category' => 1, + 'module' => 'TEST', + 'type' => PermissionType::READ, + 'category' => 1, ], ], ], diff --git a/tests/Security/EncryptionHelperTest.php b/tests/Security/EncryptionHelperTest.php index c3c82bc05..017dbb862 100644 --- a/tests/Security/EncryptionHelperTest.php +++ b/tests/Security/EncryptionHelperTest.php @@ -49,8 +49,8 @@ final class EncryptionHelperTest extends \PHPUnit\Framework\TestCase public function testFileEncryption() : void { - if (\is_file(__DIR__ . '/encrytped.txt')) { - \unlink(__DIR__ . '/encrytped.txt'); + if (\is_file(__DIR__ . '/encrypted.txt')) { + \unlink(__DIR__ . '/encrypted.txt'); } if (\is_file(__DIR__ . '/decrypted.txt')) { @@ -58,22 +58,22 @@ final class EncryptionHelperTest extends \PHPUnit\Framework\TestCase } $key = EncryptionHelper::createSharedKey(); - self::assertTrue(EncryptionHelper::encryptFile(__DIR__ . '/plain.txt', __DIR__ . '/encrytped.txt', $key)); + self::assertTrue(EncryptionHelper::encryptFile(__DIR__ . '/plain.txt', __DIR__ . '/encrypted.txt', $key)); self::assertNotEquals( \file_get_contents(__DIR__ . '/plain.txt'), - \file_get_contents(__DIR__ . '/encrytped.txt') + \file_get_contents(__DIR__ . '/encrypted.txt') ); - self::assertTrue(EncryptionHelper::decryptFile(__DIR__ . '/encrytped.txt', __DIR__ . '/decrypted.txt', $key)); + self::assertTrue(EncryptionHelper::decryptFile(__DIR__ . '/encrypted.txt', __DIR__ . '/decrypted.txt', $key)); self::assertEquals( \file_get_contents(__DIR__ . '/plain.txt'), \file_get_contents(__DIR__ . '/decrypted.txt') ); - if (\is_file(__DIR__ . '/encrytped.txt')) { - \unlink(__DIR__ . '/encrytped.txt'); + if (\is_file(__DIR__ . '/encrypted.txt')) { + \unlink(__DIR__ . '/encrypted.txt'); } if (\is_file(__DIR__ . '/decrypted.txt')) { diff --git a/tests/Security/GuardTest.php b/tests/Security/GuardTest.php index 89b80078f..8bac47439 100644 --- a/tests/Security/GuardTest.php +++ b/tests/Security/GuardTest.php @@ -56,4 +56,16 @@ final class GuardTest extends \PHPUnit\Framework\TestCase ) ); } + + /** + * @testdox A string can be validated for shell safety + * @covers phpOMS\Security\Guard + * @group framework + */ + public function testIsShellSafe() : void + { + self::assertTrue(Guard::isShellSafe('asdf')); + self::assertFalse(Guard::isShellSafe('&#;`|*?~<>^()[]{}$\\')); + self::assertFalse(Guard::isShellSafe('™')); + } } diff --git a/tests/Socket/Client/ClientTest.php b/tests/Socket/Client/ClientTest.php index 719c3cfc1..c850dee93 100755 --- a/tests/Socket/Client/ClientTest.php +++ b/tests/Socket/Client/ClientTest.php @@ -59,18 +59,18 @@ final class ClientTest extends \PHPUnit\Framework\TestCase protected string $appName = 'Socket'; }; - $this->app->logger = new FileLogger(__DIR__ . '/client.log', false); - $this->app->dbPool = $GLOBALS['dbpool']; - $this->app->unitId = 1; - $this->app->cachePool = new CachePool($this->app->dbPool); - $this->app->accountManager = new AccountManager($GLOBALS['session']); - $this->app->appSettings = new CoreSettings(); - $this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../../../Modules/'); - $this->app->dispatcher = new Dispatcher($this->app); - $this->app->eventManager = new EventManager($this->app->dispatcher); + $this->app->logger = new FileLogger(__DIR__ . '/client.log', false); + $this->app->dbPool = $GLOBALS['dbpool']; + $this->app->unitId = 1; + $this->app->cachePool = new CachePool($this->app->dbPool); + $this->app->accountManager = new AccountManager($GLOBALS['session']); + $this->app->appSettings = new CoreSettings(); + $this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../../../Modules/'); + $this->app->dispatcher = new Dispatcher($this->app); + $this->app->eventManager = new EventManager($this->app->dispatcher); $this->app->eventManager->importFromFile(__DIR__ . '/../../../Socket/Hooks.php'); - $this->app->l11nManager = new L11nManager(); - $this->app->router = new SocketRouter(); + $this->app->l11nManager = new L11nManager(); + $this->app->router = new SocketRouter(); } protected function tearDown() : void diff --git a/tests/Socket/Client/ClientTestHelper.php b/tests/Socket/Client/ClientTestHelper.php index 84700ab90..1728af467 100755 --- a/tests/Socket/Client/ClientTestHelper.php +++ b/tests/Socket/Client/ClientTestHelper.php @@ -49,18 +49,18 @@ $app = new class() extends ApplicationAbstract protected string $appName = 'Socket'; }; -$app->logger = FileLogger::getInstance(__DIR__ . '/server.log', true); -$app->dbPool = $GLOBALS['dbpool']; -$app->unitId = 1; -$app->cachePool = new CachePool($app->dbPool); -$app->accountManager = new AccountManager($GLOBALS['session']); -$app->appSettings = new CoreSettings($app->dbPool->get()); -$app->moduleManager = new ModuleManager($app, __DIR__ . '/../../../../Modules/'); -$app->dispatcher = new Dispatcher($app); -$app->eventManager = new EventManager($app->dispatcher); +$app->logger = FileLogger::getInstance(__DIR__ . '/server.log', true); +$app->dbPool = $GLOBALS['dbpool']; +$app->unitId = 1; +$app->cachePool = new CachePool($app->dbPool); +$app->accountManager = new AccountManager($GLOBALS['session']); +$app->appSettings = new CoreSettings($app->dbPool->get()); +$app->moduleManager = new ModuleManager($app, __DIR__ . '/../../../../Modules/'); +$app->dispatcher = new Dispatcher($app); +$app->eventManager = new EventManager($app->dispatcher); $app->eventManager->importFromFile(__DIR__ . '/../../../Socket/Hooks.php'); -$app->l11nManager = new L11nManager(); -$app->router = new SocketRouter(); +$app->l11nManager = new L11nManager(); +$app->router = new SocketRouter(); $socket = new Server($app); $socket->create('127.0.0.1', $config['socket']['master']['port']); diff --git a/tests/Socket/Server/ServerTest.php b/tests/Socket/Server/ServerTest.php index 247415a4e..a44349699 100755 --- a/tests/Socket/Server/ServerTest.php +++ b/tests/Socket/Server/ServerTest.php @@ -59,18 +59,18 @@ final class ServerTest extends \PHPUnit\Framework\TestCase protected string $appName = 'Socket'; }; - $this->app->logger = new FileLogger(__DIR__ . '/server.log', false); - $this->app->dbPool = $GLOBALS['dbpool']; - $this->app->unitId = 1; - $this->app->cachePool = new CachePool($this->app->dbPool); - $this->app->accountManager = new AccountManager($GLOBALS['session']); - $this->app->appSettings = new CoreSettings(); - $this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../../../Modules/'); - $this->app->dispatcher = new Dispatcher($this->app); - $this->app->eventManager = new EventManager($this->app->dispatcher); + $this->app->logger = new FileLogger(__DIR__ . '/server.log', false); + $this->app->dbPool = $GLOBALS['dbpool']; + $this->app->unitId = 1; + $this->app->cachePool = new CachePool($this->app->dbPool); + $this->app->accountManager = new AccountManager($GLOBALS['session']); + $this->app->appSettings = new CoreSettings(); + $this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../../../Modules/'); + $this->app->dispatcher = new Dispatcher($this->app); + $this->app->eventManager = new EventManager($this->app->dispatcher); $this->app->eventManager->importFromFile(__DIR__ . '/../../../Socket/Hooks.php'); - $this->app->l11nManager = new L11nManager(); - $this->app->router = new SocketRouter(); + $this->app->l11nManager = new L11nManager(); + $this->app->router = new SocketRouter(); } protected function tearDown() : void diff --git a/tests/Stdlib/Base/AddressTypeTest.php b/tests/Stdlib/Base/AddressTypeTest.php index f1e89e61e..931513cf3 100755 --- a/tests/Stdlib/Base/AddressTypeTest.php +++ b/tests/Stdlib/Base/AddressTypeTest.php @@ -27,7 +27,7 @@ final class AddressTypeTest extends \PHPUnit\Framework\TestCase */ public function testEnumCount() : void { - self::assertCount(8, AddressType::getconstants()); + self::assertCount(8, AddressType::getConstants()); } /** @@ -50,7 +50,6 @@ final class AddressTypeTest extends \PHPUnit\Framework\TestCase self::assertEquals(3, AddressType::SHIPPING); self::assertEquals(4, AddressType::BILLING); self::assertEquals(5, AddressType::WORK); - self::assertEquals(6, AddressType::CONTRACT); self::assertEquals(7, AddressType::OTHER); self::assertEquals(8, AddressType::EDUCATION); } diff --git a/tests/Stdlib/Base/LocationTest.php b/tests/Stdlib/Base/LocationTest.php index 08bd21eb1..d5e4dd2f3 100755 --- a/tests/Stdlib/Base/LocationTest.php +++ b/tests/Stdlib/Base/LocationTest.php @@ -53,11 +53,11 @@ final class LocationTest extends \PHPUnit\Framework\TestCase self::assertEquals('', $this->location->postal); self::assertEquals('', $this->location->city); - self::assertEquals('XX', $this->location->getCountry()); + self::assertEquals('XX', $this->location->country); self::assertEquals('', $this->location->address); self::assertEquals('', $this->location->state); - self::assertEquals(0, $this->location->getId()); - self::assertEquals(AddressType::HOME, $this->location->getType()); + self::assertEquals(0, $this->location->id); + self::assertEquals(AddressType::HOME, $this->location->type); self::assertEquals($expected, $this->location->toArray()); self::assertEquals($expected, $this->location->jsonSerialize()); } @@ -73,17 +73,6 @@ final class LocationTest extends \PHPUnit\Framework\TestCase self::assertEquals('0123456789', $this->location->postal); } - /** - * @testdox The type can be set and returned - * @covers phpOMS\Stdlib\Base\Location - * @group framework - */ - public function testTypeInputOutput() : void - { - $this->location->setType(AddressType::BUSINESS); - self::assertEquals(AddressType::BUSINESS, $this->location->getType()); - } - /** * @testdox The city can be set and returned * @covers phpOMS\Stdlib\Base\Location @@ -103,7 +92,7 @@ final class LocationTest extends \PHPUnit\Framework\TestCase public function testCountryInputOutput() : void { $this->location->setCountry('Country'); - self::assertEquals('Country', $this->location->getCountry()); + self::assertEquals('Country', $this->location->country); } /** @@ -145,8 +134,8 @@ final class LocationTest extends \PHPUnit\Framework\TestCase 'lon' => 11.2, ]; - $this->location->postal = '0123456789'; - $this->location->setType(AddressType::BUSINESS); + $this->location->postal = '0123456789'; + $this->location->type = AddressType::BUSINESS; $this->location->city = 'city'; $this->location->address = 'Some address here'; $this->location->state = 'This is a state 123'; @@ -174,8 +163,8 @@ final class LocationTest extends \PHPUnit\Framework\TestCase 'lon' => 11.2, ]; - $this->location->postal = '0123456789'; - $this->location->setType(AddressType::BUSINESS); + $this->location->postal = '0123456789'; + $this->location->type = AddressType::BUSINESS; $this->location->city = 'city'; $this->location->address = 'Some address here'; $this->location->state = 'This is a state 123'; diff --git a/tests/Stdlib/Graph/NodeTest.php b/tests/Stdlib/Graph/NodeTest.php index 953f19610..a399d7706 100755 --- a/tests/Stdlib/Graph/NodeTest.php +++ b/tests/Stdlib/Graph/NodeTest.php @@ -143,7 +143,8 @@ final class NodeTest extends \PHPUnit\Framework\TestCase * @covers phpOMS\Stdlib\Graph\Node * @group framework * - * @todo: is there bug where directed graphs return invalid neighbors? + * @bug Directed graphs may return invalid neighbors + * https://github.com/Karaka-Management/phpOMS/issues/366 */ public function testNeighborsInputOutput() : void { diff --git a/tests/Stdlib/Tree/BinarySearchTreeTest.php b/tests/Stdlib/Tree/BinarySearchTreeTest.php new file mode 100644 index 000000000..73276be49 --- /dev/null +++ b/tests/Stdlib/Tree/BinarySearchTreeTest.php @@ -0,0 +1,149 @@ +insert(new Node('D', 'D')); + $bst->insert(new Node('I', 'I')); + $bst->insert(new Node('N', 'N')); + $bst->insert(new Node('O', 'O')); + $bst->insert(new Node('S', 'S')); + $bst->insert(new Node('A', 'A')); + $bst->insert(new Node('U', 'U')); + $bst->insert(new Node('R', 'R')); + + self::assertEquals( + [ + 'key' => 'D', + 0 => [ + 'key' => 'A', + 0 => null, + 1 => null, + ], + 1 => [ + 'key' => 'I', + 0 => null, + 1 => [ + 'key' => 'N', + 0 => null, + 1 => [ + 'key' => 'O', + 0 => null, + 1 => [ + 'key' => 'S', + 0 => [ + 'key' => 'R', + 0 => null, + 1 => null, + ], + 1 => [ + 'key' => 'U', + 0 => null, + 1 => null, + ], + ], + ], + ], + ], + ], + $bst->toArray() + ); + } + + public function testSearch() : void + { + $bst = new BinarySearchTree(); + $bst->insert(new Node('D', 'D')); + $bst->insert(new Node('I', 'I')); + $bst->insert(new Node('N', 'N')); + $bst->insert(new Node('O', 'O')); + $bst->insert(new Node('S', 'S')); + $bst->insert(new Node('A', 'A')); + $bst->insert(new Node('U', 'U')); + $bst->insert(new Node('R', 'R')); + + self::assertEquals('S', $bst->search('S')->key); + self::assertEquals('U', $bst->search('U')->key); + self::assertEquals('R', $bst->search('R')->key); + } + + public function testDelete() : void + { + $bst = new BinarySearchTree(); + $bst->insert(new Node('D', 'D')); + $bst->insert(new Node('I', 'I')); + $bst->insert(new Node('N', 'N')); + $bst->insert(new Node('O', 'O')); + $bst->insert(new Node('S', 'S')); + $bst->insert(new Node('A', 'A')); + $bst->insert(new Node('U', 'U')); + $bst->insert(new Node('R', 'R')); + $bst->delete($bst->search('I')); + $bst->insert(new Node('Z', 'Z')); + $bst->delete($bst->search('S')); + $bst->insert(new Node('T', 'T')); + + self::assertEquals( + [ + 'key' => 'D', + 0 => [ + 'key' => 'A', + 0 => null, + 1 => null, + ], + 1 => [ + 'key' => 'N', + 0 => null, + 1 => [ + 'key' => 'O', + 0 => null, + 1 => [ + 'key' => 'U', + 0 => [ + 'key' => 'R', + 0 => null, + 1 => [ + 'key' => 'T', + 0 => null, + 1 => null, + ], + ], + 1 => [ + 'key' => 'Z', + 0 => null, + 1 => null, + ], + ], + ], + ], + ], + $bst->toArray() + ); + } +} diff --git a/tests/System/File/Local/FileTest.php b/tests/System/File/Local/FileTest.php index b7b2fd361..47a0b6c23 100755 --- a/tests/System/File/Local/FileTest.php +++ b/tests/System/File/Local/FileTest.php @@ -562,7 +562,7 @@ final class FileTest extends \PHPUnit\Framework\TestCase \unlink($testFile); } - $newPath = __DIR__ . '/test2.txt'; + $newPath = __DIR__ . '/test2.txt'; if (\is_file($newPath)) { \unlink($newPath); } diff --git a/tests/System/SystemUtilsTest.php b/tests/System/SystemUtilsTest.php index c9edcaf14..61750bee2 100755 --- a/tests/System/SystemUtilsTest.php +++ b/tests/System/SystemUtilsTest.php @@ -52,4 +52,9 @@ final class SystemUtilsTest extends \PHPUnit\Framework\TestCase { self::assertGreaterThan(0, SystemUtils::getCpuUsage()); } + + public function testHostname() : void + { + self::assertGreaterThan(0, \strlen(SystemUtils::getHostname())); + } } diff --git a/tests/Utils/Converter/EnergyPowerTypeTest.php b/tests/Utils/Converter/EnergyPowerTypeTest.php index 708e36149..e8b3106e0 100755 --- a/tests/Utils/Converter/EnergyPowerTypeTest.php +++ b/tests/Utils/Converter/EnergyPowerTypeTest.php @@ -45,13 +45,13 @@ final class EnergyPowerTypeTest extends \PHPUnit\Framework\TestCase */ public function testEnums() : void { - self::assertEquals('kWh', EnergyPowerType::KILOWATT_HOUERS); - self::assertEquals('MWh', EnergyPowerType::MEGAWATT_HOUERS); + self::assertEquals('kWh', EnergyPowerType::KILOWATT_HOURS); + self::assertEquals('MWh', EnergyPowerType::MEGAWATT_HOURS); self::assertEquals('kt', EnergyPowerType::KILOTONS); - self::assertEquals('J', EnergyPowerType::JOULS); + self::assertEquals('J', EnergyPowerType::JOULES); self::assertEquals('Cal', EnergyPowerType::CALORIES); self::assertEquals('BTU', EnergyPowerType::BTU); - self::assertEquals('kJ', EnergyPowerType::KILOJOULS); + self::assertEquals('kJ', EnergyPowerType::KILOJOULES); self::assertEquals('thmEC', EnergyPowerType::THERMEC); self::assertEquals('Nm', EnergyPowerType::NEWTON_METERS); } diff --git a/tests/Utils/Converter/MeasurementTest.php b/tests/Utils/Converter/MeasurementTest.php index 9523eca04..5b49243cf 100755 --- a/tests/Utils/Converter/MeasurementTest.php +++ b/tests/Utils/Converter/MeasurementTest.php @@ -245,7 +245,7 @@ final class MeasurementTest extends \PHPUnit\Framework\TestCase } /** - * @testdox Filesizes can be converted + * @testdox File sizes can be converted * @covers phpOMS\Utils\Converter\Measurement * @group framework */ @@ -490,7 +490,7 @@ final class MeasurementTest extends \PHPUnit\Framework\TestCase { $this->expectException(\InvalidArgumentException::class); - Measurement::convertEnergy(1.1, 'invalid', EnergyPowerType::JOULS); + Measurement::convertEnergy(1.1, 'invalid', EnergyPowerType::JOULES); } /** @@ -502,7 +502,7 @@ final class MeasurementTest extends \PHPUnit\Framework\TestCase { $this->expectException(\InvalidArgumentException::class); - Measurement::convertEnergy(1.1, EnergyPowerType::JOULS, 'invalid'); + Measurement::convertEnergy(1.1, EnergyPowerType::JOULES, 'invalid'); } /** diff --git a/tests/Utils/IO/Spreadsheet/SpreadsheetDatabaseMapperTest.php b/tests/Utils/IO/Spreadsheet/SpreadsheetDatabaseMapperTest.php index b30db62f7..032ed1ab9 100755 --- a/tests/Utils/IO/Spreadsheet/SpreadsheetDatabaseMapperTest.php +++ b/tests/Utils/IO/Spreadsheet/SpreadsheetDatabaseMapperTest.php @@ -14,9 +14,9 @@ declare(strict_types=1); namespace phpOMS\tests\Utils\IO\Spreadsheet; -use phpOMS\tests\Autoloader; use phpOMS\DataStorage\Database\Connection\SQLiteConnection; use phpOMS\DataStorage\Database\Query\Builder; +use phpOMS\tests\Autoloader; use phpOMS\Utils\IO\Spreadsheet\SpreadsheetDatabaseMapper; use phpOMS\Utils\StringUtils; diff --git a/tests/Utils/ImageUtilsTest.php b/tests/Utils/ImageUtilsTest.php index 84eed2dc2..7d4aec704 100755 --- a/tests/Utils/ImageUtilsTest.php +++ b/tests/Utils/ImageUtilsTest.php @@ -39,16 +39,18 @@ final class ImageUtilsTest extends \PHPUnit\Framework\TestCase public function testResize() : void { - ImageUtils::resize(__DIR__ . '/logo.png', __DIR__ . '/logo_resized.png', 256, 256); - self::assertTrue(\is_file(__DIR__ . '/logo_resized.png')); + ImageUtils::resize(__DIR__ . '/img/logo.png', __DIR__ . '/img/logo_resized.png', 256, 256); + self::assertTrue(\is_file(__DIR__ . '/img/logo_resized.png')); } public function testDifference() : void { - $diff = ImageUtils::difference(__DIR__ . '/img1.png', __DIR__ . '/img2.png', __DIR__ . '/diff1.png', 0); - $diff = ImageUtils::difference(__DIR__ . '/img1.png', __DIR__ . '/img2.png', __DIR__ . '/diff2.png', 1); + foreach (['png', 'gif'] as $type) { + $diff = ImageUtils::difference(__DIR__ . '/img/img1.' . $type, __DIR__ . '/img/img2.' . $type, __DIR__ . '/img/diff1.' . $type, 0); + $diff = ImageUtils::difference(__DIR__ . '/img/img1.' . $type, __DIR__ . '/img/img2.' . $type, __DIR__ . '/img/diff2.' . $type, 1); - self::assertGreaterThan(0, $diff); + self::assertGreaterThan(0, $diff); + } } /** @@ -59,7 +61,7 @@ final class ImageUtilsTest extends \PHPUnit\Framework\TestCase public function testImage() : void { self::assertEquals( - \file_get_contents(__DIR__ . '/logo.png'), + \file_get_contents(__DIR__ . '/img/logo.png'), ImageUtils::decodeBase64Image( 'data:image/png;base64,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' ) diff --git a/tests/Utils/Parser/Calendar/ICalParserTest.php b/tests/Utils/Parser/Calendar/ICalParserTest.php new file mode 100644 index 000000000..64ddafac2 --- /dev/null +++ b/tests/Utils/Parser/Calendar/ICalParserTest.php @@ -0,0 +1,42 @@ +toPdfString(__DIR__ . '/data/WordMpdf.pdf'); + self::assertFalse(\is_file(__DIR__ . '/data/WordMpdf.pdf')); + + \file_put_contents(__DIR__ . '/data/WordMpdf.pdf', $pdf); + self::assertTrue(\is_file(__DIR__ . '/data/WordMpdf.pdf')); + self::assertGreaterThan(100, \strlen(\file_get_contents(__DIR__ . '/data/WordMpdf.pdf'))); + } +} diff --git a/tests/Utils/Parser/Document/data/Word.docx b/tests/Utils/Parser/Document/data/Word.docx new file mode 100644 index 000000000..811baba61 Binary files /dev/null and b/tests/Utils/Parser/Document/data/Word.docx differ diff --git a/tests/Utils/Parser/Document/data/Word.html b/tests/Utils/Parser/Document/data/Word.html new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Utils/Parser/Markdown/MarkdownTest.php b/tests/Utils/Parser/Markdown/MarkdownTest.php index 87fc643fb..50db61079 100755 --- a/tests/Utils/Parser/Markdown/MarkdownTest.php +++ b/tests/Utils/Parser/Markdown/MarkdownTest.php @@ -47,9 +47,142 @@ final class MarkdownTest extends \PHPUnit\Framework\TestCase public function testSafeMode() : void { - $parser = new Markdown(); - $parser->setSafeMode(true); + $parser = new Markdown(); + $parser->safeMode = true; - self::assertTrue(\file_get_contents(__DIR__ . '/manualdata/xss_bad_url.html') === ($parsed = $parser->text(\file_get_contents(__DIR__ . '/manualdata/xss_bad_url.md'))), $parsed); + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/xss_bad_url.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/xss_bad_url.md')) + ); + } + + public function testTablespan() : void + { + $parser = new Markdown([ + 'tables' => [ + 'tablespan' => true, + ], + ]); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/tablespan.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/tablespan.md')) + ); + } + + public function testMap() : void + { + $parser = new Markdown([ + 'map' => true, + ]); + + self::assertLessThan(9, + \levenshtein( + \file_get_contents(__DIR__ . '/manualdata/map.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/map.md')) + ) + ); + } + + public function testContact() : void + { + $parser = new Markdown([ + 'contact' => true, + ]); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/contact.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/contact.md')) + ); + } + + public function testTypographer() : void + { + $parser = new Markdown([ + 'typographer' => true, + ]); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/typographer.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/typographer.md')) + ); + } + + public function testAddress() : void + { + $parser = new Markdown([ + 'address' => true, + ]); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/address.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/address.md')) + ); + } + + public function testProgress() : void + { + $parser = new Markdown([ + 'progress' => true, + ]); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/progress.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/progress.md')) + ); + } + + public function testEmbed() : void + { + $parser = new Markdown([ + 'embeding' => true, + ]); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/embed.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/embed.md')) + ); + } + + public function testMath() : void + { + $parser = new Markdown([ + 'math' => true, + ]); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/katex.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/katex.md')) + ); + } + + public function testTOC() : void + { + $parser = new Markdown([ + 'toc' => true, + ]); + $parser->text(\file_get_contents(__DIR__ . '/manualdata/toc.md')); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/toc.html'), + $parser->contentsList() + ); + } + + public function testSpoiler() : void + { + $parser = new Markdown([ + 'spoiler' => true, + ]); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/spoiler.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/spoiler.md')) + ); + + self::assertEquals( + \file_get_contents(__DIR__ . '/manualdata/spoiler_block.html'), + $parser->text(\file_get_contents(__DIR__ . '/manualdata/spoiler_block.md')) + ); } } diff --git a/tests/Utils/Parser/Markdown/data/chartjs.html b/tests/Utils/Parser/Markdown/data/chartjs.html new file mode 100644 index 000000000..c173ca4d7 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/chartjs.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/chartjs.md b/tests/Utils/Parser/Markdown/data/chartjs.md new file mode 100644 index 000000000..6ff76ccd9 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/chartjs.md @@ -0,0 +1,2 @@ +```chart +``` \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/checkbox.html b/tests/Utils/Parser/Markdown/data/checkbox.html new file mode 100644 index 000000000..2fe532fb7 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/checkbox.html @@ -0,0 +1,4 @@ +
            +
          • Unchecked
          • +
          • Checked
          • +
          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/checkbox.md b/tests/Utils/Parser/Markdown/data/checkbox.md new file mode 100644 index 000000000..1949d401f --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/checkbox.md @@ -0,0 +1,2 @@ +- [ ] Unchecked +- [x] Checked \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/em_strong.html b/tests/Utils/Parser/Markdown/data/em_strong.html index c2cddc0fc..d16606cd8 100755 --- a/tests/Utils/Parser/Markdown/data/em_strong.html +++ b/tests/Utils/Parser/Markdown/data/em_strong.html @@ -1,8 +1,4 @@ -

          em strong

          -

          em strong strong

          -

          strong em strong

          -

          strong em strong strong

          -

          em strong

          -

          em strong strong

          -

          strong em strong

          -

          strong em strong strong

          \ No newline at end of file +

          em underline

          +

          underline em

          +

          strong em

          +

          strong em

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/em_strong.md b/tests/Utils/Parser/Markdown/data/em_strong.md index 9abeb3fd4..224cf308d 100755 --- a/tests/Utils/Parser/Markdown/data/em_strong.md +++ b/tests/Utils/Parser/Markdown/data/em_strong.md @@ -1,15 +1,7 @@ -___em strong___ +___em underline___ -___em strong_ strong__ +__*underline em*__ -__strong _em strong___ +***strong em*** -__strong _em strong_ strong__ - -***em strong*** - -***em strong* strong** - -**strong *em strong*** - -**strong *em strong* strong** \ No newline at end of file +**_strong em_** \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/emoji.html b/tests/Utils/Parser/Markdown/data/emoji.html new file mode 100644 index 000000000..d4de04c55 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/emoji.html @@ -0,0 +1 @@ +

          📹

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/emoji.md b/tests/Utils/Parser/Markdown/data/emoji.md new file mode 100644 index 000000000..914af9824 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/emoji.md @@ -0,0 +1 @@ +:video_camera: \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/emphasis.html b/tests/Utils/Parser/Markdown/data/emphasis.html index 60ff4bd8b..4571c85f8 100755 --- a/tests/Utils/Parser/Markdown/data/emphasis.html +++ b/tests/Utils/Parser/Markdown/data/emphasis.html @@ -1,4 +1,4 @@ -

          underscore, asterisk, one two, three four, a, b

          +

          underscore, asterisk, one two, three four, a, b

          strong and em and strong and em

          line line diff --git a/tests/Utils/Parser/Markdown/data/emphasis.md b/tests/Utils/Parser/Markdown/data/emphasis.md index 85b9d2299..99073ae5c 100755 --- a/tests/Utils/Parser/Markdown/data/emphasis.md +++ b/tests/Utils/Parser/Markdown/data/emphasis.md @@ -1,10 +1,10 @@ -_underscore_, *asterisk*, _one two_, *three four*, _a_, *b* +__underscore__, *asterisk*, _one two_, *three four*, _a_, *b* **strong** and *em* and **strong** and *em* -_line +*line line -line_ +line* this_is_not_an_emphasis diff --git a/tests/Utils/Parser/Markdown/data/keystroke.html b/tests/Utils/Parser/Markdown/data/keystroke.html new file mode 100644 index 000000000..8e6d5ca4d --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/keystroke.html @@ -0,0 +1 @@ +

          ctrl + shift + A

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/keystroke.md b/tests/Utils/Parser/Markdown/data/keystroke.md new file mode 100644 index 000000000..281e60490 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/keystroke.md @@ -0,0 +1 @@ +[[ctrl]] + [[shift]] + [[A]] \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/mark.html b/tests/Utils/Parser/Markdown/data/mark.html new file mode 100644 index 000000000..2b6624130 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/mark.html @@ -0,0 +1 @@ +

          Text with Mark test inline.

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/mark.md b/tests/Utils/Parser/Markdown/data/mark.md new file mode 100644 index 000000000..efefef266 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/mark.md @@ -0,0 +1 @@ +Text with ==Mark test== inline. \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/mermaidjs.html b/tests/Utils/Parser/Markdown/data/mermaidjs.html new file mode 100644 index 000000000..a49f1d1b3 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/mermaidjs.html @@ -0,0 +1,11 @@ +
          sequenceDiagram + participant Alice + participant Bob + Alice->>John: Hello John, how are you? + loop Healthcheck + John->>John: Fight against hypochondria + end + Note right of John: Rational thoughts<br/>prevail... + John-->>Alice: Great! + John->>Bob: How about you? + Bob-->>John: Jolly good!
          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/data/mermaidjs.md b/tests/Utils/Parser/Markdown/data/mermaidjs.md new file mode 100644 index 000000000..b9c32ff04 --- /dev/null +++ b/tests/Utils/Parser/Markdown/data/mermaidjs.md @@ -0,0 +1,13 @@ +```mermaid +sequenceDiagram + participant Alice + participant Bob + Alice->>John: Hello John, how are you? + loop Healthcheck + John->>John: Fight against hypochondria + end + Note right of John: Rational thoughts
          prevail... + John-->>Alice: Great! + John->>Bob: How about you? + Bob-->>John: Jolly good! +``` \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/address.html b/tests/Utils/Parser/Markdown/manualdata/address.html new file mode 100644 index 000000000..7a6a79b3b --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/address.html @@ -0,0 +1,7 @@ +

          +AddrName +Addr +AddrZip +AddrCity +AddrCoutry +

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/address.md b/tests/Utils/Parser/Markdown/manualdata/address.md new file mode 100644 index 000000000..ed1c1e308 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/address.md @@ -0,0 +1 @@ +[addr name="AddrName" address="Addr" city="AddrCity" country="AddrCoutry" zip="AddrZip"] \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/contact.html b/tests/Utils/Parser/Markdown/manualdata/contact.html new file mode 100644 index 000000000..92760f913 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/contact.html @@ -0,0 +1,36 @@ +

          + ++49 123 456 789 + + + +@jinggaApp + + + +jingga + + + +test@email.com + + + +@jinggaApp + + + +@jinggaApp + + + +jinggaApp + + + +jinggaApp + + + +jinggaApp +

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/contact.md b/tests/Utils/Parser/Markdown/manualdata/contact.md new file mode 100644 index 000000000..3b56536b9 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/contact.md @@ -0,0 +1,9 @@ +[contact phone="+49 123 456 789"] +[contact facebook="@jinggaApp"] +[contact discord="jingga"] +[contact email="test@email.com"] +[contact twitter="@jinggaApp"] +[contact youtube="@jinggaApp"] +[contact instagram="jinggaApp"] +[contact slack="jinggaApp"] +[contact teams="jinggaApp"] \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/embed.html b/tests/Utils/Parser/Markdown/manualdata/embed.html new file mode 100644 index 000000000..d30d6762c --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/embed.html @@ -0,0 +1,5 @@ +

          + + + +

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/embed.md b/tests/Utils/Parser/Markdown/manualdata/embed.md new file mode 100644 index 000000000..58e47889a --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/embed.md @@ -0,0 +1,5 @@ +[video src="https://www.youtube.com/watch?v=dQw4w9WgXcQ&ab_channel=RickAstley"] +[video src="https://vimeo.com/874474957"] +[video src="https://www.dailymotion.com/video/x3w7rss"] +[video src="test.mp4"] +[audio src="test.mp3"] \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/katex.html b/tests/Utils/Parser/Markdown/manualdata/katex.html new file mode 100644 index 000000000..4df09dc60 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/katex.html @@ -0,0 +1,2 @@ +$$ + x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/katex.md b/tests/Utils/Parser/Markdown/manualdata/katex.md new file mode 100644 index 000000000..2c583e1c0 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/katex.md @@ -0,0 +1,3 @@ +$$ + x = {-b \pm \sqrt{b^2-4ac} \over 2a}. +$$ \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/map.html b/tests/Utils/Parser/Markdown/manualdata/map.html new file mode 100644 index 000000000..c6f54ef15 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/map.html @@ -0,0 +1 @@ +

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/map.md b/tests/Utils/Parser/Markdown/manualdata/map.md new file mode 100644 index 000000000..a7963e89a --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/map.md @@ -0,0 +1 @@ +[map lat="1.0" lon="1.0"] \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/progress.html b/tests/Utils/Parser/Markdown/manualdata/progress.html new file mode 100644 index 000000000..24ae75383 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/progress.html @@ -0,0 +1 @@ +

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/progress.md b/tests/Utils/Parser/Markdown/manualdata/progress.md new file mode 100644 index 000000000..579e077e5 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/progress.md @@ -0,0 +1 @@ +[progress value="33"] \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/spoiler.html b/tests/Utils/Parser/Markdown/manualdata/spoiler.html new file mode 100644 index 000000000..2085a4d8e --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/spoiler.html @@ -0,0 +1,4 @@ +

          This is a + +test spoiler + in text.

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/spoiler.md b/tests/Utils/Parser/Markdown/manualdata/spoiler.md new file mode 100644 index 000000000..011c08b14 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/spoiler.md @@ -0,0 +1 @@ +This is a >!test spoiler!< in text. \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/spoiler_block.html b/tests/Utils/Parser/Markdown/manualdata/spoiler_block.html new file mode 100644 index 000000000..067b6cdb3 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/spoiler_block.html @@ -0,0 +1,5 @@ +
          +Test summary + +This is a test spoiler. +
          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/spoiler_block.md b/tests/Utils/Parser/Markdown/manualdata/spoiler_block.md new file mode 100644 index 000000000..f855f1231 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/spoiler_block.md @@ -0,0 +1,3 @@ +???Test summary +This is a test spoiler. +??? \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/tablespan.html b/tests/Utils/Parser/Markdown/manualdata/tablespan.html new file mode 100644 index 000000000..83d411ac2 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/tablespan.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          Colspanfor thead
          Loremipsumdolorsitamet
          -right align.
          ,center align2x2 cell
          another 2x2+
          !
          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/tablespan.md b/tests/Utils/Parser/Markdown/manualdata/tablespan.md new file mode 100644 index 000000000..3b35723eb --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/tablespan.md @@ -0,0 +1,7 @@ +| > | > | Colspan | > | for thead | +| ----- | :---------: | -----------: | ----------- | --------- | +| Lorem | ipsum | dolor | sit | amet | +| ^ | - | > | right align | . | +| , | > | center align | > | 2x2 cell | +| > | another 2x2 | + | > | ^ | +| > | ^ | | | ! | \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/toc.html b/tests/Utils/Parser/Markdown/manualdata/toc.html new file mode 100644 index 000000000..b7951fe2c --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/toc.html @@ -0,0 +1,10 @@ +
            +
          • A
              +
            • 1
                +
              • i
              • +
              +
            • +
            +
          • +
          • B
          • +
          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/toc.md b/tests/Utils/Parser/Markdown/manualdata/toc.md new file mode 100644 index 000000000..156f263f0 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/toc.md @@ -0,0 +1,7 @@ +# A + +## 1 + +### i + +# B diff --git a/tests/Utils/Parser/Markdown/manualdata/typographer.html b/tests/Utils/Parser/Markdown/manualdata/typographer.html new file mode 100644 index 000000000..ea5570267 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/typographer.html @@ -0,0 +1 @@ +

          ©

          \ No newline at end of file diff --git a/tests/Utils/Parser/Markdown/manualdata/typographer.md b/tests/Utils/Parser/Markdown/manualdata/typographer.md new file mode 100644 index 000000000..e58d56b30 --- /dev/null +++ b/tests/Utils/Parser/Markdown/manualdata/typographer.md @@ -0,0 +1 @@ +(c) \ No newline at end of file diff --git a/tests/Utils/Parser/Presentation/PresentationWriterTest.php b/tests/Utils/Parser/Presentation/PresentationWriterTest.php new file mode 100644 index 000000000..eb4d87a60 --- /dev/null +++ b/tests/Utils/Parser/Presentation/PresentationWriterTest.php @@ -0,0 +1,38 @@ +renderHtml())) < 100 + ); + } +} diff --git a/tests/Utils/Parser/Presentation/data/Powerpoint.html b/tests/Utils/Parser/Presentation/data/Powerpoint.html new file mode 100644 index 000000000..ba1d70e16 --- /dev/null +++ b/tests/Utils/Parser/Presentation/data/Powerpoint.html @@ -0,0 +1 @@ +
          • folder_open PhpPresentation
            • info Info "PhpPresentation"
            • indeterminate_check_box Slide
              • info Info "Slide"
              • Shape "Drawing\Gd"
              • Shape "RichText"
            • indeterminate_check_box Slide
              • info Info "Slide"
              • Shape "RichText"
              • Shape "RichText"
              • Shape "Drawing\Gd"
            • indeterminate_check_box Slide
              • info Info "Slide"
              • Shape "RichText"
              • Shape "RichText"
              • Shape "Drawing\Gd"
            • indeterminate_check_box Slide
              • info Info "Slide"
              • Shape "RichText"
              • Shape "RichText"
              • Shape "Drawing\Gd"
            • indeterminate_check_box Slide
              • info Info "Slide"
              • Shape "Drawing\Gd"
              • Shape "RichText"
          Number of slides
          5
          Document Layout Name
          screen4x3
          Document Layout Height
          190.5 mm
          Document Layout Width
          254 mm
          Properties : Category
          demo
          Properties : Company
          Microsoft Corporation
          Properties : Created
          1608365230
          Properties : Creator
          Orange Management
          Properties : Description
          Demo
          Properties : Keywords
          demo helper report
          Properties : Last Modified By
          Orange Management
          Properties : Modified
          1608365230
          Properties : Subject
          Orange Management - Demo Report
          Properties : Title
          Orange Management - Demo Report
          HashCode
          d19832a1793b2a9275a860669e43a033
          Offset X
          180
          Offset Y
          120
          Extent X
          780
          Extent Y
          750
          Background Color
          #434a51
          HashCode
          421875ef4d736d74b39b1648295aa512
          Offset X
          320
          Offset Y
          120
          Height
          300
          Width
          319
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          Name
          Company Logo
          Description
          Company Logo
          Mime-Type
          image/png
          Image
          HashCode
          094c9efa83b1869b1d219b4e5f39846d
          Offset X
          180
          Offset Y
          450
          Height
          300
          Width
          600
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          # of paragraphs
          1
          Inset (T / R / B / L)
          4.8px / 9.6px / 4.8px / 9.6px
          Text
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_CENTER
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          0 px / 0px
          Alignment Indent
          0 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_NONE
          Line Spacing
          100
          RichText
          TextElement
          Demo Report
          Font Name
          Calibri
          Font Size
          35
          Font Color
          #FF3697db
          Font Transform
          Bold : Y - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          HashCode
          b31764654c01e4333a2bff41a84b85c4
          Offset X
          10
          Offset Y
          65
          Extent X
          940
          Extent Y
          770
          HashCode
          8f11f8af6ebcb6adcd7073c8b13ef3bf
          Offset X
          50
          Offset Y
          65
          Height
          75
          Width
          860
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          # of paragraphs
          1
          Inset (T / R / B / L)
          4.8px / 9.6px / 4.8px / 9.6px
          Text
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          0 px / 0px
          Alignment Indent
          0 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_NONE
          Line Spacing
          100
          RichText
          TextElement
          Demo Report - 2020-12-01
          Font Name
          Calibri
          Font Size
          35
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          HashCode
          a0b4015874cdc64c0449b56982621676
          Offset X
          10
          Offset Y
          170
          Height
          600
          Width
          930
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          # of paragraphs
          13
          Inset (T / R / B / L)
          4.8px / 9.6px / 4.8px / 9.6px
          Text
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Create custom localized reports
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          They are 100% customizable in terms of style, layout and content
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          You can export them as:
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Excel
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          PDF
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Print
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          PowerPoint
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          CSV
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Word
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          HashCode
          288b8d4c2232e4f7ac784712d6681de5
          Offset X
          880
          Offset Y
          650
          Height
          50
          Width
          53
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          Name
          Company Logo
          Description
          Company Logo
          Mime-Type
          image/png
          Image
          HashCode
          48f7a298a02a2c52b31f1fa7329602b0
          Offset X
          10
          Offset Y
          65
          Extent X
          940
          Extent Y
          770
          HashCode
          9fe246273154ea31296d70e4271405fe
          Offset X
          50
          Offset Y
          65
          Height
          75
          Width
          860
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          # of paragraphs
          1
          Inset (T / R / B / L)
          4.8px / 9.6px / 4.8px / 9.6px
          Text
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          0 px / 0px
          Alignment Indent
          0 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_NONE
          Line Spacing
          100
          RichText
          TextElement
          Ideas for helpers
          Font Name
          Calibri
          Font Size
          35
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          HashCode
          eccd7226a23c0a416f5c1d47999ba7a3
          Offset X
          10
          Offset Y
          170
          Height
          600
          Width
          930
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          # of paragraphs
          11
          Inset (T / R / B / L)
          4.8px / 9.6px / 4.8px / 9.6px
          Text
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Reports (e.g. sales, finance marketing
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Mailing generator based on pre-defined layouts
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Document generator based on pre-defined layouts
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Calculators (e.g. margin calculator)
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          HashCode
          b2510ceece8f5d05845ad4eaea05975f
          Offset X
          880
          Offset Y
          650
          Height
          50
          Width
          53
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          Name
          Company Logo
          Description
          Company Logo
          Mime-Type
          image/png
          Image
          HashCode
          7e480a0459c2fc59f92dff79fafd8c48
          Offset X
          10
          Offset Y
          65
          Extent X
          940
          Extent Y
          770
          HashCode
          d7070d8edd5f1161b1ad142e32989866
          Offset X
          50
          Offset Y
          65
          Height
          75
          Width
          860
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          # of paragraphs
          1
          Inset (T / R / B / L)
          4.8px / 9.6px / 4.8px / 9.6px
          Text
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          0 px / 0px
          Alignment Indent
          0 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_NONE
          Line Spacing
          100
          RichText
          TextElement
          Data Source
          Font Name
          Calibri
          Font Size
          35
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          HashCode
          93a715a86ea18715299857ed5dd1c057
          Offset X
          10
          Offset Y
          170
          Height
          600
          Width
          930
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          # of paragraphs
          7
          Inset (T / R / B / L)
          4.8px / 9.6px / 4.8px / 9.6px
          Text
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          50 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          You can provide data for the helpers in many different ways
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Manual user input
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          File upload (e.g. excel, csv)
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Database upload (e.g. sqlite)
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Database connection to local or remote database
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          External APIs
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_GENERAL
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          150 px / 0px
          Alignment Indent
          -25 px
          Alignment Level
          1
          Bullet Style
          Bullet::TYPE_BULLET
          Bullet Font
          Calibri
          Bullet Color
          FF000000
          Bullet Char
          Line Spacing
          100
          RichText
          TextElement
          Internal APIs (everything from the Orange Management backend)
          Font Name
          Calibri
          Font Size
          21
          Font Color
          #FF000000
          Font Transform
          Bold : N - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          HashCode
          d0a1172b6748c00f997c887f952b3c09
          Offset X
          880
          Offset Y
          650
          Height
          50
          Width
          53
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          Name
          Company Logo
          Description
          Company Logo
          Mime-Type
          image/png
          Image
          HashCode
          b860e0d1191d2e5975c19a28620c7cfb
          Offset X
          180
          Offset Y
          120
          Extent X
          780
          Extent Y
          750
          Background Color
          #434a51
          HashCode
          e06f2bda477e646faae2234a49a8c685
          Offset X
          320
          Offset Y
          120
          Height
          300
          Width
          319
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          Name
          Company Logo
          Description
          Company Logo
          Mime-Type
          image/png
          Image
          HashCode
          1628fc9d89b4451c3f1d8b588b69f42a
          Offset X
          180
          Offset Y
          450
          Height
          300
          Width
          600
          Rotation
          Hyperlink
          False
          Fill
          None
          Border
          @Todo
          IsPlaceholder
          false
          # of paragraphs
          1
          Inset (T / R / B / L)
          4.8px / 9.6px / 4.8px / 9.6px
          Text
          Paragraph
          Alignment Horizontal
          Alignment::HORIZONTAL_CENTER
          Alignment Vertical
          Alignment::VERTICAL_BASE
          Alignment Margin (L / R)
          0 px / 0px
          Alignment Indent
          0 px
          Alignment Level
          0
          Bullet Style
          Bullet::TYPE_NONE
          Line Spacing
          100
          RichText
          TextElement
          Thank You!
          Font Name
          Calibri
          Font Size
          42
          Font Color
          #FF3697db
          Font Transform
          Bold : Y - Italic : N - Underline : Underline::UNDERLINE_NONE - Strikethrough : N - SubScript : N - SuperScript : N
          \ No newline at end of file diff --git a/tests/Utils/Parser/Presentation/data/Powerpoint.pptx b/tests/Utils/Parser/Presentation/data/Powerpoint.pptx new file mode 100644 index 000000000..831d10368 Binary files /dev/null and b/tests/Utils/Parser/Presentation/data/Powerpoint.pptx differ diff --git a/tests/Utils/Parser/Spreadsheet/SpreadsheetWriterTest.php b/tests/Utils/Parser/Spreadsheet/SpreadsheetWriterTest.php new file mode 100644 index 000000000..c8a869868 --- /dev/null +++ b/tests/Utils/Parser/Spreadsheet/SpreadsheetWriterTest.php @@ -0,0 +1,39 @@ +toPdfString(__DIR__ . '/data/ExcelMpdf.pdf'); + self::assertFalse(\is_file(__DIR__ . '/data/ExcelMpdf.pdf')); + + \file_put_contents(__DIR__ . '/data/ExcelMpdf.pdf', $pdf); + self::assertTrue(\is_file(__DIR__ . '/data/ExcelMpdf.pdf')); + self::assertGreaterThan(100, \strlen(\file_get_contents(__DIR__ . '/data/ExcelMpdf.pdf'))); + } +} diff --git a/tests/Utils/Parser/Spreadsheet/data/Excel.html b/tests/Utils/Parser/Spreadsheet/data/Excel.html new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Utils/Parser/Spreadsheet/data/Excel.xlsx b/tests/Utils/Parser/Spreadsheet/data/Excel.xlsx new file mode 100644 index 000000000..c76c77dab Binary files /dev/null and b/tests/Utils/Parser/Spreadsheet/data/Excel.xlsx differ diff --git a/tests/Utils/PermutationTest.php b/tests/Utils/PermutationTest.php index c1cf8a984..ddf1f6599 100755 --- a/tests/Utils/PermutationTest.php +++ b/tests/Utils/PermutationTest.php @@ -36,8 +36,8 @@ final class PermutationTest extends \PHPUnit\Framework\TestCase $permutations = ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']; $permutations2 = [['a', 'b', 'c'], ['a', 'c', 'b'], ['b', 'a', 'c'], ['b', 'c', 'a'], ['c', 'a', 'b'], ['c', 'b', 'a']]; - self::assertEquals($permutations, Permutation::permut($arr)); - self::assertEquals($permutations2, Permutation::permut($arr, [], false)); + self::assertEquals($permutations, Permutation::permuteAll($arr)); + self::assertEquals($permutations2, Permutation::permuteAll($arr, [], false)); } /** @@ -68,9 +68,9 @@ final class PermutationTest extends \PHPUnit\Framework\TestCase * @covers phpOMS\Utils\Permutation * @group framework */ - public function testPermutate() : void + public function testPermute() : void { - self::assertEquals(['c', 'b', 'a'], Permutation::permutate(['a', 'b', 'c'], [2, 1, 1])); + self::assertEquals(['c', 'b', 'a'], Permutation::permuteByKey(['a', 'b', 'c'], [2, 1, 1])); } /** @@ -82,6 +82,6 @@ final class PermutationTest extends \PHPUnit\Framework\TestCase { $this->expectException(\OutOfBoundsException::class); - Permutation::permutate('abc', [2, 1, 1, 6]); + Permutation::permuteByKey('abc', [2, 1, 1, 6]); } } diff --git a/tests/Utils/StringUtilsTest.php b/tests/Utils/StringUtilsTest.php index f09a31c3e..531266491 100755 --- a/tests/Utils/StringUtilsTest.php +++ b/tests/Utils/StringUtilsTest.php @@ -229,16 +229,4 @@ final class StringUtilsTest extends \PHPUnit\Framework\TestCase StringUtils::createDiffMarkup($original, $new) ); } - - /** - * @testdox A string can be validated for shell safety - * @covers phpOMS\Utils\StringUtils - * @group framework - */ - public function testIsShellSafe() : void - { - self::assertTrue(StringUtils::isShellSafe('asdf')); - self::assertFalse(StringUtils::isShellSafe('&#;`|*?~<>^()[]{}$\\')); - self::assertFalse(StringUtils::isShellSafe('™')); - } } diff --git a/tests/Utils/TaskSchedule/ScheduleTest.php b/tests/Utils/TaskSchedule/ScheduleTest.php index 1d167e192..e5bacad7e 100755 --- a/tests/Utils/TaskSchedule/ScheduleTest.php +++ b/tests/Utils/TaskSchedule/ScheduleTest.php @@ -39,8 +39,6 @@ final class ScheduleTest extends \PHPUnit\Framework\TestCase * @testdox A task can be created from an array and rendered * @covers phpOMS\Utils\TaskSchedule\Schedule * - * @todo the interval has to be implemented! - * * @group framework */ public function testCreateJobWithData() : void diff --git a/tests/Utils/TaskSchedule/TaskAbstractTest.php b/tests/Utils/TaskSchedule/TaskAbstractTest.php index 6ecefb5c7..31f8ab72f 100755 --- a/tests/Utils/TaskSchedule/TaskAbstractTest.php +++ b/tests/Utils/TaskSchedule/TaskAbstractTest.php @@ -54,7 +54,7 @@ final class TaskAbstractTest extends \PHPUnit\Framework\TestCase { self::assertEquals('', $this->class->getId()); self::assertEquals('', $this->class->getCommand()); - self::assertEquals(TaskStatus::ACTIVE, $this->class->getStatus()); + self::assertEquals(TaskStatus::ACTIVE, $this->class->status); self::assertInstanceOf('\DateTime', $this->class->getNextRunTime()); self::assertInstanceOf('\DateTime', $this->class->getLastRuntime()); self::assertEquals('', $this->class->getComment()); @@ -83,17 +83,6 @@ final class TaskAbstractTest extends \PHPUnit\Framework\TestCase self::assertEquals('Interval', $this->class->getInterval()); } - /** - * @testdox The status can be set and returned - * @covers phpOMS\Utils\TaskSchedule\TaskAbstract - * @group framework - */ - public function testStatusInputOutput() : void - { - $this->class->setStatus(TaskStatus::FINISHED); - self::assertEquals(TaskStatus::FINISHED, $this->class->getStatus()); - } - /** * @testdox The comment can be set and returned * @covers phpOMS\Utils\TaskSchedule\TaskAbstract diff --git a/tests/Utils/TestUtilsClass.php b/tests/Utils/TestUtilsClass.php index 9ebe0230a..46cdf4a71 100755 --- a/tests/Utils/TestUtilsClass.php +++ b/tests/Utils/TestUtilsClass.php @@ -16,11 +16,11 @@ namespace phpOMS\tests\Utils; class TestUtilsClass { - private $a = 1; + private $a = 1; protected $b = 2; - public $c = 3; + public $c = 3; - public $d = '4'; + public $d = '4'; } diff --git a/tests/Utils/diff1.png b/tests/Utils/img/diff1.png similarity index 100% rename from tests/Utils/diff1.png rename to tests/Utils/img/diff1.png diff --git a/tests/Utils/diff2.png b/tests/Utils/img/diff2.png similarity index 100% rename from tests/Utils/diff2.png rename to tests/Utils/img/diff2.png diff --git a/tests/Utils/img/img1.gif b/tests/Utils/img/img1.gif new file mode 100644 index 000000000..6c8a04730 Binary files /dev/null and b/tests/Utils/img/img1.gif differ diff --git a/tests/Utils/img/img1.jpg b/tests/Utils/img/img1.jpg new file mode 100644 index 000000000..0e48f3f5c Binary files /dev/null and b/tests/Utils/img/img1.jpg differ diff --git a/tests/Utils/img1.png b/tests/Utils/img/img1.png similarity index 100% rename from tests/Utils/img1.png rename to tests/Utils/img/img1.png diff --git a/tests/Utils/img/img2.gif b/tests/Utils/img/img2.gif new file mode 100644 index 000000000..47285d4ea Binary files /dev/null and b/tests/Utils/img/img2.gif differ diff --git a/tests/Utils/img/img2.jpg b/tests/Utils/img/img2.jpg new file mode 100644 index 000000000..1f850581a Binary files /dev/null and b/tests/Utils/img/img2.jpg differ diff --git a/tests/Utils/img2.png b/tests/Utils/img/img2.png similarity index 100% rename from tests/Utils/img2.png rename to tests/Utils/img/img2.png diff --git a/tests/Utils/logo.png b/tests/Utils/img/logo.png old mode 100755 new mode 100644 similarity index 100% rename from tests/Utils/logo.png rename to tests/Utils/img/logo.png diff --git a/tests/Utils/logo_resized.png b/tests/Utils/img/logo_resized.png similarity index 100% rename from tests/Utils/logo_resized.png rename to tests/Utils/img/logo_resized.png diff --git a/tests/Views/ViewTest.php b/tests/Views/ViewTest.php index b0bc13d0e..59a6f6d8a 100755 --- a/tests/Views/ViewTest.php +++ b/tests/Views/ViewTest.php @@ -251,7 +251,7 @@ final class ViewTest extends \PHPUnit\Framework\TestCase */ public function testGetRequest() : void { - $view = new View($this->app->l11nManager, $request = new HttpRequest(new HttpUri('')), $response = new HttpResponse()); + $view = new View($this->app->l11nManager, $request = new HttpRequest(), $response = new HttpResponse()); self::assertEquals($request, $view->request); self::assertEquals($response, $view->response); @@ -264,7 +264,7 @@ final class ViewTest extends \PHPUnit\Framework\TestCase */ public function testGetResponse() : void { - $view = new View($this->app->l11nManager, new HttpRequest(new HttpUri('')), $response = new HttpResponse()); + $view = new View($this->app->l11nManager, new HttpRequest(), $response = new HttpResponse()); self::assertEquals($response, $view->response); } @@ -276,7 +276,7 @@ final class ViewTest extends \PHPUnit\Framework\TestCase */ public function testPrintHtml() : void { - $view = new View($this->app->l11nManager, $request = new HttpRequest(new HttpUri('')), $response = new HttpResponse()); + $view = new View($this->app->l11nManager, $request = new HttpRequest(), $response = new HttpResponse()); self::assertEquals('<a href="test">Test</a>', $view->printHtml('Test')); self::assertEquals('<a href="test">Test</a>', ViewAbstract::html('Test'));