mirror of
https://github.com/Karaka-Management/phpOMS.git
synced 2026-01-10 17:28:40 +00:00
commit
381bf25f13
64
.github/ISSUE_TEMPLATE/bugs.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/bugs.yml
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
assignees:
|
||||
- spl1nes
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
options:
|
||||
- Alpha (Default)
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Opera
|
||||
- Microsoft Edge
|
||||
- Other
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
|
@ -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 = clone $this->similarityMatrix;
|
||||
$this->availabilityMatrix = clone $this->similarityMatrix;
|
||||
|
||||
for ($c = 0; $c < $iterations; ++$c) {
|
||||
for ($i = 0; $i < $n; ++$i) {
|
||||
for ($k = 0; $k < $n; ++$k) {
|
||||
$max = \PHP_INT_MIN;
|
||||
for ($j = 0; $j < $k; ++$j) {
|
||||
if (($temp = $this->similarityMatrix[$i][$j] + $this->availabilityMatrix[$i][$j]) > $max) {
|
||||
$max = $temp;
|
||||
}
|
||||
}
|
||||
|
||||
for ($j = $k + 1; $j < $n; ++$j) {
|
||||
if (($temp = $this->similarityMatrix[$i][$j] + $this->availabilityMatrix[$i][$j]) > $max) {
|
||||
$max = $temp;
|
||||
}
|
||||
}
|
||||
|
||||
$this->responsibilityMatrix[$i][$k] = (1 - 0.9) * ($this->similarityMatrix[$i][$k] - $max) + 0.9 * $this->responsibilityMatrix[$i][$k];
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $n; ++$i) {
|
||||
for ($k = 0; $k < $n; ++$k) {
|
||||
$sum = 0.0;
|
||||
|
||||
if ($i === $k) {
|
||||
for ($j = 0; $j < $i; ++$j) {
|
||||
$sum += \max(0.0, $this->responsibilityMatrix[$j][$k]);
|
||||
}
|
||||
|
||||
for (++$j; $j < $n; ++$j) {
|
||||
$sum += \max(0.0, $this->responsibilityMatrix[$j][$k]);
|
||||
}
|
||||
|
||||
$this->availabilityMatrix[$i][$k] = (1 - 0.9) * $sum + 0.9 * $this->availabilityMatrix[$i][$k];
|
||||
} else {
|
||||
$max = \max($i, $k);
|
||||
$min = \min($i, $k);
|
||||
|
||||
for ($j = 0; $j < $min; ++$j) {
|
||||
$sum += \max(0.0, $this->responsibilityMatrix[$j][$k]);
|
||||
}
|
||||
|
||||
for ($j = $min + 1; $j < $max; ++$j) {
|
||||
$sum += \max(0.0, $this->responsibilityMatrix[$j][$k]);
|
||||
}
|
||||
|
||||
for ($j = $max + 1; $j < $n; ++$j) {
|
||||
$sum += \max(0.0, $this->responsibilityMatrix[$j][$k]);
|
||||
}
|
||||
|
||||
$this->availabilityMatrix[$i][$k] = (1 - 0.9) * \min(0.0, $this->responsibilityMatrix[$k][$k] + $sum) + 0.9 * $this->availabilityMatrix[$i][$k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find center points (exemplar)
|
||||
for ($i = 0; $i < $n; ++$i) {
|
||||
$temp = $this->responsibilityMatrix[$i][$i] + $this->availabilityMatrix[$i][$i];
|
||||
|
||||
if ($temp > 0) {
|
||||
$this->clusterCenters[$i] = $this->points[$i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nearest group for a point
|
||||
*
|
||||
* @param array<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 = clone $this->points;
|
||||
$points[] = $point;
|
||||
|
||||
$similarityMatrix = $this->createSimilarityMatrix($points);
|
||||
|
||||
$c = $this->findNearestGroup(
|
||||
$similarityMatrix,
|
||||
\count($points) - 1,
|
||||
);
|
||||
|
||||
return $this->clusterCenters[$c];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
if (!empty($this->clusters)) {
|
||||
return $this->clusters;
|
||||
}
|
||||
|
||||
$n = \count($this->points);
|
||||
for ($i = 0; $i < $n; ++$i) {
|
||||
$group = $this->findNearestGroup($this->points, $i);
|
||||
|
||||
$this->clusters[$group] = $this->points[$i];
|
||||
}
|
||||
|
||||
return $this->clusters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return $this->clusterCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,17 +14,109 @@ declare(strict_types=1);
|
|||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* The parent category of this clustering algorithm is hierarchical clustering.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license Base: MIT Copyright (c) 2020 Greene Laboratory
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./DivisiveClustering.php
|
||||
* @see ./clustering_overview.png
|
||||
* @see https://en.wikipedia.org/wiki/Hierarchical_clustering
|
||||
* @see https://github.com/greenelab/hclust/blob/master/README.md
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement
|
||||
* @todo Implement missing linkage functions
|
||||
*/
|
||||
final class AgglomerativeClustering
|
||||
final class AgglomerativeClustering implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Metric to calculate the distance between two points
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private \Closure $metric;
|
||||
|
||||
/**
|
||||
* Metric to calculate the distance between two points
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private \Closure $linkage;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param null|\Closure $metric metric to use for the distance between two points
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(?\Closure $metric = null, ?\Closure $linkage = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (PointInterface $a, PointInterface $b) {
|
||||
$aCoordinates = $a->coordinates;
|
||||
$bCoordinates = $b->coordinates;
|
||||
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
|
||||
$this->linkage = $linkage ?? function (array $a, array $b, array $distances) {
|
||||
return self::averageDistanceLinkage($a, $b, $distances);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum/Complete-Linkage clustering
|
||||
*/
|
||||
public static function maximumDistanceLinkage(array $setA, array $setB, array $distances) : float
|
||||
{
|
||||
$max = \PHP_INT_MIN;
|
||||
foreach ($setA as $a) {
|
||||
foreach ($setB as $b) {
|
||||
if ($distances[$a][$b] > $max) {
|
||||
$max = $distances[$a][$b];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum/Single-Linkage clustering
|
||||
*/
|
||||
public static function minimumDistanceLinkage(array $setA, array $setB, array $distances) : float
|
||||
{
|
||||
$min = \PHP_INT_MAX;
|
||||
foreach ($setA as $a) {
|
||||
foreach ($setB as $b) {
|
||||
if ($distances[$a][$b] < $min) {
|
||||
$min = $distances[$a][$b];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unweighted average linkage clustering (UPGMA)
|
||||
*/
|
||||
public static function averageDistanceLinkage(array $setA, array $setB, array $distances) : float
|
||||
{
|
||||
$distance = 0;
|
||||
foreach ($setA as $a) {
|
||||
$distance += \array_sum($distances[$a]);
|
||||
}
|
||||
|
||||
return $distance / \count($setA) / \count($setB);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ namespace phpOMS\Algorithm\Clustering;
|
|||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class Birch
|
||||
final class Birch implements ClusteringInterface
|
||||
{
|
||||
}
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,18 @@ namespace phpOMS\Algorithm\Clustering;
|
|||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* The parent category of this clustering algorithm is hierarchical clustering.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./AgglomerativeClustering.php
|
||||
* @see ./clustering_overview.png
|
||||
* @see https://en.wikipedia.org/wiki/Hierarchical_clustering
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class Ward
|
||||
final class DivisiveClustering implements ClusteringInterface
|
||||
{
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ use phpOMS\Math\Topology\MetricsND;
|
|||
* @see ./clustering_overview.png
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Kmeans
|
||||
final class Kmeans implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Epsilon for float comparison.
|
||||
|
|
@ -49,7 +49,23 @@ final class Kmeans
|
|||
* @var PointInterface[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private $clusterCenters = [];
|
||||
private array $clusterCenters = [];
|
||||
|
||||
/**
|
||||
* Points of the clusters
|
||||
*
|
||||
* @var PointInterface[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusters = [];
|
||||
|
||||
/**
|
||||
* Points
|
||||
*
|
||||
* @var PointInterface[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $points = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
|
@ -58,7 +74,7 @@ final class Kmeans
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(\Closure $metric = null)
|
||||
public function __construct(?\Closure $metric = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (PointInterface $a, PointInterface $b) {
|
||||
$aCoordinates = $a->coordinates;
|
||||
|
|
@ -66,18 +82,10 @@ final class Kmeans
|
|||
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
|
||||
//$this->generateClusters($points, $clusters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the cluster for a point
|
||||
*
|
||||
* @param PointInterface $point Point to find the cluster for
|
||||
*
|
||||
* @return null|PointInterface Cluster center point
|
||||
*
|
||||
* @since 1.0.0
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(PointInterface $point) : ?PointInterface
|
||||
{
|
||||
|
|
@ -95,17 +103,21 @@ final class Kmeans
|
|||
}
|
||||
|
||||
/**
|
||||
* Get cluster centroids
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return $this->clusterCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the clusters of the points
|
||||
*
|
||||
|
|
@ -118,6 +130,7 @@ final class Kmeans
|
|||
*/
|
||||
public function generateClusters(array $points, int $clusters) : void
|
||||
{
|
||||
$this->points = $points;
|
||||
$n = \count($points);
|
||||
$clusterCenters = $this->kpp($points, $clusters);
|
||||
$coordinates = \count($points[0]->coordinates);
|
||||
|
|
@ -215,7 +228,7 @@ final class Kmeans
|
|||
|
||||
foreach ($points as $key => $point) {
|
||||
$d[$key] = $this->nearestClusterCenter($point, $clusters)[1];
|
||||
$sum += $d[$key];
|
||||
$sum += $d[$key];
|
||||
}
|
||||
|
||||
$sum *= \mt_rand(0, \mt_getrandmax()) / \mt_getrandmax();
|
||||
|
|
@ -245,4 +258,21 @@ final class Kmeans
|
|||
|
||||
return $clusters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
if (!empty($this->clusters)) {
|
||||
return $this->clusters;
|
||||
}
|
||||
|
||||
foreach ($this->points as $point) {
|
||||
$c = $this->cluster($point);
|
||||
$this->clusters[$c] = $point;
|
||||
}
|
||||
|
||||
return $this->clusters;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 @@ namespace phpOMS\Algorithm\Clustering;
|
|||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class SpectralClustering
|
||||
final class SpectralClustering implements ClusteringInterface
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -25,22 +25,67 @@ use phpOMS\Math\Stochastic\Distribution\NormalDistribution;
|
|||
* @since 1.0.0
|
||||
* @see https://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf
|
||||
*
|
||||
* @todo implement https://github.com/sublee/trueskill/blob/master/trueskill/__init__.py
|
||||
* @todo Implement https://github.com/sublee/trueskill/blob/master/trueskill/__init__.py
|
||||
* https://github.com/Karaka-Management/phpOMS/issues/337
|
||||
*/
|
||||
class TrueSkill
|
||||
{
|
||||
public int $DEFAULT_MU = 25;
|
||||
public const DEFAULT_MU = 25;
|
||||
|
||||
public float $DEFAULT_SIGMA = 25 / 3;
|
||||
public const DEFAULT_SIGMA = 25 / 3;
|
||||
|
||||
public float $DEFAULT_BETA = 25 / 3 / 2;
|
||||
public const DEFAULT_BETA = 25 / 3 / 2;
|
||||
|
||||
public float $DEFAULT_TAU = 25 / 3 / 100;
|
||||
public const DEFAULT_TAU = 25 / 3 / 100;
|
||||
|
||||
public float $DEFAULT_DRAW_PROBABILITY = 0.1;
|
||||
public const DEFAULT_DRAW_PROBABILITY = 0.1;
|
||||
|
||||
public function __construct()
|
||||
private float $mu = 0.0;
|
||||
|
||||
private float $sigma = 0.0;
|
||||
|
||||
private float $beta = 0.0;
|
||||
|
||||
private float $tau = 0.0;
|
||||
|
||||
private float $drawProbability = 0.0;
|
||||
|
||||
public function __construct(
|
||||
?float $mu = null,
|
||||
?float $sigma = null,
|
||||
?float $beta = null,
|
||||
?float $tau = null,
|
||||
?float $drawProbability = null)
|
||||
{
|
||||
$this->mu = $mu ?? self::DEFAULT_MU;
|
||||
$this->sigma = $sigma ?? self::DEFAULT_SIGMA;
|
||||
$this->beta = $beta ?? self::DEFAULT_BETA;
|
||||
$this->tau = $tau ?? self::DEFAULT_TAU;
|
||||
$this->drawProbability = $drawProbability ?? self::DEFAULT_DRAW_PROBABILITY;
|
||||
}
|
||||
|
||||
public function winProbability(array $team1, array $team2, float $drawMargin = 0.0)
|
||||
{
|
||||
$sigmaSum = 0.0;
|
||||
$mu1 = 0.0;
|
||||
foreach ($team1 as $player) {
|
||||
$mu1 += $player->mu;
|
||||
$sigmaSum += $player->sigma * $player->sigma;
|
||||
}
|
||||
|
||||
$mu2 = 0.0;
|
||||
foreach ($team2 as $player) {
|
||||
$mu2 += $player->mu;
|
||||
$sigmaSum += $player->sigma * $player->sigma;
|
||||
}
|
||||
|
||||
$deltaMu = $mu1 - $mu2;
|
||||
|
||||
return NormalDistribution::getCdf(
|
||||
($deltaMu - $drawMargin) / \sqrt((\count($team1) + \count($team2)) * ($this->beta * $this->beta) + $sigmaSum),
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// Draw margin = epsilon
|
||||
|
|
|
|||
0
Algorithm/Rating/TrueSkillFactoryGraph.php
Normal file
0
Algorithm/Rating/TrueSkillFactoryGraph.php
Normal file
|
|
@ -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\DHLShipping();
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ final class BayesianPersonalizedRanking
|
|||
|
||||
// num_factors determines the dimensionality of the latent factor space.
|
||||
// learning_rate controls the step size for updating the latent factors during optimization.
|
||||
// regularization prevents overfitting by adding a penalty for large parameter values.
|
||||
// regularization prevents over-fitting by adding a penalty for large parameter values.
|
||||
public function __construct(int $numFactors, float $learningRate, float $regularization)
|
||||
{
|
||||
$this->numFactors = $numFactors;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
44
Business/Warehouse/OrderSuggestion.php
Normal file
44
Business/Warehouse/OrderSuggestion.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?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
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
164
CONTRIBUTING.md
164
CONTRIBUTING.md
|
|
@ -1,12 +1,16 @@
|
|||
# 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).
|
||||
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).
|
||||
Every organization member and contributor to the organization must follow the [Code of Conduct](../Policies%20&%20Guidelines/Code%20of%20Conduct.md).
|
||||
|
||||
## Becoming a contributor
|
||||
|
||||
For public repositories you can immediately start by creating forks and pull requests. For private repositories which are necessary to setup the complete developer environment, feel free to request access. Please not that we may not immediately give you access to private repositories and instead will give you smaller tasks regarding public repositories. Please contact info@jingga.app for more details. (**R1**)
|
||||
|
||||
For all contributions our [Contributor License Agreement "CLA"](https://github.com/Karaka-Management/Organization-Guide/blob/master/Processes/HR/Hiring/Individual%20Contributor%20License%20Agreement.md) comes into effect. (**R2**)
|
||||
|
||||
## Code changes
|
||||
|
||||
|
|
@ -14,58 +18,106 @@ Every organization member and contributor to the organization must follow the [c
|
|||
|
||||
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.
|
||||
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)
|
||||
Open tasks can be found in the project overview: [Todos](https://github.com/orgs/Karaka-Management/projects/10)
|
||||
|
||||
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.
|
||||
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.
|
||||
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
|
||||
### Quality
|
||||
|
||||
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.
|
||||
#### Code style
|
||||
|
||||
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.
|
||||
Code changes must follow the [style guidelines](https://github.com/Karaka-Management/Developer-Guide/tree/develop/standards) (**R3**). 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. (**R4**)
|
||||
|
||||
Automated checks which are run during the review process:
|
||||
In rare cases errors, failures or warnings during the automatic inspection are acceptable. Reasons can be for example special cases which are difficult automatize 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. (**R5**)
|
||||
|
||||
Automated checks which are run during the review process (**R4**):
|
||||
|
||||
```sh
|
||||
php ./vendor/bin/phpcs --severity=1 ./ --standard="Build/Config/phpcs.xml"
|
||||
php ./vendor/bin/phpcs ./ --standard="Build/Config/phpcs.xml"
|
||||
php ./vendor/bin/php-cs-fixer fix ./ --config=Build/Config/.php-cs-fixer.php --allow-risky=yes
|
||||
php ./vendor/bin/phpcbf --standard=Build/Config/phpcs.xml ./
|
||||
php ./vendor/bin/rector process --dry-run --config Build/Config/rector.php ./
|
||||
npx eslint ./ -c ./Build/Config/.eslintrc.json
|
||||
```
|
||||
|
||||
### Tests
|
||||
#### 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.
|
||||
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) (**R6**). 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. (**R7**)
|
||||
|
||||
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.
|
||||
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. (**R8**)
|
||||
|
||||
Automated tests which are run during the review process:
|
||||
Automated tests which are run during the review process (**R7**):
|
||||
|
||||
```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 ./
|
||||
php ./vendor/bin/phpstan analyse --no-progress -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.
|
||||
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. (**R7**)
|
||||
|
||||
### Demo
|
||||
#### Performance
|
||||
|
||||
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.
|
||||
Developers should occasionaly check performance statistics. At this point no target metrics are defined.
|
||||
|
||||
### Code review
|
||||
Since the primary application is a web based application a similar tool as the Google lighthouse tool can be used to inspect the application for best practicies which can significantly improve the application performance. The sitespeed.io tool shows potential performance improvements and slow pages. With the php trace and profiler enabled in the `php.ini` file the VM automatically generates profiling and trace reports for every web request. These can be found in the webgrind logs directory and inspected in webgrind and dropped into the trace visualizer for a flame chart visualization. With mysqldumpslow you can inspect slow sql queries which may need optimization.
|
||||
|
||||
1. Automatic trace and benchmark generation with every web request in `/var/www/html/webgrind/Logs`
|
||||
2. Webgrind view `http://vm_ip:82`
|
||||
3. Trace visualization `http://vm_ip:81`
|
||||
1. Download the latest trace from `http://vm_ip:82/Logs`
|
||||
2. Drag and drop that downloaded `*.xt` file in the trace visualizer
|
||||
4. `sitespeed.io ./Build/Helper/Scripts/sitespeedDemoUrls.txt -b chrome --outputFolder /var/www/html/sitespeed`
|
||||
5. Slow query inspection.
|
||||
|
||||
```sh
|
||||
mysqldumpslow -t 10 /var/log/mysql/mysql-slow.log
|
||||
mysqldumpslow -t 10 -s l /var/log/mysql/mysql-slow.log
|
||||
```
|
||||
|
||||
#### 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.
|
||||
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. (**R5**+**R8**)
|
||||
|
||||
#### 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. (**R9**)
|
||||
|
||||
```sh
|
||||
sudo -u www-data php -dxdebug.remote_enable=1 -dxdebug.start_with_request=yes -dxdebug.mode=coverage,develop,debug demoSetup/setup.php
|
||||
```
|
||||
|
||||
#### Documentation
|
||||
|
||||
Occasionally new code or code changes also require new documentation or documentation changes. Developers should make sure that the new code is also reflected in the existing documentation ([Developer-Guide](), [User-Guide]() and/or module documentation) or if additional documentation is necessary.
|
||||
|
||||
#### Improvements, features, bugs
|
||||
|
||||
If a developer (or employee in general) has an idea for an improvement, feature or finds a potential bug it should be reported at https://github.com/Karaka-Management/Karaka/issues. A senior developer has to check these issues and decide how to proceed with them. The decision how to proceed with the issue must be explained by the senior developer as a response in the issue. Possible steps are:
|
||||
|
||||
* Accept the issue and put the task into the [Todos](https://github.com/orgs/Karaka-Management/projects/10)
|
||||
* Dismiss the issue with an explanation
|
||||
|
||||
### Release flow
|
||||
|
||||
In case SCSS/CSS or JS files got changed they must get re-built locally before comitting the code change:
|
||||
|
||||
```sh
|
||||
npx esbuild Web/Backend/js/backend.js --bundle --outfile=Install/Application/Backend/js/backend.min.js --minify
|
||||
scss cssOMS/styles.scss > cssOMS/styles.css
|
||||
```
|
||||
|
||||
For JS you may also use the shorthand command `npm run build`.
|
||||
|
||||
Code changes must be performed in a new branch. A new branch can be created with:
|
||||
|
||||
```sh
|
||||
|
|
@ -75,8 +127,70 @@ git checkout -b new-branch-name
|
|||
The name of the branch can be chosen freely however it is recommended to follow the following branch naming conventions:
|
||||
|
||||
* `feature-*` for feature implementations
|
||||
* `hotfix-*` for security related fixes/improvements
|
||||
* `bug-*` for bug fixes
|
||||
* `security-*` for security related fixes/improvements
|
||||
* `general-*` for general improvements (i.e. code documentation improvements, code style improvements)
|
||||
* `general-*` for general improvements (i.e. documentation, code style & performance improvements)
|
||||
|
||||
The senior developer who performs the code review merges the change request into the `develop` branch upon approval.
|
||||
```mermaid
|
||||
%%{init: { 'gitGraph': {'mainBranchName': 'master'}} }%%
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix-xxx
|
||||
commit
|
||||
checkout master
|
||||
branch develop
|
||||
checkout master
|
||||
merge hotfix-xxx
|
||||
checkout develop
|
||||
branch bug-xxx
|
||||
commit
|
||||
commit
|
||||
checkout hotfix-xxx
|
||||
commit
|
||||
checkout master
|
||||
merge hotfix-xxx
|
||||
checkout develop
|
||||
merge bug-xxx
|
||||
commit
|
||||
checkout develop
|
||||
branch feature-xxx
|
||||
commit
|
||||
commit
|
||||
commit
|
||||
checkout develop
|
||||
merge feature-xxx
|
||||
checkout master
|
||||
merge develop
|
||||
checkout develop
|
||||
branch general-xxx
|
||||
commit
|
||||
checkout develop
|
||||
merge general-xxx
|
||||
branch security-xxx
|
||||
commit
|
||||
commit
|
||||
checkout develop
|
||||
merge security-xxx
|
||||
checkout master
|
||||
merge develop
|
||||
|
||||
```
|
||||
|
||||
The senior developer who performs the code review merges the change request into the `develop` branch after their successful code review. Unsuccessful reviews lead to change requests by the original developer, other developers who can make the requested changes, changes by the senior developer who performed the review, or dismissal of the changed code. (**R10**)
|
||||
|
||||
## Approved dependencies
|
||||
|
||||
### Customer dependencies
|
||||
|
||||
Developers may only rely on the dependencies defined in [Approved Customer Software]() when developing a solution. If new software should be added to this list or a different version is required developers should make a request with their team leader/head of department who forwards this requests if appropriate to the CTO and explain the reasoning for the different dependency needs. The CTO can decide if the dependency will be accepted. (**R11**)
|
||||
|
||||
### Developer dependencies
|
||||
|
||||
Developers may only rely on the dependencies defined in [IT Equipment & Software](). If new software should be added to this list or a different version is required developers should make a request with their team leader/head of department who forwards this requests if appropriate to the CTO and explain the reasoning for the different dependency needs. The CTO can decide if the dependency will be accepted. Changing the package managers such as `composer.json` or `package.json` is not allowed by anyone else than the CTO. (**R12**)
|
||||
|
||||
## Other related documents
|
||||
|
||||
* [Confidentiality Policy](../Policies%20&%20Guidelines/Confidentiality%20Policy.md)
|
||||
* [Organization Activity Policy](../Policies%20&%20Guidelines/Organization%20Activity%20Policy.md)
|
||||
* [Tutorials](./Development/Tutorials)
|
||||
|
|
@ -45,12 +45,12 @@ interface SettingsInterface extends OptionsInterface
|
|||
*/
|
||||
public function get(
|
||||
mixed $ids = null,
|
||||
string | array $names = null,
|
||||
int $unit = null,
|
||||
int $app = null,
|
||||
string $module = null,
|
||||
int $group = null,
|
||||
int $account = null
|
||||
string | array|null $names = null,
|
||||
?int $unit = null,
|
||||
?int $app = null,
|
||||
?string $module = null,
|
||||
?int $group = null,
|
||||
?int $account = null
|
||||
) : mixed;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,15 @@ abstract class DataMapperAbstract
|
|||
$this->db = $db;
|
||||
}
|
||||
|
||||
protected string $indexedBy = '';
|
||||
|
||||
public function indexedBy(string $index) : self
|
||||
{
|
||||
$this->indexedBy = $index;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a query which is merged with the internal query generation.
|
||||
*
|
||||
|
|
@ -152,7 +176,7 @@ abstract class DataMapperAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function query(Builder $query = null) : self
|
||||
public function query(?Builder $query = null) : self
|
||||
{
|
||||
$this->query = $query;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,25 @@ class DataMapperFactory
|
|||
return static::FACTORY::createWith($data);
|
||||
}
|
||||
|
||||
public static function hasFactory() : bool
|
||||
{
|
||||
return !empty(static::FACTORY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the empty base model
|
||||
*
|
||||
* @param null|array $data Data to use for initialization
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function getBaseModelClass() : string
|
||||
{
|
||||
return empty(static::MODEL) ? \substr(static::class, 0, -6) : static::MODEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id of object
|
||||
*
|
||||
|
|
@ -493,14 +504,12 @@ class DataMapperFactory
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function getObjectId(object $obj, string $member = null, \ReflectionClass &$refClass = null) : mixed
|
||||
public static function getObjectId(object $obj, ?string $member = null, ?\ReflectionClass &$refClass = null) : mixed
|
||||
{
|
||||
$propertyName = $member ?? static::COLUMNS[static::PRIMARYFIELD]['internal'];
|
||||
|
||||
if (static::COLUMNS[static::PRIMARYFIELD]['private'] ?? false) {
|
||||
if ($refClass === null) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
}
|
||||
$refClass ??= new \ReflectionClass($obj);
|
||||
|
||||
$refProp = $refClass->getProperty($propertyName);
|
||||
|
||||
|
|
@ -521,15 +530,13 @@ class DataMapperFactory
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function setObjectId(object $obj, mixed $objId, \ReflectionClass &$refClass = null) : void
|
||||
public static function setObjectId(object $obj, mixed $objId, ?\ReflectionClass &$refClass = null) : void
|
||||
{
|
||||
$propertyName = static::COLUMNS[static::PRIMARYFIELD]['internal'];
|
||||
\settype($objId, static::COLUMNS[static::PRIMARYFIELD]['type']);
|
||||
|
||||
if (static::COLUMNS[static::PRIMARYFIELD]['private'] ?? false) {
|
||||
if ($refClass === null) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
}
|
||||
$refClass ??= new \ReflectionClass($obj);
|
||||
|
||||
$refProp = $refClass->getProperty($propertyName);
|
||||
$refProp->setValue($obj, $objId);
|
||||
|
|
@ -569,7 +576,7 @@ class DataMapperFactory
|
|||
* @param int $pageLimit Limit result set
|
||||
* @param string $sortBy Model member name to sort by
|
||||
* @param string $sortOrder Sort order
|
||||
* @param array $searchFields Fields to search in. ([] = all) @todo: maybe change to all which have autocomplete = true defined?
|
||||
* @param array $searchFields Fields to search in. ([] = all) @todo maybe change to all which have autocomplete = true defined?
|
||||
* @param array $filters Additional search filters applied ['type', 'value1', 'logic1', 'value2', 'logic2']
|
||||
*
|
||||
* @return array{hasPrevious:bool, hasNext:bool, data:object[]}
|
||||
|
|
@ -577,18 +584,18 @@ class DataMapperFactory
|
|||
* @since 1.0.0
|
||||
*/
|
||||
public static function find(
|
||||
string $search = null,
|
||||
DataMapperAbstract $mapper = null,
|
||||
?string $search = null,
|
||||
?DataMapperAbstract $mapper = null,
|
||||
int $id = 0,
|
||||
string $secondaryId = '',
|
||||
string $type = null,
|
||||
?string $type = null,
|
||||
int $pageLimit = 25,
|
||||
string $sortBy = null,
|
||||
?string $sortBy = null,
|
||||
string $sortOrder = OrderType::DESC,
|
||||
array $searchFields = [],
|
||||
array $filters = []
|
||||
) : array {
|
||||
$mapper ??= static::getAll();
|
||||
$mapper ??= static::getAll();
|
||||
$sortOrder = \strtoupper($sortOrder);
|
||||
|
||||
$data = [];
|
||||
|
|
@ -636,7 +643,7 @@ class DataMapperFactory
|
|||
}
|
||||
}
|
||||
|
||||
// @todo: how to handle columns which are NOT members (columns which are manipulated)
|
||||
// @todo how to handle columns which are NOT members (columns which are manipulated)
|
||||
// Maybe pass callback array which can handle these cases?
|
||||
|
||||
if ($type === 'p') {
|
||||
|
|
@ -688,26 +695,15 @@ class DataMapperFactory
|
|||
--$count;
|
||||
}
|
||||
} else {
|
||||
if (\reset($data)->getId() === $id) {
|
||||
if (\reset($data)->id === $id) {
|
||||
\array_shift($data);
|
||||
$hasNext = true;
|
||||
--$count;
|
||||
}
|
||||
|
||||
if ($count > $pageLimit) {
|
||||
// @todo: can be maybe removed?
|
||||
/*
|
||||
if (!$hasNext) {
|
||||
\array_pop($data);
|
||||
$hasNext = true;
|
||||
--$count;
|
||||
}
|
||||
*/
|
||||
|
||||
if ($count > $pageLimit) {
|
||||
$hasPrevious = true;
|
||||
\array_pop($data);
|
||||
}
|
||||
$hasPrevious = true;
|
||||
\array_pop($data);
|
||||
}
|
||||
|
||||
$data = \array_reverse($data);
|
||||
|
|
@ -742,7 +738,7 @@ class DataMapperFactory
|
|||
];
|
||||
}
|
||||
|
||||
if (\reset($data)->getId() === $id) {
|
||||
if (\reset($data)->id === $id) {
|
||||
\array_shift($data);
|
||||
$hasPrevious = true;
|
||||
--$count;
|
||||
|
|
|
|||
|
|
@ -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,9 +162,6 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
}
|
||||
}
|
||||
|
||||
// @todo:
|
||||
// @bug: Sqlite doesn't allow table_name.column_name in set queries for whatver reason.
|
||||
|
||||
$sth = $this->db->con->prepare($query->toSql());
|
||||
if ($sth !== false) {
|
||||
$sth->execute();
|
||||
|
|
@ -196,6 +194,14 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
/** @var class-string<DataMapperFactory> $mapper */
|
||||
$mapper = $this->mapper::BELONGS_TO[$propertyName]['mapper'];
|
||||
|
||||
if (!isset($this->with[$propertyName])) {
|
||||
$id = $mapper::getObjectId($obj);
|
||||
|
||||
return empty($id) && $mapper::isNullModel($obj)
|
||||
? null
|
||||
: $id;
|
||||
}
|
||||
|
||||
/** @var self $relMapper */
|
||||
$relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
|
||||
$relMapper->depth = $this->depth + 1;
|
||||
|
|
@ -218,6 +224,14 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
/** @var class-string<DataMapperFactory> $mapper */
|
||||
$mapper = $this->mapper::OWNS_ONE[$propertyName]['mapper'];
|
||||
|
||||
if (!isset($this->with[$propertyName])) {
|
||||
$id = $mapper::getObjectId($obj);
|
||||
|
||||
return empty($id) && $mapper::isNullModel($obj)
|
||||
? null
|
||||
: $id;
|
||||
}
|
||||
|
||||
/** @var self $relMapper */
|
||||
$relMapper = $this->createRelationMapper($mapper::update(db: $this->db), $propertyName);
|
||||
$relMapper->depth = $this->depth + 1;
|
||||
|
|
@ -238,7 +252,7 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function updateHasMany(object $obj, mixed $objId, \ReflectionClass &$refClass = null) : void
|
||||
private function updateHasMany(object $obj, mixed $objId, ?\ReflectionClass &$refClass = null) : void
|
||||
{
|
||||
if (empty($this->with) || empty($this->mapper::HAS_MANY)) {
|
||||
return;
|
||||
|
|
@ -260,9 +274,7 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
$values = null;
|
||||
|
||||
if ($isPrivate) {
|
||||
if ($refClass === null) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
}
|
||||
$refClass ??= new \ReflectionClass($obj);
|
||||
|
||||
$property = $refClass->getProperty($propertyName);
|
||||
$values = $property->getValue($obj);
|
||||
|
|
@ -346,7 +358,7 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
$query = new Builder($this->db);
|
||||
$src = $many['external'] ?? $many['mapper']::PRIMARYFIELD;
|
||||
|
||||
// @todo: what if a specific column name is defined instead of primaryField for the join? Fix, it should be stored in 'column'
|
||||
// @todo what if a specific column name is defined instead of primaryField for the join? Fix, it should be stored in 'column'
|
||||
$query->select($many['table'] . '.' . $src)
|
||||
->from($many['table'])
|
||||
->where($many['table'] . '.' . $many['self'], '=', $objId);
|
||||
|
|
@ -375,7 +387,7 @@ final class UpdateMapper extends DataMapperAbstract
|
|||
$this->mapper::remover(db: $this->db)->deleteRelationTable($member, $removes, $objId);
|
||||
}
|
||||
|
||||
if (!empty($adds)) {
|
||||
if (!empty($adds) && isset($this->mapper::HAS_MANY[$member]['external'])) {
|
||||
$this->mapper::writer(db: $this->db)->createRelationTable($member, $adds, $objId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ use phpOMS\Utils\ArrayUtils;
|
|||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Lock data for concurrency (e.g. table row lock or heartbeat)
|
||||
* https://github.com/Karaka-Management/Karaka/issues/152
|
||||
*/
|
||||
final class WriteMapper extends DataMapperAbstract
|
||||
{
|
||||
|
|
@ -74,20 +77,16 @@ final class WriteMapper extends DataMapperAbstract
|
|||
*/
|
||||
public function executeCreate(object $obj) : mixed
|
||||
{
|
||||
$refClass = null;
|
||||
|
||||
if ($this->mapper::isNullModel($obj)) {
|
||||
$objId = $this->mapper::getObjectId($obj);
|
||||
|
||||
$objId = $this->mapper::getObjectId($obj);
|
||||
if ((!empty($objId) && $this->mapper::AUTOINCREMENT)
|
||||
|| $this->mapper::isNullModel($obj)
|
||||
) {
|
||||
return $objId === 0 ? null : $objId;
|
||||
}
|
||||
|
||||
if (!empty($id = $this->mapper::getObjectId($obj)) && $this->mapper::AUTOINCREMENT) {
|
||||
$objId = $id;
|
||||
} else {
|
||||
$objId = $this->createModel($obj, $refClass);
|
||||
$this->mapper::setObjectId($obj, $objId, $refClass);
|
||||
}
|
||||
$refClass = null;
|
||||
$objId = $this->createModel($obj, $refClass);
|
||||
$this->mapper::setObjectId($obj, $objId, $refClass);
|
||||
|
||||
$this->createHasMany($obj, $objId, $refClass);
|
||||
|
||||
|
|
@ -104,7 +103,7 @@ final class WriteMapper extends DataMapperAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createModel(object $obj, \ReflectionClass &$refClass = null) : mixed
|
||||
private function createModel(object $obj, ?\ReflectionClass &$refClass = null) : mixed
|
||||
{
|
||||
try {
|
||||
$query = new Builder($this->db);
|
||||
|
|
@ -123,9 +122,7 @@ final class WriteMapper extends DataMapperAbstract
|
|||
|
||||
$tValue = null;
|
||||
if ($column['private'] ?? false) {
|
||||
if ($refClass === null) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
}
|
||||
$refClass ??= new \ReflectionClass($obj);
|
||||
|
||||
$property = $refClass->getProperty($propertyName);
|
||||
$tValue = $property->getValue($obj);
|
||||
|
|
@ -231,7 +228,6 @@ final class WriteMapper extends DataMapperAbstract
|
|||
|
||||
if (isset($this->mapper::BELONGS_TO[$propertyName]['by'])) {
|
||||
// has by (obj is stored as a different model e.g. model = profile but reference/db is account)
|
||||
|
||||
if ($this->mapper::BELONGS_TO[$propertyName]['private'] ?? false) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
$refProp = $refClass->getProperty($this->mapper::BELONGS_TO[$propertyName]['by']);
|
||||
|
|
@ -245,8 +241,11 @@ final class WriteMapper extends DataMapperAbstract
|
|||
$mapper = $this->mapper::BELONGS_TO[$propertyName]['mapper'];
|
||||
$primaryKey = $mapper::getObjectId($obj);
|
||||
|
||||
// @todo: the $mapper::create() might cause a problem if 'by' is set. because we don't want to create this obj but the child obj.
|
||||
return empty($primaryKey) ? $mapper::create(db: $this->db)->execute($obj) : $primaryKey;
|
||||
// @bug The $mapper::create() might cause a problem if 'by' is set.
|
||||
// This is because we don't want to create this obj but the child obj.
|
||||
return empty($primaryKey)
|
||||
? $mapper::create(db: $this->db)->execute($obj)
|
||||
: $primaryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -262,7 +261,7 @@ final class WriteMapper extends DataMapperAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createHasMany(object $obj, mixed $objId, \ReflectionClass &$refClass = null) : void
|
||||
private function createHasMany(object $obj, mixed $objId, ?\ReflectionClass &$refClass = null) : void
|
||||
{
|
||||
foreach ($this->mapper::HAS_MANY as $propertyName => $rel) {
|
||||
if (!isset($this->mapper::HAS_MANY[$propertyName]['mapper'])) {
|
||||
|
|
@ -274,9 +273,7 @@ final class WriteMapper extends DataMapperAbstract
|
|||
$values = null;
|
||||
|
||||
if ($isPrivate) {
|
||||
if ($refClass === null) {
|
||||
$refClass = new \ReflectionClass($obj);
|
||||
}
|
||||
$refClass ??= new \ReflectionClass($obj);
|
||||
|
||||
$property = $refClass->getProperty($propertyName);
|
||||
$values = $property->getValue($obj);
|
||||
|
|
@ -286,16 +283,12 @@ final class WriteMapper extends DataMapperAbstract
|
|||
|
||||
/** @var class-string<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 +299,13 @@ final class WriteMapper extends DataMapperAbstract
|
|||
|
||||
$mapper::create(db: $this->db)->execute($values);
|
||||
continue;
|
||||
} elseif (!\is_array($values)) {
|
||||
// @todo: conditionals???
|
||||
} elseif (!\is_array($values) || empty($values)) {
|
||||
// @todo conditionals?
|
||||
continue;
|
||||
}
|
||||
|
||||
$objsIds = [];
|
||||
$isRelPrivate = $mapper::COLUMNS[$this->mapper::HAS_MANY[$propertyName]['self']]['private'] ?? false;
|
||||
$relReflectionClass = $isRelPrivate && !empty($values) ? new \ReflectionClass(\reset($values)) : null;
|
||||
$relReflectionClass = $isRelPrivate ? new \ReflectionClass(\reset($values)) : null;
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if (!\is_object($value)) {
|
||||
|
|
@ -339,14 +331,13 @@ final class WriteMapper extends DataMapperAbstract
|
|||
$relProperty = $relReflectionClass->getProperty($internalName);
|
||||
}
|
||||
|
||||
// @todo maybe consider to just set the column type to object, and then check for that (might be faster)
|
||||
if (isset($mapper::BELONGS_TO[$internalName])
|
||||
|| isset($mapper::OWNS_ONE[$internalName])
|
||||
) {
|
||||
if ($isRelPrivate) {
|
||||
$relProperty->setValue($value, $this->mapper::createNullModel($objId));
|
||||
} else {
|
||||
$value->{$internalName} = $this->mapper::createNullModel($objId);
|
||||
$value->{$internalName} = $this->mapper::createNullModel($objId);
|
||||
}
|
||||
} elseif ($isRelPrivate) {
|
||||
$relProperty->setValue($value, $objId);
|
||||
|
|
@ -355,10 +346,18 @@ final class WriteMapper extends DataMapperAbstract
|
|||
}
|
||||
}
|
||||
|
||||
// @performance This inserts one element at a time. SQL allows to insert multiple rows.
|
||||
// The problem with this is, that we then need to manually calculate the objIds
|
||||
// since lastInsertId returns the first generated id.
|
||||
// However, the current use case in Jingga only rarely has multiple hasMany during the creation
|
||||
// since we are calling the API individually.
|
||||
// https://github.com/Karaka-Management/phpOMS/issues/370
|
||||
$objsIds[$key] = $mapper::create(db: $this->db)->execute($value);
|
||||
}
|
||||
|
||||
$this->createRelationTable($propertyName, $objsIds, $objId);
|
||||
if (!empty($objsIds) && isset($this->mapper::HAS_MANY[$propertyName]['external'])) {
|
||||
$this->createRelationTable($propertyName, $objsIds, $objId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -376,9 +375,11 @@ final class WriteMapper extends DataMapperAbstract
|
|||
public function createRelationTable(string $propertyName, array $objsIds, mixed $objId) : void
|
||||
{
|
||||
try {
|
||||
/* This check got pulled out to avoid function call to begin with
|
||||
if (empty($objsIds) || !isset($this->mapper::HAS_MANY[$propertyName]['external'])) {
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
$relQuery = new Builder($this->db);
|
||||
$relQuery->into($this->mapper::HAS_MANY[$propertyName]['table'])
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use phpOMS\Algorithm\Graph\DependencyResolver;
|
|||
use phpOMS\Contract\SerializableInterface;
|
||||
use phpOMS\DataStorage\Database\BuilderAbstract;
|
||||
use phpOMS\DataStorage\Database\Connection\ConnectionAbstract;
|
||||
use phpOMS\DataStorage\Database\Query\Grammar\Grammar;
|
||||
|
||||
/**
|
||||
* Database query builder.
|
||||
|
|
@ -26,9 +27,22 @@ use phpOMS\DataStorage\Database\Connection\ConnectionAbstract;
|
|||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @question Consider to delete the builder but create a Select, Insert, ... builder
|
||||
* Then directly call the compileSelect + compileFrom ... from the toSql
|
||||
* This way the object generated would be much slimmer since we don't need to initialize empty data for
|
||||
* Insert etc. We also wouldn't have to call compileComponents since this would happen directly in toSql().
|
||||
*/
|
||||
class Builder extends BuilderAbstract
|
||||
{
|
||||
/**
|
||||
* Grammar.
|
||||
*
|
||||
* @var Grammar
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected Grammar $grammar;
|
||||
|
||||
/**
|
||||
* Log queries.
|
||||
*
|
||||
|
|
@ -62,7 +76,9 @@ class Builder extends BuilderAbstract
|
|||
public array $updates = [];
|
||||
|
||||
/**
|
||||
* Stupid work around because value needs to be not null for it to work in Grammar.
|
||||
* Deletes.
|
||||
*
|
||||
* @todo Find fix for stupid work around because value needs to be not null for it to work in Grammar.
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
|
|
@ -231,6 +247,8 @@ class Builder extends BuilderAbstract
|
|||
'similar to',
|
||||
'not similar to',
|
||||
'in',
|
||||
'exists',
|
||||
'not exists',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -352,7 +370,16 @@ class Builder extends BuilderAbstract
|
|||
$this->resolveJoinDependencies();
|
||||
}
|
||||
|
||||
$query = $this->grammar->compileQuery($this);
|
||||
$components = $this->grammar->compileComponents($this);
|
||||
$queryString = '';
|
||||
|
||||
foreach ($components as $component) {
|
||||
if ($component !== '') {
|
||||
$queryString .= $component . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
$query = \substr($queryString, 0, -1) . ';';
|
||||
|
||||
if (self::$log) {
|
||||
\phpOMS\Log\FileLogger::getInstance()->debug($query);
|
||||
|
|
@ -391,13 +418,13 @@ class Builder extends BuilderAbstract
|
|||
}
|
||||
|
||||
// add from to existing dependencies
|
||||
foreach ($this->from as $table => $from) {
|
||||
foreach ($this->from as $table => $_) {
|
||||
$dependencies[$table] = [];
|
||||
}
|
||||
|
||||
$resolved = DependencyResolver::resolve($dependencies);
|
||||
|
||||
// cyclomatic dependencies
|
||||
// cyclic dependencies
|
||||
if ($resolved === null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -536,7 +563,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function where(string | array | self $columns, string | array $operator = null, mixed $values = null, string | array $boolean = 'and') : self
|
||||
public function where(string | array | self $columns, string | array|null $operator = null, mixed $values = null, string | array $boolean = 'and') : self
|
||||
{
|
||||
if (!\is_array($columns)) {
|
||||
$columns = [$columns];
|
||||
|
|
@ -575,7 +602,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function andWhere(string | array | Where $where, string | array $operator = null, mixed $values = null) : self
|
||||
public function andWhere(string | array | Where $where, string | array|null $operator = null, mixed $values = null) : self
|
||||
{
|
||||
return $this->where($where, $operator, $values, 'and');
|
||||
}
|
||||
|
|
@ -591,7 +618,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function orWhere(string | array | self $where, string | array $operator = null, mixed $values = null) : self
|
||||
public function orWhere(string | array | self $where, string | array|null $operator = null, mixed $values = null) : self
|
||||
{
|
||||
return $this->where($where, $operator, $values, 'or');
|
||||
}
|
||||
|
|
@ -814,81 +841,88 @@ class Builder extends BuilderAbstract
|
|||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->grammar->compileQuery($this);
|
||||
}
|
||||
$components = $this->grammar->compileComponents($this);
|
||||
$queryString = '';
|
||||
|
||||
/**
|
||||
* Find query.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function find() : void
|
||||
{
|
||||
foreach ($components as $component) {
|
||||
if ($component !== '') {
|
||||
$queryString .= $component . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return \substr($queryString, 0, -1) . ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count results.
|
||||
*
|
||||
* @param string $table Table to count the result set
|
||||
* @param string $column Table to count the result set
|
||||
*
|
||||
* @return Builder
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function count(string $table = '*') : self
|
||||
public function count(string $column = '*', ?string $as = null) : self
|
||||
{
|
||||
/**
|
||||
* @todo
|
||||
* Don't do this as a string, create a new object $this->select(new Count($table)).
|
||||
* The parser should be able to handle this much better
|
||||
*/
|
||||
return $this->select('COUNT(' . $table . ')');
|
||||
return $as === null
|
||||
? $this->select('COUNT(' . $column . ')')
|
||||
: $this->selectAs('COUNT(' . $column . ')', $as);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select minimum.
|
||||
*
|
||||
* @return void
|
||||
* @return Builder
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function min() : void
|
||||
public function min(string $column = '*', ?string $as = null) : self
|
||||
{
|
||||
return $as === null
|
||||
? $this->select('MIN(' . $column . ')')
|
||||
: $this->selectAs('MIN(' . $column . ')', $as);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select maximum.
|
||||
*
|
||||
* @return void
|
||||
* @return Builder
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function max() : void
|
||||
public function max(string $column = '*', ?string $as = null) : self
|
||||
{
|
||||
return $as === null
|
||||
? $this->select('MAX(' . $column . ')')
|
||||
: $this->selectAs('MAX(' . $column . ')', $as);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select sum.
|
||||
*
|
||||
* @return void
|
||||
* @return Builder
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function sum() : void
|
||||
public function sum(string $column = '*', ?string $as = null) : self
|
||||
{
|
||||
return $as === null
|
||||
? $this->select('SUM(' . $column . ')')
|
||||
: $this->selectAs('SUM(' . $column . ')', $as);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select average.
|
||||
*
|
||||
* @return void
|
||||
* @return Builder
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function avg() : void
|
||||
public function avg(string $column = '*', ?string $as = null) : self
|
||||
{
|
||||
return $as === null
|
||||
? $this->select('AVG(' . $column . ')')
|
||||
: $this->selectAs('AVG(' . $column . ')', $as);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -974,7 +1008,7 @@ class Builder extends BuilderAbstract
|
|||
{
|
||||
\end($this->values);
|
||||
|
||||
$key = \key($this->values);
|
||||
$key = \key($this->values);
|
||||
$key ??= 0;
|
||||
|
||||
if (\is_array($value)) {
|
||||
|
|
@ -1062,28 +1096,6 @@ class Builder extends BuilderAbstract
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment value.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function increment() : void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement value.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function decrement() : void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Join.
|
||||
*
|
||||
|
|
@ -1095,7 +1107,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function join(string | self $table, string $type = JoinType::JOIN, string $alias = null) : self
|
||||
public function join(string | self $table, string $type = JoinType::JOIN, ?string $alias = null) : self
|
||||
{
|
||||
$this->joins[$alias ?? $table] = ['type' => $type, 'table' => $table, 'alias' => $alias];
|
||||
|
||||
|
|
@ -1112,7 +1124,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function leftJoin(string | self $table, string $alias = null) : self
|
||||
public function leftJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::LEFT_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1127,7 +1139,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function leftOuterJoin(string | self $table, string $alias = null) : self
|
||||
public function leftOuterJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::LEFT_OUTER_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1142,7 +1154,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function leftInnerJoin(string | self $table, string $alias = null) : self
|
||||
public function leftInnerJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::LEFT_INNER_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1157,7 +1169,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function rightJoin(string | self $table, string $alias = null) : self
|
||||
public function rightJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::RIGHT_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1172,7 +1184,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function rightOuterJoin(string | self $table, string $alias = null) : self
|
||||
public function rightOuterJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::RIGHT_OUTER_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1187,7 +1199,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function rightInnerJoin(string | self $table, string $alias = null) : self
|
||||
public function rightInnerJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::RIGHT_INNER_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1202,7 +1214,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function outerJoin(string | self $table, string $alias = null) : self
|
||||
public function outerJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::OUTER_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1217,7 +1229,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function innerJoin(string | self $table, string $alias = null) : self
|
||||
public function innerJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::INNER_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1232,7 +1244,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function crossJoin(string | self $table, string $alias = null) : self
|
||||
public function crossJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::CROSS_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1247,7 +1259,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function fullJoin(string | self $table, string $alias = null) : self
|
||||
public function fullJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::FULL_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1262,7 +1274,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function fullOuterJoin(string | self $table, string $alias = null) : self
|
||||
public function fullOuterJoin(string | self $table, ?string $alias = null) : self
|
||||
{
|
||||
return $this->join($table, JoinType::FULL_OUTER_JOIN, $alias);
|
||||
}
|
||||
|
|
@ -1285,7 +1297,7 @@ class Builder extends BuilderAbstract
|
|||
* @param string|array $columns Columns to join on
|
||||
* @param null|string|array $operator Comparison operator
|
||||
* @param null|string|array $values Values to compare with
|
||||
* @param string|array $boolean Concatonator
|
||||
* @param string|array $boolean Concatenation
|
||||
* @param null|string $table Table this belongs to
|
||||
*
|
||||
* @return Builder
|
||||
|
|
@ -1294,7 +1306,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function on(string | array $columns, string | array $operator = null, mixed $values = null, string | array $boolean = 'and', string $table = null) : self
|
||||
public function on(string | array $columns, string | array|null $operator = null, mixed $values = null, string | array $boolean = 'and', ?string $table = null) : self
|
||||
{
|
||||
if (!\is_array($columns)) {
|
||||
$columns = [$columns];
|
||||
|
|
@ -1305,7 +1317,7 @@ class Builder extends BuilderAbstract
|
|||
|
||||
$joinCount = \count($this->joins) - 1;
|
||||
$i = 0;
|
||||
$table ??= \array_keys($this->joins)[$joinCount];
|
||||
$table ??= \array_keys($this->joins)[$joinCount];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (isset($operator[$i]) && !\in_array(\strtolower($operator[$i]), self::OPERATORS)) {
|
||||
|
|
@ -1336,7 +1348,7 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function orOn(string | array $columns, string | array $operator = null, string | array $values = null) : self
|
||||
public function orOn(string | array $columns, string | array|null $operator = null, string | array|null $values = null) : self
|
||||
{
|
||||
return $this->on($columns, $operator, $values, 'or');
|
||||
}
|
||||
|
|
@ -1352,34 +1364,21 @@ class Builder extends BuilderAbstract
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function andOn(string | array $columns, string | array $operator = null, string | array $values = null) : self
|
||||
public function andOn(string | array $columns, string | array|null $operator = null, string | array|null $values = null) : self
|
||||
{
|
||||
return $this->on($columns, $operator, $values, 'and');
|
||||
}
|
||||
|
||||
/**
|
||||
* Merging query.
|
||||
*
|
||||
* Merging query in order to remove database query volume
|
||||
*
|
||||
* @return Builder
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function merge() : self
|
||||
{
|
||||
return clone($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() : ?\PDOStatement
|
||||
{
|
||||
$sth = null;
|
||||
$sql = '';
|
||||
|
||||
try {
|
||||
$sth = $this->connection->con->prepare($this->toSql());
|
||||
$sth = $this->connection->con->prepare($sql = $this->toSql());
|
||||
if ($sth === false) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1395,7 +1394,7 @@ class Builder extends BuilderAbstract
|
|||
// @codeCoverageIgnoreStart
|
||||
\phpOMS\Log\FileLogger::getInstance()->error(
|
||||
\phpOMS\Log\FileLogger::MSG_FULL, [
|
||||
'message' => $t->getMessage() . ':' . $this->toSql(),
|
||||
'message' => $t->getMessage() . ':' . $sql,
|
||||
'line' => __LINE__,
|
||||
'file' => self::class,
|
||||
]
|
||||
|
|
@ -1445,8 +1444,6 @@ class Builder extends BuilderAbstract
|
|||
{
|
||||
if (\is_string($column)) {
|
||||
return $column;
|
||||
} elseif ($column instanceof Column) {
|
||||
return $column->getColumn();
|
||||
} elseif ($column instanceof SerializableInterface) {
|
||||
return $column->serialize();
|
||||
} elseif ($column instanceof self) {
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\DataStorage\Database\Query;
|
||||
|
||||
/**
|
||||
* Database query builder.
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Column extends Builder
|
||||
{
|
||||
}
|
||||
8
DataStorage/Database/Query/From.php → DataStorage/Database/Query/ColumnName.php
Executable file → Normal file
8
DataStorage/Database/Query/From.php → DataStorage/Database/Query/ColumnName.php
Executable file → Normal file
|
|
@ -22,6 +22,12 @@ namespace phpOMS\DataStorage\Database\Query;
|
|||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class From extends Builder
|
||||
final class ColumnName
|
||||
{
|
||||
public string $name = '';
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
67
DataStorage/Database/Query/Concat.php
Normal file
67
DataStorage/Database/Query/Concat.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\DataStorage\Database\Query;
|
||||
|
||||
use phpOMS\DataStorage\Database\Connection\ConnectionAbstract;
|
||||
|
||||
/**
|
||||
* Database query builder.
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Concat extends Builder
|
||||
{
|
||||
public string $delim = '';
|
||||
|
||||
public string $as = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param ConnectionAbstract $connection Database connection
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(ConnectionAbstract $connection)
|
||||
{
|
||||
parent::__construct($connection);
|
||||
$this->type = QueryType::SELECT;
|
||||
}
|
||||
|
||||
public function columns(string $as, string $delim, ...$columns) : void
|
||||
{
|
||||
$this->delim = $delim;
|
||||
$this->as = $as;
|
||||
|
||||
$this->select($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toSql() : string
|
||||
{
|
||||
$query = $this->grammar->compileConcat($this, $this->selects);
|
||||
|
||||
if (self::$log) {
|
||||
\phpOMS\Log\FileLogger::getInstance()->debug($query);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\DataStorage\Database\Query;
|
||||
|
||||
/**
|
||||
* Database query builder.
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Count extends Builder
|
||||
{
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\DataStorage\Database\Query;
|
||||
|
||||
/**
|
||||
* Database query builder.
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Expression extends Builder
|
||||
{
|
||||
}
|
||||
|
|
@ -17,9 +17,7 @@ namespace phpOMS\DataStorage\Database\Query\Grammar;
|
|||
use phpOMS\DataStorage\Database\BuilderAbstract;
|
||||
use phpOMS\DataStorage\Database\GrammarAbstract;
|
||||
use phpOMS\DataStorage\Database\Query\Builder;
|
||||
use phpOMS\DataStorage\Database\Query\From;
|
||||
use phpOMS\DataStorage\Database\Query\QueryType;
|
||||
use phpOMS\DataStorage\Database\Query\Where;
|
||||
|
||||
/**
|
||||
* Database query grammar.
|
||||
|
|
@ -36,7 +34,7 @@ class Grammar extends GrammarAbstract
|
|||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function compileComponents(BuilderAbstract $query) : array
|
||||
public function compileComponents(BuilderAbstract $query) : array
|
||||
{
|
||||
/** @var Builder $query */
|
||||
|
||||
|
|
@ -182,6 +180,21 @@ class Grammar extends GrammarAbstract
|
|||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create concat
|
||||
*
|
||||
* @param Concat $query Builder
|
||||
* @param array $columns Columns
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function compileConcat(\phpOMS\DataStorage\Database\Query\Concat $query, array $columns) : string
|
||||
{
|
||||
return 'CONCAT(' . $this->expressionizeTableColumn($columns) . ') AS ' . $query->as;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile select.
|
||||
*
|
||||
|
|
@ -243,21 +256,21 @@ class Grammar extends GrammarAbstract
|
|||
* Compile where.
|
||||
*
|
||||
* @param Builder $query Builder
|
||||
* @param array $wheres Where elmenets
|
||||
* @param bool $first Is first element (usefull for nesting)
|
||||
* @param array $wheres Where elements
|
||||
* @param bool $first Is first element (useful for nesting)
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function compileWheres(Builder $query, array $wheres, bool $first = true) : string
|
||||
public function compileWheres(Builder $query, array $wheres, bool $first = true) : string
|
||||
{
|
||||
$expression = '';
|
||||
|
||||
foreach ($wheres as $where) {
|
||||
foreach ($where as $element) {
|
||||
$expression .= $this->compileWhereElement($element, $query, $first);
|
||||
$first = false;
|
||||
$first = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,7 +286,7 @@ class Grammar extends GrammarAbstract
|
|||
*
|
||||
* @param array $element Element data
|
||||
* @param Builder $query Query builder
|
||||
* @param bool $first Is first element (usefull for nesting)
|
||||
* @param bool $first Is first element (useful for nesting)
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
|
|
@ -282,58 +295,46 @@ class Grammar extends GrammarAbstract
|
|||
protected function compileWhereElement(array $element, Builder $query, bool $first = true) : string
|
||||
{
|
||||
$expression = '';
|
||||
$prefix = '';
|
||||
|
||||
if (!$first) {
|
||||
$expression = ' ' . \strtoupper($element['boolean']) . ' ';
|
||||
$prefix = ' ' . \strtoupper($element['boolean']) . ' ';
|
||||
}
|
||||
|
||||
if (\is_string($element['column'])) {
|
||||
$expression .= $this->compileSystem($element['column']);
|
||||
} elseif ($element['column'] instanceof \Closure) {
|
||||
$expression .= $element['column']();
|
||||
} elseif ($element['column'] instanceof Where) {
|
||||
$where = \rtrim($this->compileWhereQuery($element['column']), ';');
|
||||
$expression .= '(' . (\str_starts_with($where, 'WHERE ') ? \substr($where, 6) : $where) . ')';
|
||||
} elseif ($element['column'] instanceof Builder) {
|
||||
$expression .= '(' . \rtrim($element['column']->toSql(), ';') . ')';
|
||||
} elseif ($element['column'] instanceof \Closure) {
|
||||
$expression .= $element['column']();
|
||||
}
|
||||
|
||||
if (isset($element['value']) && (!empty($element['value']) || !\is_array($element['value']))) {
|
||||
// Handle null for IN (...)
|
||||
// This is not allowed and must be written as (IN (...) OR IS NULL)
|
||||
$isArray = \is_array($element['value']);
|
||||
$hasNull = false;
|
||||
if ($isArray && ($key = \array_search(null, $element['value'], true)) !== false) {
|
||||
$hasNull = true;
|
||||
unset($element['value'][$key]);
|
||||
|
||||
if (empty($element['value'])) {
|
||||
$element['operator'] = '=';
|
||||
$element['value'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($element['value']) && (!empty($element['value']) || !$isArray)) {
|
||||
$expression .= ' ' . \strtoupper($element['operator']) . ' ' . $this->compileValue($query, $element['value']);
|
||||
|
||||
if ($hasNull) {
|
||||
$expression = '(' . $expression . ' OR ' . $this->compileSystem($element['column']) . ' IS NULL)';
|
||||
}
|
||||
} elseif ($element['value'] === null && !($element['column'] instanceof Builder)) {
|
||||
$operator = $element['operator'] === '=' ? 'IS' : 'IS NOT';
|
||||
$operator = $element['operator'] === '=' ? 'IS' : 'IS NOT';
|
||||
$expression .= ' ' . $operator . ' ' . $this->compileValue($query, $element['value']);
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile where query.
|
||||
*
|
||||
* @param Where $where Where query
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function compileWhereQuery(Where $where) : string
|
||||
{
|
||||
return $where->toSql();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile from query.
|
||||
*
|
||||
* @param From $from Where query
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function compileFromQuery(From $from) : string
|
||||
{
|
||||
return $from->toSql();
|
||||
return $prefix . $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -414,7 +415,7 @@ class Grammar extends GrammarAbstract
|
|||
|
||||
foreach ($ons as $on) {
|
||||
$expression .= $this->compileOnElement($on, $query, $first);
|
||||
$first = false;
|
||||
$first = false;
|
||||
}
|
||||
|
||||
if ($expression === '') {
|
||||
|
|
@ -429,7 +430,7 @@ class Grammar extends GrammarAbstract
|
|||
*
|
||||
* @param array $element Element data
|
||||
* @param Builder $query Query builder
|
||||
* @param bool $first Is first element (usefull for nesting)
|
||||
* @param bool $first Is first element (useful for nesting)
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
|
|
@ -444,25 +445,27 @@ class Grammar extends GrammarAbstract
|
|||
}
|
||||
|
||||
if (\is_string($element['column'])) {
|
||||
// handle bug when no table is specified in the where column
|
||||
// @bug Handle bug when no table is specified in the where column
|
||||
if (\count($query->from) === 1 && \stripos($element['column'], '.') === false) {
|
||||
$element['column'] = $query->from[0] . '.' . $element['column'];
|
||||
}
|
||||
|
||||
$expression .= $this->compileSystem($element['column']);
|
||||
} elseif ($element['column'] instanceof \Closure) {
|
||||
$expression .= $element['column']();
|
||||
} elseif ($element['column'] instanceof Builder) {
|
||||
$expression .= '(' . $element['column']->toSql() . ')';
|
||||
} elseif ($element['column'] instanceof Where) {
|
||||
$expression .= '(' . \rtrim($this->compileWhereQuery($element['column']), ';') . ')';
|
||||
} elseif ($element['column'] instanceof \Closure) {
|
||||
$expression .= $element['column']();
|
||||
}
|
||||
|
||||
// @todo: on doesn't allow values as value (only table column names). This is bad and needs to be fixed!
|
||||
// @bug The on part of a join doesn't allow string values because they conflict with column name
|
||||
// Other data types are possible because they don't conflict with the data type of columns (string)
|
||||
// Consider to create a ColumnName() class.
|
||||
// https://github.com/Karaka-Management/phpOMS/issues/369
|
||||
if (isset($element['value'])) {
|
||||
$expression .= ' ' . \strtoupper($element['operator']) . ' ' . $this->compileSystem($element['value']);
|
||||
$expression .= ' ' . \strtoupper($element['operator']) . ' '
|
||||
. (\is_string($element['value']) ? $this->compileSystem($element['value']) : $element['value']);
|
||||
} else {
|
||||
$operator = $element['operator'] === '=' ? 'IS' : 'IS NOT';
|
||||
$operator = $element['operator'] === '=' ? 'IS' : 'IS NOT';
|
||||
$expression .= ' ' . $operator . ' ' . $this->compileValue($query, $element['value']);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query\Grammar
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\DataStorage\Database\Query\Grammar;
|
||||
|
||||
/**
|
||||
* Grammar interface.
|
||||
*
|
||||
* @package phpOMS\DataStorage\Database\Query\Grammar
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface GrammarInterface
|
||||
{
|
||||
}
|
||||
|
|
@ -60,6 +60,10 @@ class MysqlGrammar extends Grammar
|
|||
$expression = '*';
|
||||
}
|
||||
|
||||
return 'SELECT ' . $expression . ' ' . $this->compileFrom($query, $query->from) . ' ' . $this->compileWheres($query, $query->wheres) . ' ORDER BY \rand() ' . $this->compileLimit($query, $query->limit ?? 1);
|
||||
return 'SELECT ' . $expression
|
||||
. ' ' . $this->compileFrom($query, $query->from)
|
||||
. ' ' . $this->compileWheres($query, $query->wheres)
|
||||
. ' ORDER BY RAND() '
|
||||
. $this->compileLimit($query, $query->limit ?? 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,4 +62,22 @@ class SQLiteGrammar extends Grammar
|
|||
|
||||
return 'SELECT ' . $expression . ' ' . $this->compileFrom($query, $query->from) . ' ORDER BY RANDOM() ' . $this->compileLimit($query, $query->limit ?? 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create concat
|
||||
*
|
||||
* @param Concat $query Builder
|
||||
* @param array $columns Columns
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function compileConcat(\phpOMS\DataStorage\Database\Query\Concat $query, array $columns) : string
|
||||
{
|
||||
$sql = $this->expressionizeTableColumn($columns);
|
||||
$sql = \str_replace(',', ' ||', $sql);
|
||||
|
||||
return $sql . ' AS ' . $query->as;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user