mirror of
https://github.com/Karaka-Management/phpOMS.git
synced 2026-01-10 17:28:40 +00:00
Merge branch 'develop'
This commit is contained in:
commit
cc243cffd7
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
|
|
@ -1,12 +0,0 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # orange_management
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://paypal.me/orangemgmt']
|
||||
37
.github/dev_bug_report.md
vendored
37
.github/dev_bug_report.md
vendored
|
|
@ -1,37 +0,0 @@
|
|||
---
|
||||
name: Dev Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: stat_backlog, type_bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Bug Description
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
# How to Reproduce
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
## Minimal Code Example
|
||||
|
||||
```php
|
||||
<?php
|
||||
// your code ...
|
||||
?>
|
||||
```
|
||||
|
||||
# Expected Behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
# Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
# Additional Information
|
||||
Add any other context about the problem here.
|
||||
18
.github/dev_feature_request.md
vendored
18
.github/dev_feature_request.md
vendored
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
name: Dev Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: stat_backlog, type_feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# What is the feature you request
|
||||
* A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
* A clear and concise description of what you want to happen.
|
||||
|
||||
# Alternatives
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
# Additional Information
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace phpOMS\Ai\NeuralNetwork;
|
|||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Neuron
|
||||
final class Neuron
|
||||
{
|
||||
/**
|
||||
* Neuron inputs
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<int, array<int, int|float>>
|
||||
*
|
||||
* @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 = $this->similarityMatrix;
|
||||
$this->availabilityMatrix = $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<int, array<int, int|float>> $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 = $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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,17 +14,141 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(PointInterface $point) : ?PointInterface
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,37 @@ namespace phpOMS\Algorithm\Clustering;
|
|||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class Birch
|
||||
final class Birch implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(PointInterface $point) : ?PointInterface
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
Algorithm/Clustering/ClusteringInterface.php
Normal file
71
Algorithm/Clustering/ClusteringInterface.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering interface.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Get cluster centroids
|
||||
*
|
||||
* @return PointInterface[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getCentroids() : array;
|
||||
|
||||
/**
|
||||
* Get cluster assignments of the training data
|
||||
*
|
||||
* @return PointInterface[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getClusters() : array;
|
||||
|
||||
/**
|
||||
* Cluster a single point
|
||||
*
|
||||
* This point doesn't have to be in the training data.
|
||||
*
|
||||
* @param PointInterface $point Point to cluster
|
||||
*
|
||||
* @return null|PointInterface
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function cluster(PointInterface $point) : ?PointInterface;
|
||||
|
||||
/**
|
||||
* Get noise data.
|
||||
*
|
||||
* Data points from the training data that are not part of a cluster.
|
||||
*
|
||||
* @return PointInterface[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getNoise() : array;
|
||||
|
||||
// Not possible to interface due to different implementations
|
||||
// public function generateClusters(...) : void
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ use phpOMS\Math\Topology\MetricsND;
|
|||
*
|
||||
* @todo Expand to n dimensions
|
||||
*/
|
||||
final class DBSCAN
|
||||
final class DBSCAN implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Epsilon for float comparison.
|
||||
|
|
@ -63,12 +63,20 @@ final class DBSCAN
|
|||
*/
|
||||
private array $points = [];
|
||||
|
||||
/**
|
||||
* Points of the cluster centers
|
||||
*
|
||||
* @var PointInterface[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusterCenters = [];
|
||||
|
||||
/**
|
||||
* Clusters
|
||||
*
|
||||
* Array of points assigned to a cluster
|
||||
*
|
||||
* @var array<int, array>
|
||||
* @var array<int, PointInterface[]>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
65
Algorithm/Clustering/DivisiveClustering.php
Normal file
65
Algorithm/Clustering/DivisiveClustering.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
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 DivisiveClustering implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(PointInterface $point) : ?PointInterface
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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?->name] = $point;
|
||||
}
|
||||
|
||||
return $this->clusters;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class Point implements PointInterface
|
|||
* Coordinates of the point
|
||||
*
|
||||
* @var array<int, int|float>
|
||||
* @sicne 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $coordinates = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,37 @@ namespace phpOMS\Algorithm\Clustering;
|
|||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class SpectralClustering
|
||||
final class SpectralClustering implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(PointInterface $point) : ?PointInterface
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./clustering_overview.png
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class Ward
|
||||
{
|
||||
}
|
||||
|
|
@ -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<array>
|
||||
*
|
||||
|
|
@ -70,13 +70,14 @@ final class Apriori
|
|||
*
|
||||
* The algorithm cheks how often a set exists in a given set of sets.
|
||||
*
|
||||
* @param array<array> $sets Sets of a set (e.g. [[1,2,3,4], [1,2], [1]])
|
||||
* @param array<string[]> $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)) {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ final class DependencyResolver
|
|||
$unresolved[] = $dependency;
|
||||
self::dependencyResolve($dependency, $items, $resolved, $unresolved);
|
||||
} else {
|
||||
continue; // circular dependency
|
||||
return; // circular dependency
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Material.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Material
|
||||
{
|
||||
public int $id = 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Material.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Qualification
|
||||
{
|
||||
public int $id = 0;
|
||||
}
|
||||
|
|
@ -24,32 +24,96 @@ namespace phpOMS\Scheduling;
|
|||
*/
|
||||
class Job
|
||||
{
|
||||
/**
|
||||
* Id
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $id = 0;
|
||||
|
||||
/**
|
||||
* Time of the execution
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $executionTime = 0;
|
||||
|
||||
/**
|
||||
* Priority.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $priority = 0.0;
|
||||
|
||||
/**
|
||||
* Value this job generates.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $value = 0.0;
|
||||
|
||||
/**
|
||||
* Cost of executing this job.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $cost = 0.0;
|
||||
|
||||
/** How many iterations has this job been on hold in the queue */
|
||||
/**
|
||||
* How many iterations has this job been on hold in the queue.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $onhold = 0;
|
||||
|
||||
/** How many iterations has this job been in process in the queue */
|
||||
/**
|
||||
* How many iterations has this job been in process in the queue.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $inprocessing = 0;
|
||||
|
||||
/**
|
||||
* What is the deadline for this job?
|
||||
*
|
||||
* @param \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $deadline;
|
||||
|
||||
/**
|
||||
* Which steps must be taken during the job execution
|
||||
*
|
||||
* @var JobStep[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $steps = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->deadline = new \DateTime('now');
|
||||
}
|
||||
|
||||
public function getProfit()
|
||||
/**
|
||||
* Get the profit of the job
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getProfit() : float
|
||||
{
|
||||
return $this->value - $this->cost;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,24 @@ namespace phpOMS\Scheduling;
|
|||
*/
|
||||
final class ScheduleQueue
|
||||
{
|
||||
/**
|
||||
* Queue
|
||||
*
|
||||
* @var Job[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $queue = [];
|
||||
|
||||
/**
|
||||
* Get element from queue
|
||||
*
|
||||
* @param int $size Amount of elements to return
|
||||
* @param int $type Priority type to use for return
|
||||
*
|
||||
* @return Job[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function get(int $size = 1, int $type = PriorityMode::FIFO) : array
|
||||
{
|
||||
$jobs = [];
|
||||
|
|
@ -103,11 +119,33 @@ final class ScheduleQueue
|
|||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new element into queue
|
||||
*
|
||||
* @param int $id Element id
|
||||
* @param Job $job Element to add
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function insert(int $id, Job $job) : void
|
||||
{
|
||||
$this->queue[$id] = $job;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop elements from the queue.
|
||||
*
|
||||
* This also removes the elements from the queue
|
||||
*
|
||||
* @param int $size Amount of elements to return
|
||||
* @param int $type Priority type to use for return
|
||||
*
|
||||
* @return Job[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function pop(int $size = 1, int $type = PriorityMode::FIFO) : array
|
||||
{
|
||||
$jobs = $this->get($size, $type);
|
||||
|
|
@ -118,6 +156,15 @@ final class ScheduleQueue
|
|||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the hold counter of an element
|
||||
*
|
||||
* @param int $id Id of the element (0 = all elements)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function bumpHold(int $id = 0) : void
|
||||
{
|
||||
if ($id === 0) {
|
||||
|
|
@ -129,6 +176,16 @@ final class ScheduleQueue
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the priority of an element
|
||||
*
|
||||
* @param int $id Id of the element (0 = all elements)
|
||||
* @param float $priority Priority to increase by
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function adjustPriority(int $id = 0, float $priority = 0.1) : void
|
||||
{
|
||||
if ($id === 0) {
|
||||
|
|
@ -140,7 +197,16 @@ final class ScheduleQueue
|
|||
}
|
||||
}
|
||||
|
||||
public function remove(string $id) : void
|
||||
/**
|
||||
* Remove an element from the queue
|
||||
*
|
||||
* @param int $id Id of the element
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function remove(int $id) : void
|
||||
{
|
||||
unset($this->queue[$id]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -80,9 +80,9 @@ class GeneticOptimization
|
|||
*
|
||||
* @example See unit test for example use case
|
||||
*
|
||||
* @param array<array> $population List of all elements with ther parameters (i.e. list of "objects" as arrays).
|
||||
* @param array<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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -22,25 +22,92 @@ use phpOMS\Math\Stochastic\Distribution\NormalDistribution;
|
|||
* @package phpOMS\Algorithm\Rating
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see https://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param null|float $mu Mu
|
||||
* @param null|float $sigma Sigma
|
||||
* @param null|float $beta Beta
|
||||
* @param null|float $tau Tau
|
||||
* @param null|float $drawProbability Draw probability
|
||||
*
|
||||
* @since 1.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate win probability
|
||||
*
|
||||
* @param array $team1 Team 1
|
||||
* @param array $team2 Team 2
|
||||
* @param float $drawMargin Draw margin
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function winProbability(array $team1, array $team2, float $drawMargin = 0.0) : float
|
||||
{
|
||||
$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
|
||||
|
|
@ -159,22 +226,37 @@ class TrueSkill
|
|||
/ (NormalDistribution::getCdf($epsilon - $tAbs, 0.0, 1.0) - NormalDistribution::getCdf(-$epsilon - $tAbs, 0.0, 1.0));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function buildRatingLayer() : void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function buildPerformanceLayer() : void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function buildTeamPerformanceLayer() : void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function buildTruncLayer() : void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function factorGraphBuilders()
|
||||
{
|
||||
// Rating layer
|
||||
|
|
@ -193,6 +275,9 @@ class TrueSkill
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function rating() : void
|
||||
{
|
||||
// Start values
|
||||
|
|
|
|||
11
DataStorage/Database/Query/Expression.php → Algorithm/Rating/TrueSkillFactoryGraph.php
Executable file → Normal file
11
DataStorage/Database/Query/Expression.php → Algorithm/Rating/TrueSkillFactoryGraph.php
Executable file → Normal file
|
|
@ -4,7 +4,7 @@
|
|||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
|
|
@ -12,16 +12,17 @@
|
|||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\DataStorage\Database\Query;
|
||||
namespace phpOMS\Algorithm\Rating;
|
||||
|
||||
/**
|
||||
* Database query builder.
|
||||
* Elo rating calculation using Elo rating
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://en.wikipedia.org/wiki/Elo_rating_system
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Expression extends Builder
|
||||
final class TrueSkillFactoryGraph
|
||||
{
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
36
Api/Shipping/AuthStatus.php
Normal file
36
Api/Shipping/AuthStatus.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Auth Status
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class AuthStatus extends Enum
|
||||
{
|
||||
public const OK = 0;
|
||||
|
||||
public const FAILED = -1;
|
||||
|
||||
public const BLOCKED = -2;
|
||||
|
||||
public const LIMIT_EXCEEDED = -3;
|
||||
}
|
||||
34
Api/Shipping/AuthType.php
Normal file
34
Api/Shipping/AuthType.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Auth Type
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class AuthType extends Enum
|
||||
{
|
||||
public const AUTOMATIC_LOGIN = 2;
|
||||
|
||||
public const MANUAL_LOGIN = 4;
|
||||
|
||||
public const KEY_LOGIN = 8;
|
||||
}
|
||||
405
Api/Shipping/DHL/DHLInternationalShipping.php
Normal file
405
Api/Shipping/DHL/DHLInternationalShipping.php
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\DHL
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\DHL;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\AuthType;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
use phpOMS\Message\Http\RequestMethod;
|
||||
use phpOMS\Message\Http\Rest;
|
||||
use phpOMS\System\MimeType;
|
||||
use phpOMS\Uri\HttpUri;
|
||||
|
||||
/**
|
||||
* Shipment API.
|
||||
*
|
||||
* In the third party API the following definitions are important to know:
|
||||
*
|
||||
* 1. Order: A collection of shipments with the same service (standard, return, packet plus, packet tracked)
|
||||
* 2. Item: A shipment/package
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\DHL
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see General: https://developer.dhl.com/
|
||||
* @see Special: https://developer.dhl.com/api-reference/deutsche-post-international-post-parcel-germany#get-started-section/
|
||||
* @see Tracking: https://developer.dhl.com/api-reference/shipment-tracking#get-started-section/
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DHLInternationalShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* Api version
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const API_VERSION = 'v1';
|
||||
|
||||
/**
|
||||
* API environment.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static string $ENV = 'live';
|
||||
|
||||
/**
|
||||
* API link to live/production version.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const LIVE_URL = 'https://api-eu.dhl.com/dpi';
|
||||
|
||||
/**
|
||||
* API link to test/sandbox version.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const SANDBOX_URL = 'https://api-sandbox.dhl.com/dpi';
|
||||
|
||||
/**
|
||||
* API link to test/sandbox version.
|
||||
*
|
||||
* This implementation uses different testing urls for the different endpoints
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const SANDBOX2_URL = 'https://api-test.dhl.com';
|
||||
|
||||
/**
|
||||
* The type of authentication that is supported.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const AUTH_TYPE = AuthType::AUTOMATIC_LOGIN | AuthType::KEY_LOGIN;
|
||||
|
||||
/**
|
||||
* Minimum auth expiration time until re-auth.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const TIME_DELTA = 10;
|
||||
|
||||
/**
|
||||
* Client id
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $client = '';
|
||||
|
||||
/**
|
||||
* Login id
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $login = '';
|
||||
|
||||
/**
|
||||
* Password
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $password = '';
|
||||
|
||||
/**
|
||||
* Current auth token
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $token = '';
|
||||
|
||||
/**
|
||||
* Current auth refresh token in case the token expires
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $refreshToken = '';
|
||||
|
||||
/**
|
||||
* Api Key
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $apiKey = '';
|
||||
|
||||
/**
|
||||
* Token expiration.
|
||||
*
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $expire;
|
||||
|
||||
/**
|
||||
* Refresh token expiration.
|
||||
*
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $refreshExpire;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
659
Api/Shipping/DHL/DHLParcelDEShipping.php
Normal file
659
Api/Shipping/DHL/DHLParcelDEShipping.php
Normal file
|
|
@ -0,0 +1,659 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\DHL
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\DHL;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\AuthType;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Localization\ISO3166CharEnum;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
use phpOMS\Message\Http\RequestMethod;
|
||||
use phpOMS\Message\Http\Rest;
|
||||
use phpOMS\System\MimeType;
|
||||
use phpOMS\Uri\HttpUri;
|
||||
|
||||
/**
|
||||
* Shipment API.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\DHL
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see General: https://developer.dhl.com/
|
||||
* @see Special: https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2#get-started-section/
|
||||
* @see Tracking: https://developer.dhl.com/api-reference/shipment-tracking#get-started-section/
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DHLParcelDEShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* Api version
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const API_VERSION = 'v2';
|
||||
|
||||
/**
|
||||
* API environment.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static string $ENV = 'live';
|
||||
|
||||
/**
|
||||
* API link to live/production version.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const LIVE_URL = 'https://api-eu.dhl.com';
|
||||
|
||||
/**
|
||||
* API link to test/sandbox version.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const SANDBOX_URL = 'https://api-sandbox.dhl.com';
|
||||
|
||||
/**
|
||||
* API link to test/sandbox version.
|
||||
*
|
||||
* This implementation uses different testing urls for the different endpoints
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const SANDBOX2_URL = 'https://api-test.dhl.com';
|
||||
|
||||
/**
|
||||
* The type of authentication that is supported.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const AUTH_TYPE = AuthType::AUTOMATIC_LOGIN | AuthType::KEY_LOGIN;
|
||||
|
||||
/**
|
||||
* Minimum auth expiration time until re-auth.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const TIME_DELTA = 10;
|
||||
|
||||
/**
|
||||
* Client id
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $client = '';
|
||||
|
||||
/**
|
||||
* Login id
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $login = '';
|
||||
|
||||
/**
|
||||
* Password
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $password = '';
|
||||
|
||||
/**
|
||||
* Current auth token
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $token = '';
|
||||
|
||||
/**
|
||||
* Current auth refresh token in case the token expires
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $refreshToken = '';
|
||||
|
||||
/**
|
||||
* Api Key
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $apiKey = '';
|
||||
|
||||
/**
|
||||
* Token expiration.
|
||||
*
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $expire;
|
||||
|
||||
/**
|
||||
* Refresh token expiration.
|
||||
*
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $refreshExpire;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
114
Api/Shipping/DHL/DHLeCommerceShipping.php
Normal file
114
Api/Shipping/DHL/DHLeCommerceShipping.php
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\DHL
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\DHL;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
|
||||
/**
|
||||
* Shipment api.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\DHL
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see General: https://developer.dhl.com/
|
||||
* @see Special: https://developer.dhl.com/api-reference/ecommerce-europe#get-started-section/
|
||||
* @see Tracking: https://developer.dhl.com/api-reference/shipment-tracking#get-started-section/
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DHLeCommerceShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authLogin(
|
||||
string $login, string $password,
|
||||
?string $client = null,
|
||||
?string $payload = null
|
||||
) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authApiKey(string $key) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refreshToken() : int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
113
Api/Shipping/DPD/DPDShipping.php
Normal file
113
Api/Shipping/DPD/DPDShipping.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\DPD
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\DPD;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
|
||||
/**
|
||||
* Shipment api.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\DPD
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://api.dpd.ro/web-api.html#href-create-shipment-req
|
||||
* @see https://www.dpd.com/de/en/entwickler-integration-in-ihre-versandloesung/
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DPDShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authLogin(
|
||||
string $login, string $password,
|
||||
?string $client = null,
|
||||
?string $payload = null
|
||||
) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authApiKey(string $key) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refreshToken() : int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
112
Api/Shipping/Fedex/FedexShipping.php
Normal file
112
Api/Shipping/Fedex/FedexShipping.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\Fedex
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\Fedex;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
|
||||
/**
|
||||
* Shipment api.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\Fedex
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://developer.fedex.com/api/en-us/home.html
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class FedexShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authLogin(
|
||||
string $login, string $password,
|
||||
?string $client = null,
|
||||
?string $payload = null
|
||||
) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authApiKey(string $key) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refreshToken() : int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
112
Api/Shipping/RoyalMail/RoyalMailShipping.php
Normal file
112
Api/Shipping/RoyalMail/RoyalMailShipping.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\RoyalMail
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\RoyalMail;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
|
||||
/**
|
||||
* Shipment api.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\RoyalMail
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://developer.royalmail.net/
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class RoyalMailShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authLogin(
|
||||
string $login, string $password,
|
||||
?string $client = null,
|
||||
?string $payload = null
|
||||
) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authApiKey(string $key) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refreshToken() : int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
59
Api/Shipping/ShippingFactory.php
Normal file
59
Api/Shipping/ShippingFactory.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping;
|
||||
|
||||
/**
|
||||
* Shipping factory.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class ShippingFactory
|
||||
{
|
||||
/**
|
||||
* Create shipping instance.
|
||||
*
|
||||
* @param int $type Shipping type
|
||||
*
|
||||
* @return ShippingInterface
|
||||
*
|
||||
* @throws \Exception This exception is thrown if the shipping type is not supported
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create(int $type) : ShippingInterface
|
||||
{
|
||||
switch ($type) {
|
||||
case ShippingType::DHL:
|
||||
return new \phpOMS\Api\Shipping\DHL\DHLInternationalShipping();
|
||||
case ShippingType::DPD:
|
||||
return new \phpOMS\Api\Shipping\DPD\DPDShipping();
|
||||
case ShippingType::FEDEX:
|
||||
return new \phpOMS\Api\Shipping\Fedex\FedexShipping();
|
||||
case ShippingType::ROYALMAIL:
|
||||
return new \phpOMS\Api\Shipping\RoyalMail\RoyalMailShipping();
|
||||
case ShippingType::TNT:
|
||||
return new \phpOMS\Api\Shipping\TNT\TNTShipping();
|
||||
case ShippingType::UPS:
|
||||
return new \phpOMS\Api\Shipping\UPS\UPSShipping();
|
||||
case ShippingType::USPS:
|
||||
return new \phpOMS\Api\Shipping\Usps\UspsShipping();
|
||||
default:
|
||||
throw new \Exception('Unsupported shipping type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
171
Api/Shipping/ShippingInterface.php
Normal file
171
Api/Shipping/ShippingInterface.php
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping;
|
||||
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
|
||||
/**
|
||||
* Shipping interface.
|
||||
*
|
||||
* For authentication there are usually 3 options depending on the service
|
||||
* 1. No user interaction: Store login+password in database or code and perform authentication via login+password and receive an access token
|
||||
* 2. No user interaction: Store api key or secret token in database and perform authentication via key/secret and receive an access token
|
||||
* 3. User interaction: Redirect to 3rd party login page. User performs manual login. 3rd party page redirects pack to own app after login incl. an access token
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo implement Sender, Receiver, Package, Transit, Tracking classes for better type hinting instead of arrays
|
||||
*
|
||||
* @property string $ENV ('live' = live environment, 'test' or 'sandbox' = test environment)
|
||||
* @property string $client
|
||||
* @property string $token
|
||||
* @property string $refreshToken
|
||||
* @property string $apiKey
|
||||
* @property string $login
|
||||
* @property string $password
|
||||
* @property \DateTime $expire
|
||||
* @property \DateTime $refreshExpire
|
||||
*/
|
||||
interface ShippingInterface
|
||||
{
|
||||
/**
|
||||
* Create request for authentication using login and password
|
||||
*
|
||||
* @param string $login Login name/email
|
||||
* @param string $password Password
|
||||
* @param null|string $client Client id
|
||||
* @param null|string $payload Other payload data
|
||||
*
|
||||
* @return int Returns auth status
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function authLogin(
|
||||
string $login, string $password,
|
||||
?string $client = null,
|
||||
?string $payload = null
|
||||
) : int;
|
||||
|
||||
/**
|
||||
* Create request for manual (user has to use form on external website) authentication.
|
||||
*
|
||||
* Creates a request object that redirects to a login page where the user has to enter
|
||||
* the login credentials. After login the external login page redirects back to the
|
||||
* redirect url which will also have a parameter containing the authentication token.
|
||||
*
|
||||
* Use tokenFromRedirect() to parse the token from the redirect after successful login.
|
||||
*
|
||||
* @param string $client Client information (e.g. client id)
|
||||
* @param null|string $redirect Redirect page after successful login
|
||||
* @param array $payload Other payload data
|
||||
*
|
||||
* @return HttpRequest Request which should be used to create the redirect (e.g. header("Location: $request->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;
|
||||
}
|
||||
42
Api/Shipping/ShippingType.php
Normal file
42
Api/Shipping/ShippingType.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Shipping Type
|
||||
*
|
||||
* @package phpOMS\Api\Shipping
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class ShippingType extends Enum
|
||||
{
|
||||
public const DHL = 1;
|
||||
|
||||
public const DPD = 2;
|
||||
|
||||
public const FEDEX = 3;
|
||||
|
||||
public const ROYALMAIL = 4;
|
||||
|
||||
public const TNT = 5;
|
||||
|
||||
public const UPS = 6;
|
||||
|
||||
public const USPS = 7;
|
||||
}
|
||||
112
Api/Shipping/TNT/TNTShipping.php
Normal file
112
Api/Shipping/TNT/TNTShipping.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\TNT
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\TNT;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
|
||||
/**
|
||||
* Shipment api.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\TNT
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://express.tnt.com/expresswebservices-website/app/landing.html
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class TNTShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authLogin(
|
||||
string $login, string $password,
|
||||
?string $client = null,
|
||||
?string $payload = null
|
||||
) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authApiKey(string $key) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refreshToken() : int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
787
Api/Shipping/UPS/UPSShipping.php
Normal file
787
Api/Shipping/UPS/UPSShipping.php
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\UPS
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\UPS;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\AuthType;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
use phpOMS\Message\Http\RequestMethod;
|
||||
use phpOMS\Message\Http\Rest;
|
||||
use phpOMS\System\MimeType;
|
||||
use phpOMS\Uri\HttpUri;
|
||||
|
||||
/**
|
||||
* Shipment API.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\UPS
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://developer.ups.com/api/reference/oauth/authorization-code?loc=en_US
|
||||
* @see https://developer.ups.com/api/reference?loc=en_US
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class UPSShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* Api version
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const API_VERSION = 'v1';
|
||||
|
||||
/**
|
||||
* API environment.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static string $ENV = 'live';
|
||||
|
||||
/**
|
||||
* API link to live/production version.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const LIVE_URL = 'https://onlinetools.ups.com';
|
||||
|
||||
/**
|
||||
* API link to test/sandbox version.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const SANDBOX_URL = 'https://wwwcie.ups.com';
|
||||
|
||||
/**
|
||||
* The type of authentication that is supported.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const AUTH_TYPE = AuthType::AUTOMATIC_LOGIN | AuthType::MANUAL_LOGIN;
|
||||
|
||||
/**
|
||||
* Minimum auth expiration time until re-auth.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const TIME_DELTA = 10;
|
||||
|
||||
/**
|
||||
* Client id
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $client = '';
|
||||
|
||||
/**
|
||||
* Login id
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $login = '';
|
||||
|
||||
/**
|
||||
* Password
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $password = '';
|
||||
|
||||
/**
|
||||
* Current auth token
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $token = '';
|
||||
|
||||
/**
|
||||
* Current auth refresh token in case the token expires
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $refreshToken = '';
|
||||
|
||||
/**
|
||||
* Api Key
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $apiKey = '';
|
||||
|
||||
/**
|
||||
* Token expiration.
|
||||
*
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $expire;
|
||||
|
||||
/**
|
||||
* Refresh token expiration.
|
||||
*
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $refreshExpire;
|
||||
|
||||
/**
|
||||
* Refresh token expiration.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
112
Api/Shipping/Usps/UspsShipping.php
Normal file
112
Api/Shipping/Usps/UspsShipping.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\Usps
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Api\Shipping\Usps;
|
||||
|
||||
use phpOMS\Api\Shipping\AuthStatus;
|
||||
use phpOMS\Api\Shipping\ShippingInterface;
|
||||
use phpOMS\Message\Http\HttpRequest;
|
||||
|
||||
/**
|
||||
* Shipment api.
|
||||
*
|
||||
* @package phpOMS\Api\Shipping\Usps
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://developer.usps.com/apis
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class UspsShipping implements ShippingInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authLogin(
|
||||
string $login, string $password,
|
||||
?string $client = null,
|
||||
?string $payload = null
|
||||
) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authApiKey(string $key) : int
|
||||
{
|
||||
return AuthStatus::FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refreshToken() : int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -195,6 +195,14 @@ class ApplicationAbstract
|
|||
*/
|
||||
protected EventManager $eventManager;
|
||||
|
||||
/**
|
||||
* Application version.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $version = '1.0.0';
|
||||
|
||||
/**
|
||||
* Set values
|
||||
*
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ abstract class StatusAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function activateRoutes(ApplicationInfo $appInfo = null) : void
|
||||
public static function activateRoutes(?ApplicationInfo $appInfo = null) : void
|
||||
{
|
||||
self::installRoutesHooks(static::PATH . '/../Routes.php', static::PATH . '/../Admin/Install/Application/Routes.php');
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ abstract class StatusAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function activateHooks(ApplicationInfo $appInfo = null) : void
|
||||
public static function activateHooks(?ApplicationInfo $appInfo = null) : void
|
||||
{
|
||||
self::installRoutesHooks(static::PATH . '/../Hooks.php', static::PATH . '/../Admin/Install/Application/Hooks.php');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use phpOMS\Stdlib\Base\Enum;
|
|||
*/
|
||||
abstract class LoginReturnType extends Enum
|
||||
{
|
||||
public const OK = 0; /* Everything is ok and the user got authed */
|
||||
public const OK = 0; /* Everything is ok and the user got authenticated */
|
||||
|
||||
public const FAILURE = -1; /* Authentication resulted in a unexpected failure */
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ final class AutoloadException 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('File "' . $message . '" could not get loaded.', $code, $previous);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,17 @@ final class Autoloader
|
|||
$class = \ltrim($class, '\\');
|
||||
$class = \strtr($class, '_\\', '//');
|
||||
|
||||
if (self::$useClassMap) {
|
||||
$nspacePos = \strpos($class, '/');
|
||||
$subclass = $nspacePos === false ? '' : \substr($class, 0, $nspacePos);
|
||||
|
||||
if (isset(self::$classmap[$subclass])) {
|
||||
$found[] = self::$classmap[$subclass] . $class . '.php';
|
||||
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::$paths as $path) {
|
||||
if (\is_file($file = $path . $class . '.php')) {
|
||||
$found[] = $file;
|
||||
|
|
@ -172,18 +183,6 @@ final class Autoloader
|
|||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
if (!isset($valid[$subclass])) {
|
||||
foreach (self::$classmap as $map => $path) {
|
||||
if (\str_starts_with($class, $map)) {
|
||||
include_once $path . $class . '.php';
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
foreach (self::$paths as $path) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -37,9 +37,15 @@ final class BayesianPersonalizedRanking
|
|||
|
||||
private array $itemFactors = [];
|
||||
|
||||
// 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.
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $numFactors Determines the dimensionality of the latent factor space.
|
||||
* @param float $learningRate Controls the step size for updating the latent factors during optimization.
|
||||
* @param float $regularization Prevents over-fitting by adding a penalty for large parameter values.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(int $numFactors, float $learningRate, float $regularization)
|
||||
{
|
||||
$this->numFactors = $numFactors;
|
||||
|
|
@ -47,7 +53,14 @@ final class BayesianPersonalizedRanking
|
|||
$this->regularization = $regularization;
|
||||
}
|
||||
|
||||
private function generateRandomFactors()
|
||||
/**
|
||||
* Calculate random factors
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function generateRandomFactors() : array
|
||||
{
|
||||
$factors = [];
|
||||
for ($i = 0; $i < $this->numFactors; ++$i) {
|
||||
|
|
@ -57,6 +70,9 @@ final class BayesianPersonalizedRanking
|
|||
return $factors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo implement
|
||||
*/
|
||||
public function predict($userId, $itemId) {
|
||||
$userFactor = $this->userFactors[$userId];
|
||||
$itemFactor = $this->itemFactors[$itemId];
|
||||
|
|
@ -69,6 +85,9 @@ final class BayesianPersonalizedRanking
|
|||
return $score;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo implement
|
||||
*/
|
||||
public function updateFactors($userId, $posItemId, $negItemId) : void
|
||||
{
|
||||
if (!isset($this->userFactors[$userId])) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<int|string, array<int|float>> $users A mxa matrix where each "m" defines how much the user likes a certain attribute type and "a" defines different users
|
||||
* @param array<int|string, array<int|float>> $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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
34
Business/Warehouse/OrderSuggestion.php
Normal file
34
Business/Warehouse/OrderSuggestion.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Business\Sales
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Business\Sales;
|
||||
|
||||
/**
|
||||
* Order suggestion calculations
|
||||
*
|
||||
* @package phpOMS\Business\Sales
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class OrderSuggestion
|
||||
{
|
||||
/**
|
||||
* Calculate the optimal order quantity using the Andler formula
|
||||
*/
|
||||
public static function andler(float $annualQuantity, float $orderCosts, float $unitPrice, float $warehousingCostRatio) : float
|
||||
{
|
||||
return \sqrt(2 * $annualQuantity * $orderCosts / ($unitPrice * $warehousingCostRatio));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at spl1nes.com@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# Development
|
||||
|
||||
## Development environment
|
||||
|
||||
The setup and configuration of the development environment is in the hands of every developer themselves. However, it is recommended to follow the setup instructions in the [Developer-Guide](https://github.com/Karaka-Management/Developer-Guide/blob/develop/general/setup.md).
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Every organization member and contributor to the organization must follow the [code of conduct](../Policies & Guidelines/Code of conduct.md).
|
||||
|
||||
## Code changes
|
||||
|
||||
### Topics / Tasks / Todos
|
||||
|
||||
Generally, the development philosophy is result orientated. This means that anyone can propose tasks, pick up existing tasks or right away implement their code changes. However, implementing code changes without consulting with a senior developer in advance has a much higher risk of code changes not getting admitted. The easiest way to discuss a code change idea in advance are the github [issues](https://github.com/Karaka-Management/Karaka/issues) or [discussions](https://github.com/Karaka-Management/Karaka/discussions).
|
||||
|
||||
Developers are encouraged to pick open tasks with high priorities according to their own skill level. Senior developers may directly assign tasks to developers based on their importance. New developers may find it easier to start with a task that has a low priority as they often also have a lower difficulty.
|
||||
|
||||
Open tasks can be found in the project overview: [PROJECT.md](https://github.com/Karaka-Management/Organization-Guide/blob/master/Project/PROJECT.md)
|
||||
|
||||
Tasks currently in development are prefixed in the priority column with an asterisk `*` and a name tag in the task description of the developer who is working on the task.
|
||||
|
||||
The open tasks are reviewed once a month by a senior developer. The senior developer updates the project overview if necessary and requests feedback regarding development status of important tasks under development. During this process important tasks may also get directly assigned to developers. This review is performed on a judgmental bases of the senior basis.
|
||||
|
||||
### Code style
|
||||
|
||||
Code changes must follow the [style guidelines](https://github.com/Karaka-Management/Developer-Guide/tree/develop/standards). Additionally, the automatic code style inspection tools must return no errors, failures or warnings. Developers should test their changes with inspection tools and configurations mentioned in the [inspection documentation](https://github.com/Karaka-Management/Developer-Guide/blob/develop/quality/inspections.md) in advance before submitting them for review.
|
||||
|
||||
In rare cases errors, failures or warnings during the automatic inspection are acceptable. Reasons can be changes in the programming language, special cases which cannot, are difficult or must be individually configured in the inspection settings. If this is the case for a code change and if inspection configuration changes are necessary are decided by the senior developer performing the code review.
|
||||
|
||||
Automated checks which are run during the review process:
|
||||
|
||||
```sh
|
||||
php ./vendor/bin/phpcs --severity=1 ./ --standard="Build/Config/phpcs.xml"
|
||||
npx eslint ./ -c ./Build/Config/.eslintrc.json
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
Code changes must follow the inspection guidelines (i.e. code coverage) mentioned in the [inspection documentation](https://github.com/Karaka-Management/Developer-Guide/blob/develop/quality/inspections.md). Developers should check if the code changes comply with the inspection guidelines before submitting them.
|
||||
|
||||
In rare cases it might be not possible to follow the inspection guidelines. In such cases the senior developer performing the code review may decide if the code change still gets accepted.
|
||||
|
||||
Automated tests which are run during the review process:
|
||||
|
||||
```sh
|
||||
php ./vendor/bin/phpunit -c tests/PHPUnit/phpunit_default.xml
|
||||
php ./vendor/bin/phpstan analyse --autoload-file=phpOMS/Autoloader.php -l 9 -c Build/Config/phpstan.neon ./
|
||||
npx jasmine-node ./
|
||||
./cOMS/tests/test.sh
|
||||
```
|
||||
|
||||
Additional inspections which are run but might be ignored during the review depending on the use case are mentioned in the [inspection documentation](https://github.com/Karaka-Management/Developer-Guide/blob/develop/quality/inspections.md) as other checks.
|
||||
|
||||
### Demo
|
||||
|
||||
Some code changes may also require changes or extensions in the demo setup scripts. The demo setup script try to simulate a real world use case by generating and modifying mostly random data. This is also a good way to setup and “manually” test the code changes in a larger picture. The demo setup script can be found in the [demoSetup](https://github.com/Karaka-Management/demoSetup) repository. The demo setup script takes a long time due to the large amount of user input simulated data which is generated. Therefore it is recommended to run this only sporadically.
|
||||
|
||||
### Code review
|
||||
|
||||
In addition to the automatic code review performed by the various inspection tools such as (phpcs, phpstan, phpunit, eslint and custom scripts) a senior developer must check the proposed code change before it is merged with the respective `develop` branch. Only upon the approval by the reviewer a code change requests gets merged as no other developers have permission in the software to make such code merges.
|
||||
|
||||
In case a code change request is not approved the reviewer states the reason for the decision, this may include some tips and requests which will allow the contributor to make improvements so that the code change may get approved.
|
||||
|
||||
If the code reviewer only finds minor issues with the proposed code change the reviewer may make small changes to the proposed code change and inform the contributor to speed up the implementation process. Code reviewers are encouraged to do this with new contributors to avoid long iteration processes and to not discourage new developers. However, communication is key and severe issues with code change requests or if the contributor already made multiple code change requests in the past the reviewer should not implement the improvements by himself and rather decline the code change requests with his reasoning.
|
||||
|
||||
### Release flow
|
||||
|
||||
Code changes must be performed in a new branch. A new branch can be created with:
|
||||
|
||||
```sh
|
||||
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
|
||||
* `bug-*` for bug fixes
|
||||
* `security-*` for security related fixes/improvements
|
||||
* `general-*` for general improvements (i.e. code documentation improvements, code style improvements)
|
||||
|
||||
The senior developer who performs the code review merges the change request into the `develop` branch upon approval.
|
||||
|
|
@ -45,19 +45,19 @@ 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;
|
||||
|
||||
/**
|
||||
* Set option by key.
|
||||
*
|
||||
* @param array<int, array{id?:?int, name?:?string, content:string, module?:?string, group?:?int, account?:?int}> $options Column values for filtering
|
||||
* @param bool $store Save this Setting immediately to database
|
||||
* @param array<int, mixed> $options Column values for filtering
|
||||
* @param bool $store Save this Setting immediately to database
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
|
|
@ -68,7 +68,7 @@ interface SettingsInterface extends OptionsInterface
|
|||
/**
|
||||
* Save options.
|
||||
*
|
||||
* @param array<int, array{id?:?int, name?:?string, content:string, module?:?string, group?:?int, account?:?int}> $options Options to save
|
||||
* @param array<int, mixed> $options Options to save
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ final class NullCache extends ConnectionAbstract
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function connect(array $data = null) : void
|
||||
public function connect(?array $data = null) : void
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -35,14 +35,6 @@ abstract class BuilderAbstract
|
|||
*/
|
||||
protected bool $isReadOnly = false;
|
||||
|
||||
/**
|
||||
* Grammar.
|
||||
*
|
||||
* @var GrammarAbstract
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected GrammarAbstract $grammar;
|
||||
|
||||
/**
|
||||
* Database connection.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ final class NullConnection extends ConnectionAbstract
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function connect(array $dbdata = null) : void
|
||||
public function connect(?array $dbdata = null) : void
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -286,7 +244,7 @@ abstract class GrammarAbstract
|
|||
}
|
||||
|
||||
return $values . $this->compileValue($query, $value[$count]) . ')';
|
||||
} elseif ($value instanceof \DateTime) {
|
||||
} elseif ($value instanceof \DateTime || $value instanceof \DateTimeImmutable) {
|
||||
return $query->quote($value->format($this->datetimeFormat));
|
||||
} elseif ($value === null) {
|
||||
return 'NULL';
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,30 @@ abstract class DataMapperAbstract
|
|||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column name of the index
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected string $indexedBy = '';
|
||||
|
||||
/**
|
||||
* Set column name where the id is defined
|
||||
*
|
||||
* @param string $index Column name of the index
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function indexedBy(string $index) : self
|
||||
{
|
||||
$this->indexedBy = $index;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a query which is merged with the internal query generation.
|
||||
*
|
||||
|
|
@ -152,7 +191,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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<T> $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<T> $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<T> $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<T> $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,30 @@ class DataMapperFactory
|
|||
return static::FACTORY::createWith($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapper uses a factory
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function hasFactory() : bool
|
||||
{
|
||||
return !empty(static::FACTORY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base model class name
|
||||
*
|
||||
* @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 +509,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 +535,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 +581,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 +589,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 +648,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 +700,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 +743,7 @@ class DataMapperFactory
|
|||
];
|
||||
}
|
||||
|
||||
if (\reset($data)->getId() === $id) {
|
||||
if (\reset($data)->id === $id) {
|
||||
\array_shift($data);
|
||||
$hasPrevious = true;
|
||||
--$count;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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,13 +162,26 @@ 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();
|
||||
if ($sth === false) {
|
||||
throw new \Exception();
|
||||
}
|
||||
|
||||
$deadlock = 0;
|
||||
do {
|
||||
$repeat = false;
|
||||
try {
|
||||
++$deadlock;
|
||||
$sth->execute();
|
||||
} catch (\Throwable $t) {
|
||||
if ($deadlock > 3 || $t->errorInfo[1] !== 1213) {
|
||||
throw $t;
|
||||
}
|
||||
|
||||
\usleep(10000);
|
||||
$repeat = true;
|
||||
}
|
||||
} while ($repeat);
|
||||
} catch (\Throwable $t) {
|
||||
// @codeCoverageIgnoreStart
|
||||
\phpOMS\Log\FileLogger::getInstance()->error(
|
||||
|
|
@ -196,11 +210,48 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
/** @var class-string<DataMapperFactory> $mapper */
|
||||
$mapper = $this->mapper::BELONGS_TO[$propertyName]['mapper'];
|
||||
|
||||
/** @var self $relMapper */
|
||||
$relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
|
||||
$relMapper->depth = $this->depth + 1;
|
||||
if (isset($this->with[$propertyName])) {
|
||||
/** @var self $relMapper */
|
||||
$relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
|
||||
$relMapper->depth = $this->depth + 1;
|
||||
|
||||
return $relMapper->execute($obj);
|
||||
$id = $relMapper->execute($obj);
|
||||
|
||||
if (!isset($this->mapper::OWNS_ONE[$propertyName]['by'])) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
if ($this->mapper::OWNS_ONE[$propertyName]['private'] ?? false) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
$refProp = $refClass->getProperty($this->mapper::OWNS_ONE[$propertyName]['by']);
|
||||
$value = $refProp->getValue($obj);
|
||||
} else {
|
||||
$value = $obj->{$this->mapper::OWNS_ONE[$propertyName]['by']};
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
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']);
|
||||
$obj = $refProp->getValue($obj);
|
||||
} else {
|
||||
$obj = $obj->{$this->mapper::BELONGS_TO[$propertyName]['by']};
|
||||
}
|
||||
|
||||
if (!\is_object($obj)) {
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
$id = $mapper::getObjectId($obj);
|
||||
|
||||
return empty($id) && $mapper::isNullModel($obj)
|
||||
? null
|
||||
: $id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -218,11 +269,48 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
/** @var class-string<DataMapperFactory> $mapper */
|
||||
$mapper = $this->mapper::OWNS_ONE[$propertyName]['mapper'];
|
||||
|
||||
/** @var self $relMapper */
|
||||
$relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
|
||||
$relMapper->depth = $this->depth + 1;
|
||||
if (isset($this->with[$propertyName])) {
|
||||
/** @var self $relMapper */
|
||||
$relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
|
||||
$relMapper->depth = $this->depth + 1;
|
||||
|
||||
return $relMapper->execute($obj);
|
||||
$id = $relMapper->execute($obj);
|
||||
|
||||
if (!isset($this->mapper::OWNS_ONE[$propertyName]['by'])) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
if ($this->mapper::OWNS_ONE[$propertyName]['private'] ?? false) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
$refProp = $refClass->getProperty($this->mapper::OWNS_ONE[$propertyName]['by']);
|
||||
$value = $refProp->getValue($obj);
|
||||
} else {
|
||||
$value = $obj->{$this->mapper::OWNS_ONE[$propertyName]['by']};
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (isset($this->mapper::OWNS_ONE[$propertyName]['by'])) {
|
||||
// has by (obj is stored as a different model e.g. model = profile but reference/db is account)
|
||||
if ($this->mapper::OWNS_ONE[$propertyName]['private'] ?? false) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
$refProp = $refClass->getProperty($this->mapper::OWNS_ONE[$propertyName]['by']);
|
||||
$obj = $refProp->getValue($obj);
|
||||
} else {
|
||||
$obj = $obj->{$this->mapper::OWNS_ONE[$propertyName]['by']};
|
||||
}
|
||||
|
||||
if (!\is_object($obj)) {
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
$id = $mapper::getObjectId($obj);
|
||||
|
||||
return empty($id) && $mapper::isNullModel($obj)
|
||||
? null
|
||||
: $id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -238,7 +326,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 +348,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 +432,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 +461,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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -162,7 +159,25 @@ final class WriteMapper extends DataMapperAbstract
|
|||
}
|
||||
|
||||
$sth = $this->db->con->prepare($query->toSql());
|
||||
$sth->execute();
|
||||
if ($sth === false) {
|
||||
throw new \Exception();
|
||||
}
|
||||
|
||||
$deadlock = 0;
|
||||
do {
|
||||
$repeat = false;
|
||||
try {
|
||||
++$deadlock;
|
||||
$sth->execute();
|
||||
} catch (\Throwable $t) {
|
||||
if ($deadlock > 3 || $t->errorInfo[1] !== 1213) {
|
||||
throw $t;
|
||||
}
|
||||
|
||||
\usleep(10000);
|
||||
$repeat = true;
|
||||
}
|
||||
} while ($repeat);
|
||||
|
||||
$objId = empty($id = $this->mapper::getObjectId($obj)) ? $this->db->con->lastInsertId() : $id;
|
||||
\settype($objId, $this->mapper::COLUMNS[$this->mapper::PRIMARYFIELD]['type']);
|
||||
|
|
@ -195,19 +210,35 @@ final class WriteMapper extends DataMapperAbstract
|
|||
*/
|
||||
private function createOwnsOne(string $propertyName, object $obj) : mixed
|
||||
{
|
||||
if (!\is_object($obj)) {
|
||||
return $obj;
|
||||
// @question This code prevents us from EVER creating an object with a 'by' reference since we always assume
|
||||
// that it already exists -> only return the custom reference id
|
||||
// See bug below.
|
||||
|
||||
// @todo We might also have to handle 'column'
|
||||
if (isset($this->mapper::OWNS_ONE[$propertyName]['by'])) {
|
||||
// has by (obj is stored as a different model e.g. model = profile but reference/db is account)
|
||||
if ($this->mapper::OWNS_ONE[$propertyName]['private'] ?? false) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
$refProp = $refClass->getProperty($this->mapper::OWNS_ONE[$propertyName]['by']);
|
||||
$obj = $refProp->getValue($obj);
|
||||
} else {
|
||||
$obj = $obj->{$this->mapper::OWNS_ONE[$propertyName]['by']};
|
||||
}
|
||||
|
||||
if (!\is_object($obj)) {
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var class-string<DataMapperFactory> $mapper */
|
||||
$mapper = $this->mapper::OWNS_ONE[$propertyName]['mapper'];
|
||||
$primaryKey = $mapper::getObjectId($obj);
|
||||
|
||||
if (empty($primaryKey)) {
|
||||
return $mapper::create(db: $this->db)->execute($obj);
|
||||
}
|
||||
|
||||
return $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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -222,16 +253,13 @@ final class WriteMapper extends DataMapperAbstract
|
|||
*/
|
||||
private function createBelongsTo(string $propertyName, object $obj) : mixed
|
||||
{
|
||||
if (!\is_object($obj)) {
|
||||
return $obj;
|
||||
}
|
||||
|
||||
$mapper = '';
|
||||
$primaryKey = 0;
|
||||
// @question This code prevents us from EVER creating an object with a 'by' reference since we always assume
|
||||
// that it already exists -> only return the custom reference id
|
||||
// See bug below.
|
||||
|
||||
// @todo We might also have to handle 'column'
|
||||
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']);
|
||||
|
|
@ -239,14 +267,21 @@ final class WriteMapper extends DataMapperAbstract
|
|||
} else {
|
||||
$obj = $obj->{$this->mapper::BELONGS_TO[$propertyName]['by']};
|
||||
}
|
||||
|
||||
if (!\is_object($obj)) {
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var class-string<DataMapperFactory> $mapper */
|
||||
$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 +297,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 +309,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 +319,12 @@ final class WriteMapper extends DataMapperAbstract
|
|||
|
||||
/** @var class-string<DataMapperFactory> $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 +335,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 +367,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 +382,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 +411,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'])
|
||||
|
|
@ -398,9 +435,25 @@ final class WriteMapper extends DataMapperAbstract
|
|||
}
|
||||
|
||||
$sth = $this->db->con->prepare($relQuery->toSql());
|
||||
if ($sth !== false) {
|
||||
$sth->execute();
|
||||
if ($sth === false) {
|
||||
throw new \Exception();
|
||||
}
|
||||
|
||||
$deadlock = 0;
|
||||
do {
|
||||
$repeat = false;
|
||||
try {
|
||||
++$deadlock;
|
||||
$sth->execute();
|
||||
} catch (\Throwable $t) {
|
||||
if ($deadlock > 3 || $t->errorInfo[1] !== 1213) {
|
||||
throw $t;
|
||||
}
|
||||
|
||||
\usleep(10000);
|
||||
$repeat = true;
|
||||
}
|
||||
} while ($repeat);
|
||||
} catch (\Throwable $t) {
|
||||
// @codeCoverageIgnoreStart
|
||||
\phpOMS\Log\FileLogger::getInstance()->error(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1284,8 +1296,8 @@ 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 mixed $values Values to compare with
|
||||
* @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) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user