From 17279594c617b44e7206afdccd789bc771404f1c Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Mon, 19 Sep 2022 09:43:49 -0400 Subject: [PATCH 1/9] PHP 8+ fix for Model::isBuilderMethod() --- src/Model.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Model.php b/src/Model.php index 50af4c3..50e7e84 100644 --- a/src/Model.php +++ b/src/Model.php @@ -4,8 +4,9 @@ use ArrayAccess; use JsonSerializable; -use Pace\XPath\Builder; use Pace\Model\Attachments; +use Pace\XPath\Builder; +use ReflectionMethod; use UnexpectedValueException; class Model implements ArrayAccess, JsonSerializable @@ -633,7 +634,16 @@ protected function guessPrimaryKey() */ protected function isBuilderMethod($name) { - return method_exists(Builder::class, $name) && is_callable([Builder::class, $name]); + if (version_compare(PHP_VERSION, '8.0.0', '>=')) { + if (method_exists(Builder::class, $name)) { + $reflection = new ReflectionMethod(Builder::class, $name); + return $reflection->isPublic(); + } else { + return false; + } + } else { + return method_exists(Builder::class, $name) && is_callable([Builder::class, $name]); + } } /** From 065b401aacd55a5c1c1b1539c2cf83ab8ac18e01 Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Thu, 13 Oct 2022 13:29:49 -0400 Subject: [PATCH 2/9] Switch to new Doctine Inflector API to support versions 1.4 and 2.0 --- composer.json | 2 +- src/Type.php | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index fe8c220..48a76be 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "ext-fileinfo": "*", "ext-soap": "*", "nesbot/carbon": "^1.20 || ^2.0", - "doctrine/inflector": "~1.0" + "doctrine/inflector": "^1.4 || ^2.0" }, "require-dev": { "phpunit/phpunit": "^8.5", diff --git a/src/Type.php b/src/Type.php index ec626b7..f66a9b1 100644 --- a/src/Type.php +++ b/src/Type.php @@ -2,7 +2,7 @@ namespace Pace; -use Doctrine\Common\Inflector\Inflector; +use Doctrine\Inflector\InflectorFactory; class Type { @@ -62,6 +62,13 @@ class Type 'FileAttachment' => 'attachment', ]; + /** + * The Doctrine Inflector instance. + * + * @var \Doctrine\Inflector\Inflector|null + */ + protected static $inflector; + /** * Convert a name to camel case. * @@ -92,7 +99,11 @@ public static function modelify($name) */ public static function singularize($name) { - return Inflector::singularize($name); + if (is_null(static::$inflector)) { + static::$inflector = InflectorFactory::create()->build(); + } + + return static::$inflector->singularize($name); } /** From ee9f1dd7b20421641cbab8d3c957a254dcb51340 Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Wed, 25 Jun 2025 15:31:14 -0400 Subject: [PATCH 3/9] Add Carbon 3 to composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 48a76be..f0f4061 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "php": ">=7.1.0", "ext-fileinfo": "*", "ext-soap": "*", - "nesbot/carbon": "^1.20 || ^2.0", + "nesbot/carbon": "^1.20 || ^2.0 || ^3.0", "doctrine/inflector": "^1.4 || ^2.0" }, "require-dev": { From b1b75849954bae3788d910de05c30ecdfb0e943a Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Wed, 18 Feb 2026 09:22:49 -0500 Subject: [PATCH 4/9] Implement invoke action service --- composer.json | 12 +-- src/Client.php | 15 +++- src/InvokeAction/InvokeActionRequest.php | 54 +++++++++++++ src/InvokeAction/InvokeActionResponse.php | 98 +++++++++++++++++++++++ src/Services/InvokeAction.php | 20 +++++ 5 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 src/InvokeAction/InvokeActionRequest.php create mode 100644 src/InvokeAction/InvokeActionResponse.php create mode 100644 src/Services/InvokeAction.php diff --git a/composer.json b/composer.json index f0f4061..91d98e5 100644 --- a/composer.json +++ b/composer.json @@ -9,16 +9,16 @@ ], "license": "MIT", "require": { - "php": ">=7.1.0", + "php": "^8.1", "ext-fileinfo": "*", "ext-soap": "*", - "nesbot/carbon": "^1.20 || ^2.0 || ^3.0", - "doctrine/inflector": "^1.4 || ^2.0" + "nesbot/carbon": "^2.0|^3.0", + "doctrine/inflector": "^2.1" }, "require-dev": { - "phpunit/phpunit": "^8.5", - "mockery/mockery": "^1.3", - "symfony/var-dumper": "^5.0" + "phpunit/phpunit": "^10.5|11.5|12.5|^13.0", + "mockery/mockery": "^1.6", + "symfony/var-dumper": "^6.4|^7.4|^8.0" }, "autoload": { "psr-4": { diff --git a/src/Client.php b/src/Client.php index a2f7011..29a2b4a 100644 --- a/src/Client.php +++ b/src/Client.php @@ -4,9 +4,10 @@ use Closure; use InvalidArgumentException; -use Pace\Soap\DateTimeMapping; -use Pace\Report\Builder as ReportBuilder; use Pace\Contracts\Soap\Factory as SoapFactory; +use Pace\InvokeAction\InvokeActionRequest; +use Pace\Report\Builder as ReportBuilder; +use Pace\Soap\DateTimeMapping; class Client { @@ -178,6 +179,16 @@ public function report($report): ReportBuilder return new ReportBuilder($this->service('ReportService'), $report); } + /** + * Get an invoke action request instance. + * + * @return InvokeActionRequest + */ + public function invokeAction(): InvokeActionRequest + { + return new InvokeActionRequest($this, $this->service('InvokeAction')); + } + /** * Get an instance of the specified service. * diff --git a/src/InvokeAction/InvokeActionRequest.php b/src/InvokeAction/InvokeActionRequest.php new file mode 100644 index 0000000..3ff5cd8 --- /dev/null +++ b/src/InvokeAction/InvokeActionRequest.php @@ -0,0 +1,54 @@ + $value) { + if (is_int($key)) { + $key = "in$key"; + } + if ($value instanceof Model) { + $value = [ + 'primaryKey' => $value->key(), + ]; + } elseif (is_array($value)) { + array_walk_recursive($value, function (&$value) { + if ($value instanceof Model) { + $value = $value->key(); + } + }); + } + $parameters[$key] = $value; + } + + $response = $this->service->invokeAction($action, ...$parameters); + + return new InvokeActionResponse($this->client, $response); + } +} diff --git a/src/InvokeAction/InvokeActionResponse.php b/src/InvokeAction/InvokeActionResponse.php new file mode 100644 index 0000000..c4a180e --- /dev/null +++ b/src/InvokeAction/InvokeActionResponse.php @@ -0,0 +1,98 @@ +response; + } + + /** + * Convert the instance to a model. + * + * @param string $type + * @return Model + */ + public function toModel(string $type): Model + { + return $this->client->model($type)->newInstance($this->response); + } + + /** + * Determine whether the offset exists. + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->response); + } + + /** + * Retrieve the offset. + * + * @param mixed $offset + * @return mixed + */ + public function offsetGet(mixed $offset): mixed + { + return $this->response[$offset]; + } + + /** + * Set the offset. + * + * @param mixed $offset + * @param mixed $value + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void + { + throw new BadMethodCallException('The response is read-only.'); + } + + /** + * Unset the offset. + * + * @param mixed $offset + * @return void + */ + public function offsetUnset(mixed $offset): void + { + throw new BadMethodCallException('The response is read-only.'); + } + + /** + * Convert the instance to JSON serializable data. + * + * @return array + */ + public function jsonSerialize(): array + { + return $this->response; + } +} diff --git a/src/Services/InvokeAction.php b/src/Services/InvokeAction.php new file mode 100644 index 0000000..66d1d36 --- /dev/null +++ b/src/Services/InvokeAction.php @@ -0,0 +1,20 @@ +soap->{$action}($parameters)->out; + } +} From f9b7bfac3ec9712b4015d6a894cfb3ce0c872099 Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Wed, 18 Feb 2026 13:03:50 -0500 Subject: [PATCH 5/9] Typed properties and arguments, return types, and fixed PHP 8.x deprecation warnings --- composer.json | 1 + src/Client.php | 53 +++++----- src/Contracts/Soap/Factory.php | 12 ++- src/Contracts/Soap/TypeMapping.php | 8 +- src/Enum/ReportExportType.php | 2 +- src/Facades/Pace.php | 2 +- src/KeyCollection.php | 96 +++++++---------- src/Model.php | 156 ++++++++++++---------------- src/Model/Attachments.php | 14 +-- src/PaceServiceProvider.php | 4 +- src/Report/Builder.php | 34 ++---- src/Report/File.php | 20 +--- src/Service.php | 10 +- src/Services/AttachmentService.php | 10 +- src/Services/CloneObject.php | 2 +- src/Services/CreateObject.php | 2 +- src/Services/DeleteObject.php | 2 +- src/Services/FindObjects.php | 4 +- src/Services/ReadObject.php | 6 +- src/Services/TransactionService.php | 10 +- src/Services/UpdateObject.php | 2 +- src/Services/Version.php | 2 +- src/Soap/DateTimeMapping.php | 12 +-- src/Soap/Factory.php | 20 ++-- src/Soap/Middleware/Transaction.php | 10 +- src/Soap/SoapClient.php | 14 +-- src/Type.php | 17 +-- src/XPath/Builder.php | 129 ++++++++++------------- tests/ModelTest.php | 7 -- tests/Report/ReportBuilderTest.php | 2 +- 30 files changed, 283 insertions(+), 380 deletions(-) diff --git a/composer.json b/composer.json index 91d98e5..b3ca1a0 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "require": { "php": "^8.1", "ext-fileinfo": "*", + "ext-simplexml": "*", "ext-soap": "*", "nesbot/carbon": "^2.0|^3.0", "doctrine/inflector": "^2.1" diff --git a/src/Client.php b/src/Client.php index 29a2b4a..048c3a6 100644 --- a/src/Client.php +++ b/src/Client.php @@ -7,6 +7,7 @@ use Pace\Contracts\Soap\Factory as SoapFactory; use Pace\InvokeAction\InvokeActionRequest; use Pace\Report\Builder as ReportBuilder; +use Pace\Services\AttachmentService; use Pace\Soap\DateTimeMapping; class Client @@ -21,21 +22,21 @@ class Client * * @var array */ - protected $services = []; + protected array $services = []; /** * The SOAP client factory. * * @var SoapFactory */ - protected $soapFactory; + protected SoapFactory $soapFactory; /** * The Pace services URL. * * @var string */ - protected $url; + protected string $url; /** * Create a new instance. @@ -46,7 +47,7 @@ class Client * @param string $password * @param string $scheme */ - public function __construct(SoapFactory $soapFactory, $host, $login, $password, $scheme = 'https') + public function __construct(SoapFactory $soapFactory, string $host, string $login, string $password, string $scheme = 'https') { $soapFactory->setOptions(compact('login', 'password')); $soapFactory->addTypeMapping(new DateTimeMapping); @@ -60,7 +61,7 @@ public function __construct(SoapFactory $soapFactory, $host, $login, $password, * * @return string[] */ - public function __sleep() + public function __sleep(): array { return ['soapFactory', 'url']; } @@ -71,7 +72,7 @@ public function __sleep() * @param string $name * @return Model */ - public function __get($name) + public function __get(string $name): Model { return $this->model(Type::modelify($name)); } @@ -79,9 +80,9 @@ public function __get($name) /** * Get an instance of the attachment service. * - * @return \Pace\Services\AttachmentService + * @return AttachmentService */ - public function attachment() + public function attachment(): AttachmentService { return $this->service('AttachmentService'); } @@ -96,7 +97,7 @@ public function attachment() * @param array|null $newParent * @return array */ - public function cloneObject($object, array $attributes, array $newAttributes, $newKey = null, array $newParent = null) + public function cloneObject(string $object, array $attributes, array $newAttributes, mixed $newKey = null, ?array $newParent = null): array { return $this->service('CloneObject')->clone($object, $attributes, $newAttributes, $newKey, $newParent); } @@ -108,7 +109,7 @@ public function cloneObject($object, array $attributes, array $newAttributes, $n * @param array $attributes * @return array */ - public function createObject($object, array $attributes) + public function createObject(string $object, array $attributes): array { return $this->service('CreateObject')->create($object, $attributes); } @@ -119,7 +120,7 @@ public function createObject($object, array $attributes) * @param string $object * @param int|string $key */ - public function deleteObject($object, $key) + public function deleteObject(string $object, mixed $key): void { $this->service('DeleteObject')->delete($object, $key); } @@ -132,7 +133,7 @@ public function deleteObject($object, $key) * @param array|null $sort * @return array */ - public function findObjects($object, $filter, array $sort = null) + public function findObjects(string $object, string $filter, ?array $sort = null): array { if (is_null($sort)) { return $this->service('FindObjects')->find($object, $filter); @@ -144,10 +145,10 @@ public function findObjects($object, $filter, array $sort = null) /** * Get a model instance. * - * @param Type|string $type + * @param string $type * @return Model */ - public function model($type) + public function model(string $type): Model { return new Model($this, $type); } @@ -159,7 +160,7 @@ public function model($type) * @param int|string $key * @return array|null */ - public function readObject($object, $key) + public function readObject(string $object, mixed $key): ?array { return $this->service('ReadObject')->read($object, $key); } @@ -170,7 +171,7 @@ public function readObject($object, $key) * @param Model|int $report * @return ReportBuilder */ - public function report($report): ReportBuilder + public function report(Model|int $report): ReportBuilder { if (!$report instanceof Model) { $report = $this->model('Report')->readOrFail($report); @@ -195,7 +196,7 @@ public function invokeAction(): InvokeActionRequest * @param string $name * @return mixed */ - public function service($name) + public function service(string $name): mixed { return $this->services[$name] ?? $this->services[$name] = $this->makeService($name); } @@ -205,7 +206,7 @@ public function service($name) * * @param Closure $callback */ - public function transaction(Closure $callback) + public function transaction(Closure $callback): void { $this->service('TransactionService')->transaction($callback); } @@ -215,7 +216,7 @@ public function transaction(Closure $callback) * * @param int $timeout */ - public function startTransaction(int $timeout = 60) + public function startTransaction(int $timeout = 60): void { $this->service('TransactionService')->startTransaction($timeout); } @@ -223,7 +224,7 @@ public function startTransaction(int $timeout = 60) /** * Rollback the transaction. */ - public function rollbackTransaction() + public function rollbackTransaction(): void { $this->service('TransactionService')->rollback(); } @@ -231,7 +232,7 @@ public function rollbackTransaction() /** * Commit the transaction. */ - public function commitTransaction() + public function commitTransaction(): void { $this->service('TransactionService')->commit(); } @@ -243,7 +244,7 @@ public function commitTransaction() * @param array $attributes * @return array */ - public function updateObject($object, $attributes) + public function updateObject(string $object, array $attributes): array { return $this->service('UpdateObject')->update($object, $attributes); } @@ -253,7 +254,7 @@ public function updateObject($object, $attributes) * * @return array */ - public function version() + public function version(): array { return $this->service('Version')->get(); } @@ -261,10 +262,10 @@ public function version() /** * Assemble the specified service's WSDL. * - * @param $service + * @param string $service * @return string */ - protected function getServiceWsdl($service) + protected function getServiceWsdl(string $service): string { return $this->url . $service . '?wsdl'; } @@ -275,7 +276,7 @@ protected function getServiceWsdl($service) * @param string $service * @return mixed */ - protected function makeService($service) + protected function makeService(string $service): mixed { $class = 'Pace\\Services\\' . $service; diff --git a/src/Contracts/Soap/Factory.php b/src/Contracts/Soap/Factory.php index 3137c95..5c05da0 100644 --- a/src/Contracts/Soap/Factory.php +++ b/src/Contracts/Soap/Factory.php @@ -2,6 +2,8 @@ namespace Pace\Contracts\Soap; +use SoapClient; + interface Factory { /** @@ -9,15 +11,15 @@ interface Factory * * @param TypeMapping $mapping */ - public function addTypeMapping(TypeMapping $mapping); + public function addTypeMapping(TypeMapping $mapping): void; /** * Create a new SoapClient instance. * * @param string $wsdl - * @return \SoapClient + * @return SoapClient */ - public function make($wsdl); + public function make(string $wsdl): SoapClient; /** * Set the specified SOAP client option. @@ -25,12 +27,12 @@ public function make($wsdl); * @param string $key * @param mixed $value */ - public function setOption($key, $value); + public function setOption(string $key, mixed $value): void; /** * Bulk set the specified SOAP client options. * * @param array $options */ - public function setOptions(array $options); + public function setOptions(array $options): void; } diff --git a/src/Contracts/Soap/TypeMapping.php b/src/Contracts/Soap/TypeMapping.php index 02a832a..253f541 100644 --- a/src/Contracts/Soap/TypeMapping.php +++ b/src/Contracts/Soap/TypeMapping.php @@ -9,14 +9,14 @@ interface TypeMapping * * @return string */ - public function getTypeName(); + public function getTypeName(): string; /** * Get the type namespace. * * @return string */ - public function getTypeNamespace(); + public function getTypeNamespace(): string; /** * Convert an XML string to a native PHP type. @@ -24,7 +24,7 @@ public function getTypeNamespace(); * @param string $xml * @return mixed */ - public function fromXml($xml); + public function fromXml(string $xml): mixed; /** * Convert a native PHP type to an XML string. @@ -32,5 +32,5 @@ public function fromXml($xml); * @param mixed $php * @return string */ - public function toXml($php); + public function toXml(mixed $php): string; } diff --git a/src/Enum/ReportExportType.php b/src/Enum/ReportExportType.php index 0fb43d1..966743a 100644 --- a/src/Enum/ReportExportType.php +++ b/src/Enum/ReportExportType.php @@ -2,7 +2,7 @@ namespace Pace\Enum; -final class ReportExportType +enum ReportExportType: int { const PDF = 2; const RTF = 3; diff --git a/src/Facades/Pace.php b/src/Facades/Pace.php index b409333..e9b2466 100644 --- a/src/Facades/Pace.php +++ b/src/Facades/Pace.php @@ -2,8 +2,8 @@ namespace Pace\Facades; -use Pace\Client; use Illuminate\Support\Facades\Facade; +use Pace\Client; class Pace extends Facade { diff --git a/src/KeyCollection.php b/src/KeyCollection.php index cb95169..58917b3 100644 --- a/src/KeyCollection.php +++ b/src/KeyCollection.php @@ -2,35 +2,21 @@ namespace Pace; -use Iterator; -use Countable; use ArrayAccess; +use Countable; +use Iterator; use JsonSerializable; -use RuntimeException; use OutOfBoundsException; +use RuntimeException; class KeyCollection implements ArrayAccess, Countable, Iterator, JsonSerializable { - /** - * The keys as returned by a find. - * - * @var array - */ - protected $keys = []; - - /** - * The model the keys belong to. - * - * @var Model - */ - protected $model; - /** * Cached reads. * * @var array */ - protected $readModels = []; + protected array $readModels = []; /** * Create a new key collection instance. @@ -38,10 +24,8 @@ class KeyCollection implements ArrayAccess, Countable, Iterator, JsonSerializabl * @param Model $model * @param array $keys */ - public function __construct(Model $model, array $keys) + public function __construct(protected Model $model, protected array $keys) { - $this->model = $model; - $this->keys = $keys; } /** @@ -49,7 +33,7 @@ public function __construct(Model $model, array $keys) * * @return string */ - public function __toString() + public function __toString(): string { return json_encode($this); } @@ -59,7 +43,7 @@ public function __toString() * * @return Model[] */ - public function all() + public function all(): array { return iterator_to_array($this); } @@ -69,7 +53,7 @@ public function all() * * @return int */ - public function count() + public function count(): int { return count($this->keys); } @@ -77,9 +61,9 @@ public function count() /** * Read the current key. * - * @return Model + * @return Model|null */ - public function current() + public function current(): ?Model { return $this->read($this->key()); } @@ -90,9 +74,9 @@ public function current() * @param mixed $keys * @return KeyCollection */ - public function diff($keys) + public function diff(mixed $keys): static { - return $this->fresh(array_diff($this->keys, ($keys instanceof self) ? $keys->keys() : (array)$keys)); + return $this->fresh(array_diff($this->keys, ($keys instanceof static) ? $keys->keys() : (array)$keys)); } /** @@ -101,7 +85,7 @@ public function diff($keys) * @param callable $callback * @return KeyCollection */ - public function filterKeys(callable $callback) + public function filterKeys(callable $callback): static { return $this->fresh(array_values(array_filter($this->keys, $callback))); } @@ -109,9 +93,9 @@ public function filterKeys(callable $callback) /** * Read only the first key. * - * @return Model + * @return Model|null */ - public function first() + public function first(): ?Model { $key = reset($this->keys); @@ -122,10 +106,10 @@ public function first() * Get the model for the specified key. * * @param string|int $key - * @return Model + * @return Model|null * @throws OutOfBoundsException if the key does not exist. */ - public function get($key) + public function get(mixed $key): ?Model { if (!$this->has($key)) { throw new OutOfBoundsException("The key '$key' does not exist"); @@ -140,7 +124,7 @@ public function get($key) * @param mixed $key * @return bool */ - public function has($key) + public function has(mixed $key): bool { return in_array($key, $this->keys, true); } @@ -150,7 +134,7 @@ public function has($key) * * @return bool */ - public function isEmpty() + public function isEmpty(): bool { return empty($this->keys); } @@ -160,7 +144,7 @@ public function isEmpty() * * @return array */ - function jsonSerialize() + function jsonSerialize(): array { return $this->all(); } @@ -170,7 +154,7 @@ function jsonSerialize() * * @return mixed */ - public function key() + public function key(): mixed { return current($this->keys); } @@ -180,7 +164,7 @@ public function key() * * @return array */ - public function keys() + public function keys(): array { return $this->keys; } @@ -188,9 +172,9 @@ public function keys() /** * Read only the last key. * - * @return Model + * @return Model|null */ - public function last() + public function last(): ?Model { $keys = array_reverse($this->keys); @@ -200,9 +184,9 @@ public function last() /** * Move forward to the next key. */ - public function next() + public function next(): void { - next($this->keys); + next($this->keys); } /** @@ -211,7 +195,7 @@ public function next() * @param mixed $key * @return bool */ - public function offsetExists($key) + public function offsetExists(mixed $key): bool { return $this->has($key); } @@ -220,9 +204,9 @@ public function offsetExists($key) * Read the specified key. * * @param mixed $key - * @return Model + * @return Model|null */ - public function offsetGet($key) + public function offsetGet(mixed $key): ?Model { return $this->get($key); } @@ -234,7 +218,7 @@ public function offsetGet($key) * @param mixed $value * @throws RuntimeException */ - public function offsetSet($key, $value) + public function offsetSet(mixed $key, mixed $value): void { $class = get_class($this); @@ -247,7 +231,7 @@ public function offsetSet($key, $value) * @param mixed $key * @throws RuntimeException */ - public function offsetUnset($key) + public function offsetUnset(mixed $key): void { $class = get_class($this); @@ -261,7 +245,7 @@ public function offsetUnset($key) * @param int $perPage * @return KeyCollection */ - public function paginate($page, $perPage = 25) + public function paginate(int $page, int $perPage = 25): static { $offset = max($page - 1, 0) * $perPage; @@ -272,10 +256,10 @@ public function paginate($page, $perPage = 25) * Get the values of a given key. * * @param string $value - * @param string $key + * @param string|null $key * @return array */ - public function pluck($value, $key = null) + public function pluck(string $value, ?string $key = null): array { $models = $this->all(); $results = []; @@ -294,7 +278,7 @@ public function pluck($value, $key = null) /** * Rewind to the first key. */ - public function rewind() + public function rewind(): void { reset($this->keys); } @@ -303,10 +287,10 @@ public function rewind() * Add a portion of the keys to a new collection. * * @param int $offset - * @param int $length + * @param int|null $length * @return KeyCollection */ - public function slice($offset, $length = null) + public function slice(int $offset, ?int $length = null): static { return $this->fresh(array_slice($this->keys, $offset, $length)); } @@ -316,7 +300,7 @@ public function slice($offset, $length = null) * * @return bool */ - public function valid() + public function valid(): bool { return $this->key() !== false; } @@ -327,7 +311,7 @@ public function valid() * @param array $keys * @return KeyCollection */ - protected function fresh(array $keys) + protected function fresh(array $keys): static { return new static($this->model, $keys); } @@ -338,7 +322,7 @@ protected function fresh(array $keys) * @param mixed $key * @return Model|null */ - protected function read($key) + protected function read(mixed $key): ?Model { if ($key === false) { return null; diff --git a/src/Model.php b/src/Model.php index 50e7e84..f16f3c7 100644 --- a/src/Model.php +++ b/src/Model.php @@ -9,51 +9,33 @@ use ReflectionMethod; use UnexpectedValueException; +/** + * @mixin Builder + */ class Model implements ArrayAccess, JsonSerializable { use Attachments; - /** - * The model type. - * - * @var string - */ - protected $type; - - /** - * The web service client instance. - * - * @var Client - */ - protected $client; - - /** - * The model's attributes. - * - * @var array - */ - protected $attributes = []; - /** * The model's original attributes. * * @var array */ - protected $original = []; + protected array $original = []; /** * Auto-magically loaded "belongs to" relationships. * - * @var array + * @var static[] */ - protected $relations = []; + protected array $relations = []; /** * Indicates if this model exists in Pace. * * @var bool */ - public $exists = false; + public bool $exists = false; /** * Create a new model instance. @@ -62,12 +44,8 @@ class Model implements ArrayAccess, JsonSerializable * @param string $type * @param array $attributes */ - public function __construct(Client $client, string $type, array $attributes = []) + public function __construct(protected Client $client, protected string $type, protected array $attributes = []) { - $this->client = $client; - $this->type = $type; - $this->attributes = $attributes; - $this->syncOriginal(); } @@ -78,7 +56,7 @@ public function __construct(Client $client, string $type, array $attributes = [] * @param array $arguments * @return mixed */ - public function __call($method, array $arguments) + public function __call(string $method, array $arguments): mixed { if ($this->isBuilderMethod($method)) { return $this->newBuilder()->$method(...$arguments); @@ -93,7 +71,7 @@ public function __call($method, array $arguments) * @param string $name * @return mixed */ - public function __get($name) + public function __get(string $name): mixed { return $this->getAttribute($name); } @@ -104,7 +82,7 @@ public function __get($name) * @param string $name * @return bool */ - public function __isset($name) + public function __isset(string $name): bool { return !is_null($this->getAttribute($name)); } @@ -115,7 +93,7 @@ public function __isset($name) * @param string $name * @param mixed $value */ - public function __set($name, $value) + public function __set(string $name, mixed $value): void { $this->setAttribute($name, $value); } @@ -125,7 +103,7 @@ public function __set($name, $value) * * @return string */ - public function __toString() + public function __toString(): string { return json_encode($this); } @@ -135,7 +113,7 @@ public function __toString() * * @param string $name */ - public function __unset($name) + public function __unset(string $name): void { $this->unsetAttribute($name); } @@ -147,7 +125,7 @@ public function __unset($name) * @param string $foreignKey * @return Model|null */ - public function belongsTo($relatedType, $foreignKey) + public function belongsTo(string $relatedType, string $foreignKey): ?static { if ($this->isCompoundKey($foreignKey)) { $key = $this->getCompoundKey($foreignKey); @@ -164,7 +142,7 @@ public function belongsTo($relatedType, $foreignKey) * @param array $attributes * @return Model */ - public function create(array $attributes) + public function create(array $attributes): static { $model = $this->newInstance($attributes); $model->save(); @@ -175,10 +153,10 @@ public function create(array $attributes) /** * Delete the model from the web service. * - * @param string $keyName - * @return bool|null + * @param string|null $keyName + * @return true|null */ - public function delete($keyName = null) + public function delete(?string $keyName = null): ?bool { if ($this->exists) { $this->client->deleteObject($this->type, $this->key($keyName)); @@ -186,6 +164,8 @@ public function delete($keyName = null) return true; } + + return null; } /** @@ -194,7 +174,7 @@ public function delete($keyName = null) * @param int|string $newKey * @return Model|null */ - public function duplicate($newKey = null) + public function duplicate(mixed $newKey = null): ?static { if ($this->exists) { $attributes = $this->client->cloneObject($this->type, $this->original, $this->getDirty(), $newKey); @@ -206,6 +186,8 @@ public function duplicate($newKey = null) return $model; } + + return null; } /** @@ -215,7 +197,7 @@ public function duplicate($newKey = null) * @param array|null $sort * @return KeyCollection */ - public function find($filter, $sort = null) + public function find(string $filter, ?array $sort = null): KeyCollection { $keys = $this->client->findObjects($this->type, $filter, $sort); @@ -225,18 +207,16 @@ public function find($filter, $sort = null) /** * Refresh the attributes of the model from the web service. * - * @param string $keyName + * @param string|null $keyName * @return Model|null */ - public function fresh($keyName = null) + public function fresh(?string $keyName = null): ?static { if (!$this->exists) { return null; } - $fresh = $this->read($this->key($keyName)); - - return $fresh; + return $this->read($this->key($keyName)); } /** @@ -244,7 +224,7 @@ public function fresh($keyName = null) * * @return array */ - public function getDirty() + public function getDirty(): array { return array_diff_assoc($this->attributes, $this->original); } @@ -255,11 +235,13 @@ public function getDirty() * @param string $name * @return mixed */ - public function getAttribute($name) + public function getAttribute(string $name): mixed { if ($this->hasAttribute($name)) { return $this->attributes[$name]; } + + return null; } /** @@ -267,7 +249,7 @@ public function getAttribute($name) * * @return string */ - public function getType() + public function getType(): string { return $this->type; } @@ -278,7 +260,7 @@ public function getType() * @param string $attribute * @return bool */ - public function hasAttribute($attribute) + public function hasAttribute(string $attribute): bool { return array_key_exists($attribute, $this->attributes); } @@ -288,10 +270,10 @@ public function hasAttribute($attribute) * * @param string $relatedType * @param string $foreignKey - * @param string $keyName + * @param string|null $keyName * @return Builder */ - public function hasMany($relatedType, $foreignKey, $keyName = null) + public function hasMany(string $relatedType, string $foreignKey, ?string $keyName = null): Builder { $builder = $this->client->model($relatedType)->newBuilder(); @@ -311,7 +293,7 @@ public function hasMany($relatedType, $foreignKey, $keyName = null) * * @return bool */ - public function isDirty() + public function isDirty(): bool { return $this->original !== $this->attributes; } @@ -322,7 +304,7 @@ public function isDirty() * @param array $keys * @return string */ - public function joinKeys(array $keys) + public function joinKeys(array $keys): string { return implode(':', $keys); } @@ -332,7 +314,7 @@ public function joinKeys(array $keys) * * @return array */ - public function jsonSerialize() + public function jsonSerialize(): array { return $this->toArray(); } @@ -340,11 +322,11 @@ public function jsonSerialize() /** * Get the model's primary key. * - * @param string $keyName - * @return string|int + * @param string|null $keyName + * @return mixed * @throws UnexpectedValueException if the key is null. */ - public function key($keyName = null) + public function key(?string $keyName = null): mixed { $key = $this->getAttribute($keyName ?: $this->guessPrimaryKey()); @@ -364,7 +346,7 @@ public function key($keyName = null) * @param string|null $keyName * @return Builder */ - public function morphMany($relatedType, $baseObject = 'baseObject', $baseObjectKey = 'baseObjectKey', $keyName = null) + public function morphMany(string $relatedType, string $baseObject = 'baseObject', string $baseObjectKey = 'baseObjectKey', ?string $keyName = null): Builder { $builder = $this->client->model($relatedType)->newBuilder(); @@ -380,7 +362,7 @@ public function morphMany($relatedType, $baseObject = 'baseObject', $baseObjectK * @param array $attributes * @return Model */ - public function newInstance(array $attributes = []) + public function newInstance(array $attributes = []): static { return new static($this->client, $this->type, $attributes); } @@ -391,7 +373,7 @@ public function newInstance(array $attributes = []) * @param mixed $offset * @return bool */ - public function offsetExists($offset) + public function offsetExists(mixed $offset): bool { return $this->hasAttribute($offset); } @@ -402,7 +384,7 @@ public function offsetExists($offset) * @param mixed $offset * @return mixed */ - public function offsetGet($offset) + public function offsetGet(mixed $offset): mixed { return $this->getAttribute($offset); } @@ -413,7 +395,7 @@ public function offsetGet($offset) * @param mixed $offset * @param mixed $value */ - public function offsetSet($offset, $value) + public function offsetSet(mixed $offset, mixed $value): void { $this->setAttribute($offset, $value); } @@ -423,7 +405,7 @@ public function offsetSet($offset, $value) * * @param mixed $offset */ - public function offsetUnset($offset) + public function offsetUnset(mixed $offset): void { $this->unsetAttribute($offset); } @@ -432,9 +414,9 @@ public function offsetUnset($offset) * Read a new model from the web service using the specified primary key. * * @param int|string $key - * @return Model + * @return Model|null */ - public function read($key) + public function read(mixed $key): ?static { // This is intentionally not strict. The web service considers // an integer 0 to be null and will respond with a fault. @@ -461,7 +443,7 @@ public function read($key) * @return Model * @throws ModelNotFoundException if the key does not exist. */ - public function readOrFail($key) + public function readOrFail(mixed $key): static { $model = $this->read($key); @@ -477,7 +459,7 @@ public function readOrFail($key) * * @return bool */ - public function save() + public function save(): bool { if ($this->exists) { // Update an existing object. @@ -500,7 +482,7 @@ public function save() * @param string $name * @param mixed $value */ - public function setAttribute($name, $value) + public function setAttribute(string $name, mixed $value): void { // Check to see if the value is a related model. if ($value instanceof self) { @@ -513,10 +495,10 @@ public function setAttribute($name, $value) /** * Split a compound key into an array. * - * @param string $key + * @param string|null $key * @return array */ - public function splitKey($key = null) + public function splitKey(?string $key = null): array { if (is_null($key)) { $key = $this->key(); @@ -530,7 +512,7 @@ public function splitKey($key = null) * * @return array */ - public function toArray() + public function toArray(): array { return $this->attributes; } @@ -540,7 +522,7 @@ public function toArray() * * @param string $name */ - public function unsetAttribute($name) + public function unsetAttribute(string $name): void { unset($this->attributes[$name]); } @@ -551,7 +533,7 @@ public function unsetAttribute($name) * @param string $foreignKey * @return string */ - protected function getCompoundKey($foreignKey) + protected function getCompoundKey(string $foreignKey): string { $keys = []; @@ -566,10 +548,10 @@ protected function getCompoundKey($foreignKey) * Get a compound key array for a "has many" relationship. * * @param string $foreignKey - * @param string $keyName + * @param string|null $keyName * @return array */ - protected function getCompoundKeyArray($foreignKey, $keyName) + protected function getCompoundKeyArray(string $foreignKey, ?string $keyName = null): array { return array_combine( $this->splitKey($foreignKey), @@ -583,7 +565,7 @@ protected function getCompoundKeyArray($foreignKey, $keyName) * @param string $method * @return Builder|Model|null */ - protected function getRelatedFromMethod($method) + protected function getRelatedFromMethod(string $method): static|Builder|null { // If the called method name exists as an attribute on the model, // assume it is the camel-cased related type and the attribute @@ -609,7 +591,7 @@ protected function getRelatedFromMethod($method) * * @return string */ - protected function guessPrimaryKey() + protected function guessPrimaryKey(): string { if ($keyName = Type::keyName($this->type)) { return $keyName; @@ -632,7 +614,7 @@ protected function guessPrimaryKey() * @param string $name * @return bool */ - protected function isBuilderMethod($name) + protected function isBuilderMethod(string $name): bool { if (version_compare(PHP_VERSION, '8.0.0', '>=')) { if (method_exists(Builder::class, $name)) { @@ -652,7 +634,7 @@ protected function isBuilderMethod($name) * @param mixed $key * @return bool */ - protected function isCompoundKey($key) + protected function isCompoundKey(mixed $key): bool { return strpos($key, ':') !== false; } @@ -662,7 +644,7 @@ protected function isCompoundKey($key) * * @return Builder */ - public function newBuilder() + public function newBuilder(): Builder { return new Builder($this); } @@ -673,7 +655,7 @@ public function newBuilder() * @param array $keys * @return KeyCollection */ - protected function newKeyCollection(array $keys) + protected function newKeyCollection(array $keys): KeyCollection { return new KeyCollection($this, $keys); } @@ -684,7 +666,7 @@ protected function newKeyCollection(array $keys) * @param string $relation * @return bool */ - protected function relationLoaded($relation) + protected function relationLoaded(string $relation): bool { return array_key_exists($relation, $this->relations); } @@ -692,7 +674,7 @@ protected function relationLoaded($relation) /** * Restore the current model attributes from the original. */ - protected function restore() + protected function restore(): void { $this->attributes = $this->original; } @@ -700,7 +682,7 @@ protected function restore() /** * Sync the original object attributes with the current. */ - protected function syncOriginal() + protected function syncOriginal(): void { $this->original = $this->attributes; } diff --git a/src/Model/Attachments.php b/src/Model/Attachments.php index fdbd130..0b6966e 100644 --- a/src/Model/Attachments.php +++ b/src/Model/Attachments.php @@ -3,6 +3,8 @@ namespace Pace\Model; use BadMethodCallException; +use Pace\Model; +use Pace\XPath\Builder; trait Attachments { @@ -12,10 +14,10 @@ trait Attachments * @param string $name * @param string $content * @param string|null $field - * @param int|string|null $keyName - * @return \Pace\Model + * @param string|null $keyName + * @return Model */ - public function attachFile($name, $content, $field = null, $keyName = null) + public function attachFile(string $name, string $content, ?string $field = null, ?string $keyName = null): Model { $key = $this->client->attachment()->add($this->type, $this->key($keyName), $field, $name, $content); @@ -25,9 +27,9 @@ public function attachFile($name, $content, $field = null, $keyName = null) /** * The file attachments relationship. * - * @return \Pace\XPath\Builder + * @return Builder */ - public function fileAttachments() + public function fileAttachments(): Builder { return $this->morphMany('FileAttachment'); } @@ -37,7 +39,7 @@ public function fileAttachments() * * @return string */ - public function getContent() + public function getContent(): string { if ($this->type !== 'FileAttachment') { throw new BadMethodCallException('Call to method which only exists on FileAttachment'); diff --git a/src/PaceServiceProvider.php b/src/PaceServiceProvider.php index c4a1e95..2973c12 100644 --- a/src/PaceServiceProvider.php +++ b/src/PaceServiceProvider.php @@ -2,8 +2,8 @@ namespace Pace; -use Pace\Soap\Factory as SoapFactory; use Illuminate\Support\ServiceProvider; +use Pace\Soap\Factory as SoapFactory; class PaceServiceProvider extends ServiceProvider { @@ -15,7 +15,7 @@ class PaceServiceProvider extends ServiceProvider public function boot() { $this->publishes([ - __DIR__.'/../config/pace.php' => config_path('pace.php'), + __DIR__ . '/../config/pace.php' => config_path('pace.php'), ]); } diff --git a/src/Report/Builder.php b/src/Report/Builder.php index e59f7d4..de69eec 100644 --- a/src/Report/Builder.php +++ b/src/Report/Builder.php @@ -2,47 +2,33 @@ namespace Pace\Report; -use Pace\Model; use InvalidArgumentException; use Pace\Enum\ReportExportType; +use Pace\Model; use Pace\Services\ReportService; class Builder { - /** - * The report service. - * - * @var ReportService - */ - protected $service; - - /** - * The report model. - * - * @var Model - */ - protected $report; - /** * The base object key (if applicable). * - * @var null + * @var mixed */ - protected $baseObjectKey = null; + protected mixed $baseObjectKey = null; /** * The report parameters. * * @var array */ - protected $parameters = []; + protected array $parameters = []; /** * The report export media types. * * @var array */ - protected $mediaTypes = [ + protected array $mediaTypes = [ ReportExportType::PDF => 'application/pdf', ReportExportType::RTF => 'text/rtf', ReportExportType::HTML => 'text/html', @@ -59,10 +45,8 @@ class Builder * @param ReportService $service * @param Model $report */ - public function __construct(ReportService $service, Model $report) + public function __construct(protected ReportService $service, protected Model $report) { - $this->service = $service; - $this->report = $report; } /** @@ -72,7 +56,7 @@ public function __construct(ReportService $service, Model $report) * @param mixed $value * @return $this */ - public function parameter(int $id, $value): self + public function parameter(int $id, mixed $value): self { $this->parameters[$id] = $value; @@ -86,7 +70,7 @@ public function parameter(int $id, $value): self * @param mixed $value * @return $this */ - public function namedParameter(string $name, $value): self + public function namedParameter(string $name, mixed $value): self { $id = $this->report->reportParameters()->filter('@name', $name)->get()->key(); @@ -118,7 +102,7 @@ public function parameters(array $parameters): self * @param mixed $key * @return $this */ - public function baseObjectKey($key): self + public function baseObjectKey(mixed $key): self { $this->baseObjectKey = $key instanceof Model ? $key->key() : $key; diff --git a/src/Report/File.php b/src/Report/File.php index 5ea5f1c..e62e327 100644 --- a/src/Report/File.php +++ b/src/Report/File.php @@ -6,30 +6,14 @@ class File { - /** - * The file's content. - * - * @var string - */ - protected $content; - - /** - * The file's media type. - * - * @var string|null - */ - protected $mediaType; - /** * Create a new file instance. * * @param string $content * @param string|null $mediaType */ - public function __construct(string $content, string $mediaType = null) + public function __construct(protected string $content, protected ?string $mediaType = null) { - $this->content = $content; - $this->mediaType = $mediaType; } /** @@ -39,7 +23,7 @@ public function __construct(string $content, string $mediaType = null) * @param string|null $mediaType * @return static */ - public static function fromBase64(string $content, string $mediaType = null): self + public static function fromBase64(string $content, ?string $mediaType = null): self { return new static(base64_decode($content), $mediaType); } diff --git a/src/Service.php b/src/Service.php index 138d220..7930b29 100644 --- a/src/Service.php +++ b/src/Service.php @@ -6,20 +6,12 @@ abstract class Service { - /** - * The SOAP client instance. - * - * @var SoapClient - */ - protected $soap; - /** * Create a new service instance. * * @param SoapClient $soap */ - public function __construct(SoapClient $soap) + public function __construct(protected SoapClient $soap) { - $this->soap = $soap; } } diff --git a/src/Services/AttachmentService.php b/src/Services/AttachmentService.php index 05873bb..68ccb3c 100644 --- a/src/Services/AttachmentService.php +++ b/src/Services/AttachmentService.php @@ -11,13 +11,13 @@ class AttachmentService extends Service * Add a new attachment to the vault. * * @param string $object - * @param mixed $key + * @param string|int $key * @param string|null $field * @param string $name * @param string $content * @return string */ - public function add($object, $key, $field, $name, $content) + public function add(string $object, mixed $key, ?string $field, string $name, string $content): string { $attachment = [ 'name' => $name, @@ -44,7 +44,7 @@ public function add($object, $key, $field, $name, $content) * @param string $key * @return array */ - public function getByKey($key) + public function getByKey(string $key): array { $request = ['in0' => $key]; @@ -61,7 +61,7 @@ public function getByKey($key) * * @param string $key */ - public function removeByKey($key) + public function removeByKey(string $key): void { $request = ['in0' => $key]; @@ -75,7 +75,7 @@ public function removeByKey($key) * @param string $content * @return string */ - protected function guessMimeType($name, $content) + protected function guessMimeType(string $name, string $content): string { $finfo = new Finfo(FILEINFO_MIME_TYPE); diff --git a/src/Services/CloneObject.php b/src/Services/CloneObject.php index 81c980d..fc9a7e0 100644 --- a/src/Services/CloneObject.php +++ b/src/Services/CloneObject.php @@ -16,7 +16,7 @@ class CloneObject extends Service * @param array|null $newParent * @return array */ - public function clone($object, array $attributes, array $newAttributes, $newKey = null, array $newParent = null) + public function clone(string $object, array $attributes, array $newAttributes, mixed $newKey = null, array $newParent = null): array { $request = [ $object => $attributes, diff --git a/src/Services/CreateObject.php b/src/Services/CreateObject.php index 54f8933..b5187dc 100644 --- a/src/Services/CreateObject.php +++ b/src/Services/CreateObject.php @@ -13,7 +13,7 @@ class CreateObject extends Service * @param array $attributes * @return array */ - public function create($object, array $attributes) + public function create(string $object, array $attributes): array { $request = [lcfirst($object) => $attributes]; diff --git a/src/Services/DeleteObject.php b/src/Services/DeleteObject.php index 3bfbe30..c094850 100644 --- a/src/Services/DeleteObject.php +++ b/src/Services/DeleteObject.php @@ -12,7 +12,7 @@ class DeleteObject extends Service * @param string $object * @param int|string $key */ - public function delete($object, $key) + public function delete(string $object, mixed $key): void { $request = ['in0' => $object, 'in1' => $key]; diff --git a/src/Services/FindObjects.php b/src/Services/FindObjects.php index b3dcf30..f71a389 100644 --- a/src/Services/FindObjects.php +++ b/src/Services/FindObjects.php @@ -13,7 +13,7 @@ class FindObjects extends Service * @param string $filter * @return array */ - public function find($object, $filter) + public function find(string $object, string $filter): array { $request = ['in0' => $object, 'in1' => $filter]; @@ -30,7 +30,7 @@ public function find($object, $filter) * @param array $sort * @return array */ - public function findAndSort($object, $filter, array $sort) + public function findAndSort(string $object, string $filter, array $sort): array { $request = ['in0' => $object, 'in1' => $filter, 'in2' => $sort]; diff --git a/src/Services/ReadObject.php b/src/Services/ReadObject.php index 03516aa..6198ba4 100644 --- a/src/Services/ReadObject.php +++ b/src/Services/ReadObject.php @@ -2,9 +2,9 @@ namespace Pace\Services; -use SoapFault; use Pace\Client; use Pace\Service; +use SoapFault; class ReadObject extends Service { @@ -16,7 +16,7 @@ class ReadObject extends Service * @return array|null * @throws SoapFault if an unexpected SOAP error occurs. */ - public function read($object, $key) + public function read(string $object, mixed $key): ?array { $request = [lcfirst($object) => [Client::PRIMARY_KEY => $key]]; @@ -39,7 +39,7 @@ public function read($object, $key) * @param SoapFault $exception * @return bool */ - protected function isObjectNotFound(SoapFault $exception) + protected function isObjectNotFound(SoapFault $exception): bool { return strpos($exception->getMessage(), 'Unable to locate object') === 0; } diff --git a/src/Services/TransactionService.php b/src/Services/TransactionService.php index 0cddc23..079d8fe 100644 --- a/src/Services/TransactionService.php +++ b/src/Services/TransactionService.php @@ -3,14 +3,14 @@ namespace Pace\Services; use Closure; -use SoapFault; -use Throwable; use Pace\Service; -use Pace\Soap\SoapClient; -use Pace\Soap\Middleware\Transaction; -use Pace\Soap\Middleware\StartTransaction; use Pace\Soap\Middleware\CommitTransaction; use Pace\Soap\Middleware\RollbackTransaction; +use Pace\Soap\Middleware\StartTransaction; +use Pace\Soap\Middleware\Transaction; +use Pace\Soap\SoapClient; +use SoapFault; +use Throwable; class TransactionService extends Service { diff --git a/src/Services/UpdateObject.php b/src/Services/UpdateObject.php index 88da7e7..aaf432d 100644 --- a/src/Services/UpdateObject.php +++ b/src/Services/UpdateObject.php @@ -13,7 +13,7 @@ class UpdateObject extends Service * @param array $attributes * @return array */ - public function update($object, $attributes) + public function update(string $object, array $attributes): array { $request = [lcfirst($object) => $attributes]; diff --git a/src/Services/Version.php b/src/Services/Version.php index c0ed11e..0b254eb 100644 --- a/src/Services/Version.php +++ b/src/Services/Version.php @@ -11,7 +11,7 @@ class Version extends Service * * @return array */ - public function get() + public function get(): array { $response = $this->soap->getVersion(); diff --git a/src/Soap/DateTimeMapping.php b/src/Soap/DateTimeMapping.php index 891a530..8eac91b 100644 --- a/src/Soap/DateTimeMapping.php +++ b/src/Soap/DateTimeMapping.php @@ -3,9 +3,9 @@ namespace Pace\Soap; use Carbon\Carbon; -use SimpleXMLElement; use InvalidArgumentException; use Pace\Contracts\Soap\TypeMapping; +use SimpleXMLElement; class DateTimeMapping implements TypeMapping { @@ -14,14 +14,14 @@ class DateTimeMapping implements TypeMapping * * @var string */ - protected $xmlFormat = 'Y-m-d\TH:i:s.u\Z'; + protected string $xmlFormat = 'Y-m-d\TH:i:s.u\Z'; /** * Get the name of the SOAP data type. * * @return string */ - public function getTypeName() + public function getTypeName(): string { return 'dateTime'; } @@ -31,7 +31,7 @@ public function getTypeName() * * @return string */ - public function getTypeNamespace() + public function getTypeNamespace(): string { return 'http://www.w3.org/2001/XMLSchema'; } @@ -42,7 +42,7 @@ public function getTypeNamespace() * @param string $xml * @return Carbon */ - public function fromXml($xml) + public function fromXml(string $xml): Carbon { return Carbon::createFromFormat($this->xmlFormat, new SimpleXMLElement($xml), 'UTC') ->timezone(date_default_timezone_get()); @@ -54,7 +54,7 @@ public function fromXml($xml) * @param Carbon $php * @return string */ - public function toXml($php) + public function toXml(mixed $php): string { if (!$php instanceof Carbon) { throw new InvalidArgumentException('PHP value must be a Carbon instance'); diff --git a/src/Soap/Factory.php b/src/Soap/Factory.php index 3bfc046..4da6144 100644 --- a/src/Soap/Factory.php +++ b/src/Soap/Factory.php @@ -2,8 +2,8 @@ namespace Pace\Soap; -use Pace\Contracts\Soap\TypeMapping; use Pace\Contracts\Soap\Factory as FactoryContract; +use Pace\Contracts\Soap\TypeMapping; class Factory implements FactoryContract { @@ -12,21 +12,21 @@ class Factory implements FactoryContract * * @var array */ - protected $options = []; + protected array $options = []; /** * Type mappings. * * @var array */ - protected $types = []; + protected array $types = []; /** * Add a new SOAP to PHP type mapping. * * @param TypeMapping $mapping */ - public function addTypeMapping(TypeMapping $mapping) + public function addTypeMapping(TypeMapping $mapping): void { $this->types[$mapping->getTypeNamespace() . ':' . $mapping->getTypeName()] = $mapping; } @@ -37,7 +37,7 @@ public function addTypeMapping(TypeMapping $mapping) * @param string $wsdl * @return SoapClient */ - public function make($wsdl) + public function make(string $wsdl): SoapClient { return new SoapClient($wsdl, $this->getOptions()); } @@ -48,7 +48,7 @@ public function make($wsdl) * @param string $key * @param mixed $value */ - public function setOption($key, $value) + public function setOption(string $key, mixed $value): void { $this->options[$key] = $value; } @@ -58,7 +58,7 @@ public function setOption($key, $value) * * @param array $options */ - public function setOptions(array $options) + public function setOptions(array $options): void { foreach ($options as $key => $value) { $this->setOption($key, $value); @@ -70,7 +70,7 @@ public function setOptions(array $options) * * @return array */ - protected function getOptions() + protected function getOptions(): array { return array_merge($this->options, [ 'typemap' => $this->getTypeMappings() @@ -83,7 +83,7 @@ protected function getOptions() * @param TypeMapping $type * @return array */ - protected function getTypeMapping(TypeMapping $type) + protected function getTypeMapping(TypeMapping $type): array { return [ 'type_name' => $type->getTypeName(), @@ -102,7 +102,7 @@ protected function getTypeMapping(TypeMapping $type) * * @return array */ - protected function getTypeMappings() + protected function getTypeMappings(): array { $types = []; diff --git a/src/Soap/Middleware/Transaction.php b/src/Soap/Middleware/Transaction.php index 24f0fdb..baebf64 100644 --- a/src/Soap/Middleware/Transaction.php +++ b/src/Soap/Middleware/Transaction.php @@ -6,21 +6,13 @@ class Transaction { - /** - * The transaction ID. - * - * @var string - */ - protected $id; - /** * Create a new middleware instance. * * @param string $id */ - public function __construct(string $id) + public function __construct(protected string $id) { - $this->id = $id; } /** diff --git a/src/Soap/SoapClient.php b/src/Soap/SoapClient.php index 6a623e4..46e82e8 100644 --- a/src/Soap/SoapClient.php +++ b/src/Soap/SoapClient.php @@ -11,7 +11,7 @@ class SoapClient extends PhpSoapClient * * @var array */ - protected static $middleware = []; + protected static array $middleware = []; /** * Add the specified middleware. @@ -19,7 +19,7 @@ class SoapClient extends PhpSoapClient * @param string $name * @param callable $callable */ - public static function addMiddleware(string $name, callable $callable) + public static function addMiddleware(string $name, callable $callable): void { static::$middleware[$name] = $callable; } @@ -29,7 +29,7 @@ public static function addMiddleware(string $name, callable $callable) * * @param string $name */ - public static function removeMiddleware(string $name) + public static function removeMiddleware(string $name): void { unset(static::$middleware[$name]); } @@ -41,21 +41,21 @@ public static function removeMiddleware(string $name) * @param array $arguments * @return mixed */ - public function __call($function, $arguments) + public function __call(string $function, array $arguments): mixed { $this->applyMiddleware(); - return parent::__call($function, $arguments); + return parent::__soapCall($function, $arguments); } /** * Apply the middleware. */ - protected function applyMiddleware() + protected function applyMiddleware(): void { $headers = []; - foreach(static::$middleware as $middleware) { + foreach (static::$middleware as $middleware) { $headers = $middleware($headers); } diff --git a/src/Type.php b/src/Type.php index f66a9b1..c53b0b8 100644 --- a/src/Type.php +++ b/src/Type.php @@ -2,6 +2,7 @@ namespace Pace; +use Doctrine\Inflector\Inflector; use Doctrine\Inflector\InflectorFactory; class Type @@ -11,7 +12,7 @@ class Type * * @var array */ - protected static $irregularNames = [ + protected static array $irregularNames = [ 'apSetup' => 'APSetup', 'arSetup' => 'ARSetup', 'crmSetup' => 'CRMSetup', @@ -58,16 +59,16 @@ class Type * * @var array */ - protected static $irregularKeys = [ + protected static array $irregularKeys = [ 'FileAttachment' => 'attachment', ]; /** * The Doctrine Inflector instance. * - * @var \Doctrine\Inflector\Inflector|null + * @var Inflector|null */ - protected static $inflector; + protected static ?Inflector $inflector = null; /** * Convert a name to camel case. @@ -75,7 +76,7 @@ class Type * @param string $name * @return string */ - public static function camelize($name) + public static function camelize(string $name): string { return array_search($name, static::$irregularNames) ?: lcfirst($name); } @@ -86,7 +87,7 @@ public static function camelize($name) * @param string $name * @return string */ - public static function modelify($name) + public static function modelify(string $name): string { return array_search($name, array_flip(static::$irregularNames)) ?: ucfirst($name); } @@ -97,7 +98,7 @@ public static function modelify($name) * @param string $name * @return string */ - public static function singularize($name) + public static function singularize(string $name): string { if (is_null(static::$inflector)) { static::$inflector = InflectorFactory::create()->build(); @@ -112,7 +113,7 @@ public static function singularize($name) * @param string $type * @return string|null */ - public static function keyName($type) + public static function keyName(string $type): ?string { if (array_key_exists($type, static::$irregularKeys)) { return static::$irregularKeys[$type]; diff --git a/src/XPath/Builder.php b/src/XPath/Builder.php index 1e43d0c..b843238 100644 --- a/src/XPath/Builder.php +++ b/src/XPath/Builder.php @@ -4,8 +4,9 @@ use Closure; use DateTime; -use Pace\Model; use InvalidArgumentException; +use Pace\KeyCollection; +use Pace\Model; use Pace\ModelNotFoundException; class Builder @@ -15,44 +16,36 @@ class Builder * * @var array */ - protected $operators = ['=', '!=', '<', '>', '<=', '>=']; + protected array $operators = ['=', '!=', '<', '>', '<=', '>=']; /** * Valid functions. * * @var array */ - protected $functions = ['contains', 'starts-with']; + protected array $functions = ['contains', 'starts-with']; /** * The filters. * * @var array */ - protected $filters = []; + protected array $filters = []; /** * The sorts. * * @var array */ - protected $sorts = []; - - /** - * The Pace model instance to perform the find request on. - * - * @var Model - */ - protected $model; + protected array $sorts = []; /** * Create a new instance. * - * @param Model $model + * @param Model|null $model */ - public function __construct(Model $model = null) + public function __construct(protected ?Model $model = null) { - $this->model = $model; } /** @@ -61,9 +54,9 @@ public function __construct(Model $model = null) * @param string $xpath * @param mixed $value * @param string $boolean - * @return self + * @return $this */ - public function contains($xpath, $value = null, $boolean = 'and') + public function contains(string $xpath, mixed $value = null, string $boolean = 'and'): static { return $this->filter($xpath, 'contains', $value, $boolean); } @@ -73,9 +66,9 @@ public function contains($xpath, $value = null, $boolean = 'and') * * @param string $xpath * @param mixed $value - * @return self + * @return $this */ - public function orContains($xpath, $value = null) + public function orContains(string $xpath, mixed $value = null): static { return $this->filter($xpath, 'contains', $value, 'or'); } @@ -83,13 +76,13 @@ public function orContains($xpath, $value = null) /** * Add a filter. * - * @param string $xpath - * @param string $operator + * @param string|Closure $xpath + * @param mixed $operator * @param mixed $value * @param string $boolean - * @return self + * @return $this */ - public function filter($xpath, $operator = null, $value = null, $boolean = 'and') + public function filter(string|Closure $xpath, mixed $operator = null, mixed $value = null, string $boolean = 'and'): static { if ($xpath instanceof Closure) { return $this->nestedFilter($xpath, $boolean); @@ -111,9 +104,9 @@ public function filter($xpath, $operator = null, $value = null, $boolean = 'and' /** * Perform the find request. * - * @return \Pace\KeyCollection + * @return KeyCollection */ - public function find() + public function find(): KeyCollection { return $this->model->find($this->toXPath(), $this->toXPathSort()); } @@ -123,7 +116,7 @@ public function find() * * @return Model|null */ - public function first() + public function first(): ?Model { return $this->find()->first(); } @@ -134,7 +127,7 @@ public function first() * @return Model * @throws ModelNotFoundException */ - public function firstOrFail() + public function firstOrFail(): Model { $result = $this->first(); @@ -150,7 +143,7 @@ public function firstOrFail() * * @return Model */ - public function firstOrNew() + public function firstOrNew(): Model { return $this->first() ?: $this->model->newInstance(); } @@ -158,9 +151,9 @@ public function firstOrNew() /** * A more "Eloquent" alias for find(). * - * @return \Pace\KeyCollection + * @return KeyCollection */ - public function get() + public function get(): KeyCollection { return $this->find(); } @@ -171,9 +164,9 @@ public function get() * @param string $xpath * @param array $values * @param string $boolean - * @return self + * @return $this */ - public function in($xpath, array $values, $boolean = 'and') + public function in(string $xpath, array $values, string $boolean = 'and'): static { return $this->filter(function ($builder) use ($xpath, $values) { foreach ($values as $value) { @@ -187,9 +180,9 @@ public function in($xpath, array $values, $boolean = 'and') * * @param string $xpath * @param array $values - * @return self + * @return $this */ - public function orIn($xpath, array $values) + public function orIn(string $xpath, array $values): static { return $this->in($xpath, $values, 'or'); } @@ -199,9 +192,9 @@ public function orIn($xpath, array $values) * * @param Closure $callback * @param string $boolean - * @return self + * @return $this */ - public function nestedFilter(Closure $callback, $boolean = 'and') + public function nestedFilter(Closure $callback, string $boolean = 'and'): static { $builder = new static; @@ -215,12 +208,12 @@ public function nestedFilter(Closure $callback, $boolean = 'and') /** * Add an "or" filter. * - * @param string $xpath - * @param string $operator + * @param string|Closure $xpath + * @param mixed $operator * @param mixed $value - * @return self + * @return $this */ - public function orFilter($xpath, $operator = null, $value = null) + public function orFilter(string|Closure $xpath, mixed $operator = null, mixed $value = null): static { return $this->filter($xpath, $operator, $value, 'or'); } @@ -231,9 +224,9 @@ public function orFilter($xpath, $operator = null, $value = null) * @param string $xpath * @param mixed $value * @param string $boolean - * @return self + * @return $this */ - public function startsWith($xpath, $value = null, $boolean = 'and') + public function startsWith(string $xpath, mixed $value = null, string $boolean = 'and'): static { return $this->filter($xpath, 'starts-with', $value, $boolean); } @@ -243,9 +236,9 @@ public function startsWith($xpath, $value = null, $boolean = 'and') * * @param string $xpath * @param mixed $value - * @return self + * @return $this */ - public function orStartsWith($xpath, $value = null) + public function orStartsWith(string $xpath, mixed $value = null): static { return $this->filter($xpath, 'starts-with', $value, 'or'); } @@ -255,9 +248,9 @@ public function orStartsWith($xpath, $value = null) * * @param string $xpath * @param bool $descending - * @return self + * @return $this */ - public function sort($xpath, $descending = false) + public function sort(string $xpath, bool $descending = false): static { $this->sorts[] = compact('xpath', 'descending'); @@ -269,7 +262,7 @@ public function sort($xpath, $descending = false) * * @return string */ - public function toXPath() + public function toXPath(): string { $xpath = []; @@ -294,7 +287,7 @@ public function toXPath() * * @return array|null */ - public function toXPathSort() + public function toXPathSort(): ?array { return count($this->sorts) ? ['XPathDataSort' => $this->sorts] : null; } @@ -305,7 +298,7 @@ public function toXPathSort() * @param array $filter * @return string */ - protected function compileFilter(array $filter) + protected function compileFilter(array $filter): string { return sprintf('%s %s %s %s', $filter['boolean'], $filter['xpath'], $filter['operator'], $this->value($filter['value'])); @@ -317,7 +310,7 @@ protected function compileFilter(array $filter) * @param array $filter * @return string */ - protected function compileFunction(array $filter) + protected function compileFunction(array $filter): string { return sprintf('%s %s(%s, %s)', $filter['boolean'], $filter['operator'], $filter['xpath'], $this->value($filter['value'])); @@ -329,7 +322,7 @@ protected function compileFunction(array $filter) * @param array $filter * @return string */ - protected function compileNested(array $filter) + protected function compileNested(array $filter): string { return sprintf('%s (%s)', $filter['boolean'], $filter['builder']->toXPath()); } @@ -337,10 +330,10 @@ protected function compileNested(array $filter) /** * Check if an operator is a valid function. * - * @param string $operator + * @param mixed $operator * @return bool */ - protected function isFunction($operator) + protected function isFunction(mixed $operator): bool { return in_array($operator, $this->functions, true); } @@ -348,10 +341,10 @@ protected function isFunction($operator) /** * Check if an operator is a valid operator. * - * @param string $operator + * @param mixed $operator * @return bool */ - protected function isOperator($operator) + protected function isOperator(mixed $operator): bool { return in_array($operator, $this->operators, true); } @@ -362,7 +355,7 @@ protected function isOperator($operator) * @param string $xpath * @return string */ - protected function stripLeadingBoolean($xpath) + protected function stripLeadingBoolean(string $xpath): string { return preg_replace('/^and |^or /', '', $xpath); } @@ -373,22 +366,14 @@ protected function stripLeadingBoolean($xpath) * @param mixed $value * @return string */ - protected function value($value) + protected function value(mixed $value): string { - switch (true) { - case ($value instanceof DateTime): - return $this->date($value); - - case (is_int($value)): - case (is_float($value)): - return (string)$value; - - case (is_bool($value)): - return $value ? '\'true\'' : '\'false\''; - - default: - return "\"$value\""; - } + return match (true) { + $value instanceof DateTime => $this->date($value), + is_int($value), is_float($value) => (string)$value, + is_bool($value) => $value ? '\'true\'' : '\'false\'', + default => "\"$value\"", + }; } /** @@ -397,7 +382,7 @@ protected function value($value) * @param DateTime $dt * @return string */ - protected function date(DateTime $dt) + protected function date(DateTime $dt): string { return $dt->format('\d\a\t\e(Y, n, j)'); } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 99bc49c..b6ec9a6 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -14,13 +14,6 @@ public function tearDown(): void Mockery::close(); } - public function testCannotBeConstructedWithCamelCase() - { - $client = Mockery::mock(Client::class); - $this->expectException(InvalidArgumentException::class); - new Model($client, 'salesPerson'); - } - public function testManipulatesProperties() { $client = Mockery::mock(Client::class); diff --git a/tests/Report/ReportBuilderTest.php b/tests/Report/ReportBuilderTest.php index ba1d1e2..d4af4bd 100644 --- a/tests/Report/ReportBuilderTest.php +++ b/tests/Report/ReportBuilderTest.php @@ -85,7 +85,7 @@ public function testNamedParameterThrowsInvalidArgumentException(): void $builder->namedParameter('Test Parameter', '2020-01-01'); } - private function newBuilder(MockInterface $service = null, MockInterface $report = null): Builder + private function newBuilder(?MockInterface $service = null, ?MockInterface $report = null): Builder { if (is_null($report)) { $report = Mockery::mock(Model::class); From 456cb165a4f96662b0a11273a5b8cc3fe417895c Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Wed, 18 Feb 2026 13:42:53 -0500 Subject: [PATCH 6/9] InvokeActionResponse::toModel() fix --- src/InvokeAction/InvokeActionResponse.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/InvokeAction/InvokeActionResponse.php b/src/InvokeAction/InvokeActionResponse.php index c4a180e..88df109 100644 --- a/src/InvokeAction/InvokeActionResponse.php +++ b/src/InvokeAction/InvokeActionResponse.php @@ -38,7 +38,10 @@ public function toArray(): array */ public function toModel(string $type): Model { - return $this->client->model($type)->newInstance($this->response); + $model = $this->client->model($type)->newInstance($this->response); + $model->exists = true; + + return $model; } /** From f2c5037f31b932a7758d71bbd62628e2d4b48a21 Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Wed, 18 Feb 2026 14:06:22 -0500 Subject: [PATCH 7/9] Updated readme to include requirements and invoke action service --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index bfa2d6b..9cce177 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Install via [Composer](http://getcomposer.org/): $ composer require robgridley/pace-api ``` +PHP 8.1+ with the SOAP, SimpleXML and Fileinfo extensions required. + ## Testing PHPUnit tests with 100% code coverage for `Model`, `KeyCollection` and `XPath\Builder` classes. @@ -409,6 +411,53 @@ $pace->report(100) ->print(); ``` +## Invoke Action + +The invoke action service methods are exposed as PHP methods. You can find a list of methods and their arguments in the InvokeAction.wsdl file provided with the Pace SDK. Arguments must be passed in the order specified in the WSDL. + +```php +$estimate = $pace->model('Estimate')->read(100000); +$pace->invokeAction()->calculateEstimate($estimate); +``` + +You can also use named arguments to pass the arguments out of order, or you can mix ordered and named arguments. + +```php +$poLine = $pace->model('PurchaseOrder')->read(50000)->purchaseOrderLines()->first(); +$pace->invokeAction()->receivePurchaseOrderLine($poLine, Carbon::now(), in3: 'Receiving note.', in5: 1); +``` + +If the method requires a complex type, you will need to pass an array. + +If one of the arguments is an instance of a model, it will automatically be converted to a complex type containing the model's primary key. The two examples above make use of this feature. Additionally, if your complex type array contains a model, it will automatically be converted to the model's primary key. + +```php +$productType = $pace->model('JobProductType')->read('FL'); +$result = $pace->invokeAction()->createEstimate([ + 'customer' => 'HOUSE', + 'estimateDescription' => 'Testing', + 'estimatePartInfo' => [ + 'product' => $productType, + 'quantity1' => 100, + 'finalSizeW' => 8.5, + 'finalSizeH' => 11, + 'colorsSide1' => 4, + 'colorsSide2' => 0, + 'totalColors' => 4, + 'eachOf' => 1, + 'grainSpecifications' => 1, + ], +]); +``` + +Finally, the result of the invoke action call can be accessed like an array, converted to an array, or converted to a model (if the method returns the matching complex type). + +```php +$result['estimateNumber']; // returns the estimate number +$result->toArray(); // returns an array +$result->toModel('Estimate'); // returns an estimate model +``` + ## Version Identify the version of Pace running on the server. From d38d064fb5cb064872d645f1b540acaf7dbb7c30 Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Thu, 19 Feb 2026 09:56:18 -0500 Subject: [PATCH 8/9] Find object aggregate service, find, sort and limit service, and JSON serialize models in KeyCollections --- README.md | 34 +++++++++++++ src/Client.php | 27 ++++++++-- src/KeyCollection.php | 36 +++++++++++++- src/Model.php | 23 ++++++++- src/Services/FindObjects.php | 54 ++++++++++++++++++++ src/XPath/Builder.php | 96 +++++++++++++++++++++++++++++++++++- tests/KeyCollectionTest.php | 2 +- tests/ModelTest.php | 8 +-- tests/XPath/BuilderTest.php | 33 +++++++++++-- 9 files changed, 296 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9cce177..d933624 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,40 @@ $jobs = $pace->job ->find(); ``` +### Limiting + +Use the `offset()` and `limit()` methods to limit your results. The default offset is 0 if it is not specified. + +```php +$jobs = $pace->job + ->filter('adminStatus/@openJob', true) + ->sort('@dateSetup', true) + ->limit(50) + ->find(); +``` + +You can also use the `paginate()` method to set the offset and limit for a page. + +```php +$jobs = $pace->job + ->filter('adminStatus/@openJob', true) + ->sort('@dateSetup', true) + ->paginate(1, 50) + ->find(); +``` + +### Eager loading + +The `load()` method preloads the models as part of the find request, using the find object aggregate service. It does not read the entire object; you must specify a list of fields in XPath. If the offset and limit are not specified, then 0 and 1,000 will be used by default. + +```php +$employees = $pace->model('Employee')->filter('@status', 'A')->load([ + '@firstName', + '@lastName', + 'department' => 'department/@description', +])->find(); +``` + ## Dates Dates are automatically converted to and from [Carbon](http://carbon.nesbot.com/) instances. Check out the `Soap\DateTimeMapper` and `Soap\Factory` classes if you want to see how this happens. diff --git a/src/Client.php b/src/Client.php index 048c3a6..9be6ce4 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,6 +2,7 @@ namespace Pace; +use BadMethodCallException; use Closure; use InvalidArgumentException; use Pace\Contracts\Soap\Factory as SoapFactory; @@ -131,15 +132,33 @@ public function deleteObject(string $object, mixed $key): void * @param string $object * @param string $filter * @param array|null $sort + * @param int|null $offset + * @param int|null $limit + * @param array $fields * @return array */ - public function findObjects(string $object, string $filter, ?array $sort = null): array + public function findObjects(string $object, string $filter, ?array $sort = null, ?int $offset = null, ?int $limit = null, array $fields = []): array { - if (is_null($sort)) { - return $this->service('FindObjects')->find($object, $filter); + if (!empty($fields)) { + if (is_null($offset) || is_null($limit)) { + throw new BadMethodCallException('Offset and limit are required when fields is not empty.'); + } + + // I think this is a bug in the Pace SOAP API? This method always returns limit + 1. + $limit -= 1; + + return $this->service('FindObjects')->loadValueObjects($object, $filter, $sort, $offset, $limit, $fields); + } + + if (!is_null($limit)) { + return $this->service('FindObjects')->findSortAndLimit($object, $filter, $sort, $offset, $limit); + } + + if (!is_null($sort)) { + return $this->service('FindObjects')->findAndSort($object, $filter, $sort); } - return $this->service('FindObjects')->findAndSort($object, $filter, $sort); + return $this->service('FindObjects')->find($object, $filter); } /** diff --git a/src/KeyCollection.php b/src/KeyCollection.php index 58917b3..33ade17 100644 --- a/src/KeyCollection.php +++ b/src/KeyCollection.php @@ -28,6 +28,40 @@ public function __construct(protected Model $model, protected array $keys) { } + /** + * Create a new key collection instance from an array of value objects. + * + * @param Model $model + * @param array $valueObjects + * @return static + */ + public static function fromValueObjects(Model $model, array $valueObjects): static + { + $keys = array_map(fn(object $valueObject) => $valueObject->primaryKey, $valueObjects); + $readModels = array_combine( + $keys, + array_map(function (object $valueObject) use ($model) { + $fields = $valueObject->fields->ValueField; + $fields = is_array($fields) ? $fields : [$fields]; + $attributes = array_combine( + array_map(fn(object $field) => $field->name, $fields), + array_map(fn(object $field) => $field->value, $fields) + ); + $attributes['primaryKey'] = $valueObject->primaryKey; + + $readModel = $model->newInstance($attributes); + $readModel->exists = true; + + return $readModel; + }, $valueObjects) + ); + + $collection = new static($model, $keys); + $collection->readModels = $readModels; + + return $collection; + } + /** * Convert this instance to its string representation. * @@ -146,7 +180,7 @@ public function isEmpty(): bool */ function jsonSerialize(): array { - return $this->all(); + return array_map(fn($value) => $value instanceof JsonSerializable ? $value->jsonSerialize() : $value, $this->all()); } /** diff --git a/src/Model.php b/src/Model.php index f16f3c7..0c87651 100644 --- a/src/Model.php +++ b/src/Model.php @@ -195,11 +195,23 @@ public function duplicate(mixed $newKey = null): ?static * * @param string $filter * @param array|null $sort + * @param int|null $offset + * @param int|null $limit + * @param array $fields * @return KeyCollection */ - public function find(string $filter, ?array $sort = null): KeyCollection + public function find(string $filter, ?array $sort = null, ?int $offset = null, ?int $limit = null, array $fields = []): KeyCollection { - $keys = $this->client->findObjects($this->type, $filter, $sort); + if (!empty($fields)) { + if (is_null($offset)) { + $offset = 0; + } + if (is_null($limit)) { + $limit = 1000; + } + } + + $keys = $this->client->findObjects($this->type, $filter, $sort, $offset, $limit, $fields); return $this->newKeyCollection($keys); } @@ -657,6 +669,13 @@ public function newBuilder(): Builder */ protected function newKeyCollection(array $keys): KeyCollection { + foreach ($keys as $key) { + if (is_object($key)) { + return KeyCollection::fromValueObjects($this, $keys); + } + break; + } + return new KeyCollection($this, $keys); } diff --git a/src/Services/FindObjects.php b/src/Services/FindObjects.php index f71a389..5cc026a 100644 --- a/src/Services/FindObjects.php +++ b/src/Services/FindObjects.php @@ -38,4 +38,58 @@ public function findAndSort(string $object, string $filter, array $sort): array return isset($response->out->string) ? (array)$response->out->string : []; } + + /** + * Find, sort and limit objects. + * + * @param string $object + * @param string $filter + * @param array|null $sort + * @param int $offset + * @param int $limit + * @return array + */ + public function findSortAndLimit(string $object, string $filter, ?array $sort, int $offset, int $limit): array + { + $request = ['in0' => $object, 'in1' => $filter, 'in2' => $sort, 'in3' => $offset, 'in4' => $limit]; + + $response = $this->soap->findSortAndLimit($request); + + return isset($response->out->string) ? (array)$response->out->string : []; + } + + /** + * Call the find object aggregate service. + * + * @param string $object + * @param string $filter + * @param array|null $sort + * @param int $offset + * @param int $limit + * @param array $fields + * @param mixed $primaryKey + * @return array + */ + public function loadValueObjects(string $object, string $filter, ?array $sort, int $offset, int $limit, array $fields, mixed $primaryKey = null): array + { + $request = [ + 'in0' => [ + 'objectName' => $object, + 'xpathFilter' => $filter, + 'xpathSorts' => $sort, + 'offset' => $offset, + 'limit' => $limit, + 'fields' => $fields, + 'primaryKey' => $primaryKey, + ], + ]; + + $response = $this->soap->loadValueObjects($request); + + if (is_array($response->out->valueObjects->ValueObject)) { + return $response->out->valueObjects->ValueObject; + } + + return [$response->out->valueObjects->ValueObject]; + } } diff --git a/src/XPath/Builder.php b/src/XPath/Builder.php index b843238..29ec107 100644 --- a/src/XPath/Builder.php +++ b/src/XPath/Builder.php @@ -39,6 +39,27 @@ class Builder */ protected array $sorts = []; + /** + * The fields to load. + * + * @var array + */ + protected array $fields = []; + + /** + * The result offset. + * + * @var int + */ + protected int $offset = 0; + + /** + * The results limit. + * + * @var int|null + */ + protected ?int $limit = null; + /** * Create a new instance. * @@ -108,7 +129,7 @@ public function filter(string|Closure $xpath, mixed $operator = null, mixed $val */ public function find(): KeyCollection { - return $this->model->find($this->toXPath(), $this->toXPathSort()); + return $this->model->find($this->toXPath(), $this->toXPathSort(), $this->offset, $this->limit, $this->toFieldDescriptor()); } /** @@ -158,6 +179,64 @@ public function get(): KeyCollection return $this->find(); } + /** + * Load the specified fields. + * + * @param array $fields + * @return $this + */ + public function load(array $fields): static + { + foreach ($fields as $key => $xpath) { + if (is_int($key)) { + $key = ltrim($xpath, '@'); + } + $this->fields[$key] = $xpath; + } + + return $this; + } + + /** + * Set the result offset. + * + * @param int $offset + * @return $this + */ + public function offset(int $offset): static + { + $this->offset = $offset; + + return $this; + } + + /** + * Set the results limit. + * + * @param int $limit + * @return $this + */ + public function limit(int $limit): static + { + $this->limit = $limit; + + return $this; + } + + /** + * Paginate the results. + * + * @param int $page + * @param int $perPage + * @return $this + */ + public function paginate(int $page, int $perPage = 25): static + { + $offset = max($page - 1, 0) * $perPage; + + return $this->offset($offset)->limit($perPage); + } + /** * Add an "in" filter. * @@ -292,6 +371,21 @@ public function toXPathSort(): ?array return count($this->sorts) ? ['XPathDataSort' => $this->sorts] : null; } + /** + * Get the field descriptor array. + * + * @return array + */ + public function toFieldDescriptor(): array + { + return array_map(function (string $name, string $xpath) { + return [ + 'name' => $name, + 'xpath' => $xpath, + ]; + }, array_keys($this->fields), array_values($this->fields)); + } + /** * Compile a simple filter. * diff --git a/tests/KeyCollectionTest.php b/tests/KeyCollectionTest.php index fde08cc..225fecb 100644 --- a/tests/KeyCollectionTest.php +++ b/tests/KeyCollectionTest.php @@ -99,10 +99,10 @@ public function testJsonSerializable() $model = Mockery::mock(Model::class); $collection = new KeyCollection($model, [5]); $model->shouldReceive('read')->with(5)->once()->andReturnSelf(); + $model->shouldReceive('jsonSerialize')->once()->andReturn([]); $array = $collection->jsonSerialize(); $this->assertInstanceOf('JsonSerializable', $collection); $this->assertIsArray($array); - $this->assertContainsOnlyInstancesOf('JsonSerializable', $array); } public function testArrayAccess() diff --git a/tests/ModelTest.php b/tests/ModelTest.php index b6ec9a6..61d3b65 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -122,7 +122,7 @@ public function testHasMany() $builder = $model->hasMany('JobPart', 'job'); $this->assertInstanceOf(Builder::class, $builder); $collection = Mockery::mock(KeyCollection::class); - $related->shouldReceive('find')->with('@job = "12345"', null)->once()->andReturn($collection); + $related->shouldReceive('find')->with('@job = "12345"', null, 0, null, [])->once()->andReturn($collection); $this->assertInstanceOf(KeyCollection::class, $builder->get()); } @@ -138,7 +138,7 @@ public function testHasManyWithCompoundKey() $this->assertInstanceOf(Builder::class, $builder); $collection = Mockery::mock(KeyCollection::class); $related->shouldReceive('find') - ->with('@job = "12345" and @jobPart = "01"', null) + ->with('@job = "12345" and @jobPart = "01"', null, 0, null, []) ->once() ->andReturn($collection); $this->assertInstanceOf(KeyCollection::class, $builder->get()); @@ -156,7 +156,7 @@ public function testMorphMany() $this->assertInstanceOf(Builder::class, $builder); $collection = Mockery::mock(KeyCollection::class); $related->shouldReceive('find') - ->with('@baseObject = "Job" and @baseObjectKey = "12345"', null) + ->with('@baseObject = "Job" and @baseObjectKey = "12345"', null, 0, null, []) ->once() ->andReturn($collection); $this->assertInstanceOf(KeyCollection::class, $builder->get()); @@ -404,7 +404,7 @@ public function testFind() $client = Mockery::mock(Client::class); $model = new Model($client, 'CSR'); $client->shouldReceive('findObjects') - ->with('CSR', "@active = 'true'", null) + ->with('CSR', "@active = 'true'", null, 0, null, []) ->once() ->andReturn([1, 4, 9]); $keys = $model->find("@active = 'true'"); diff --git a/tests/XPath/BuilderTest.php b/tests/XPath/BuilderTest.php index ad0994a..cb49e25 100644 --- a/tests/XPath/BuilderTest.php +++ b/tests/XPath/BuilderTest.php @@ -1,10 +1,10 @@ load([ + '@description', + 'description_2' => '@description2', + ]); + $this->assertEquals( + [ + [ + 'name' => 'description', + 'xpath' => '@description', + ], + [ + 'name' => 'description_2', + 'xpath' => '@description2', + ], + ], + $builder->toFieldDescriptor() + ); + } + public function testFind() { $model = Mockery::mock(Model::class); $collection = Mockery::mock(KeyCollection::class); - $model->shouldReceive('find')->with("@active = 'true'", null)->once()->andReturn($collection); $model->shouldReceive('find') - ->with("@active = 'true'", ['XPathDataSort' => [['xpath' => '@name', 'descending' => false]]]) + ->with("@active = 'true'", null, 0, null, []) + ->once() + ->andReturn($collection); + $model->shouldReceive('find') + ->with("@active = 'true'", ['XPathDataSort' => [['xpath' => '@name', 'descending' => false]]], 0, null, []) ->once() ->andReturn($collection); $builder = new Builder($model); From 5dafb7ce77662c7f60ef5cdf4497f291e82b86cc Mon Sep 17 00:00:00 2001 From: Rob Gridley Date: Thu, 19 Feb 2026 10:12:55 -0500 Subject: [PATCH 9/9] Update XPath Builder::first() method to use limit and offset (find, sort and limit objects service) --- README.md | 2 +- src/XPath/Builder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d933624..3b5285e 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ foreach ($estimates as $estimate) { } ``` -KeyCollection also has a number of useful methods such as `all()`, `paginate()` and `first()`. In fact, the single object find example from earlier is just a shortcut to the `KeyCollection::first()` method. +KeyCollection also has a number of useful methods such as `all()`, `paginate()` and `first()`. ## Relationships diff --git a/src/XPath/Builder.php b/src/XPath/Builder.php index 29ec107..d775cf7 100644 --- a/src/XPath/Builder.php +++ b/src/XPath/Builder.php @@ -139,7 +139,7 @@ public function find(): KeyCollection */ public function first(): ?Model { - return $this->find()->first(); + return $this->model->find($this->toXPath(), $this->toXPathSort(), 0, 1, $this->toFieldDescriptor())->first(); } /**