From 14f0bb83dd9c4aae30f9eb45a23d65181bb026ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Wed, 11 Aug 2021 17:26:30 +0200 Subject: [PATCH] V 2.0.1 More methods on CrmApiV2 --- composer.json | 5 +- composer.lock | 182 +++++++++++++++++++++- phpcs.xml | 1 + src/CrmApiV2.php | 11 ++ src/Enums/QuestionType.php | 25 +++ src/Models/Answer.php | 114 ++++++++++++++ src/Models/CrmClientModel.php | 36 +++++ src/Models/Customer.php | 10 +- src/Models/Question.php | 282 ++++++++++++++++++++++++++++++++++ src/Transport/Response.php | 1 + src/routes.json | 51 ++++-- 11 files changed, 698 insertions(+), 20 deletions(-) create mode 100644 src/Enums/QuestionType.php create mode 100644 src/Models/Answer.php create mode 100644 src/Models/Question.php diff --git a/composer.json b/composer.json index bc501f3..4d45f37 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "library", "description": "LDC CRM Client V2", "license": "MIT", - "version": "2.0.0", + "version": "2.0.1", "authors": [ { "name": "Jérémy Munsch", @@ -32,7 +32,8 @@ "require-dev": { "phpunit/phpunit": "~9.0", "squizlabs/php_codesniffer": "^3.5", - "orchestra/testbench": "^6.13" + "orchestra/testbench": "^6.13", + "slevomat/coding-standard": "^7.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 488d3d2..27affc1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8ae44f3768b7ee6045ae61fb2655ea9c", + "content-hash": "89b14a69db5103b9d01833ed279ab009", "packages": [ { "name": "brick/math", @@ -4562,6 +4562,76 @@ } ], "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.7.1", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "fe390591e0241955f22eb9ba327d137e501c771c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/fe390591e0241955f22eb9ba327d137e501c771c", + "reference": "fe390591e0241955f22eb9ba327d137e501c771c", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "phpcompatibility/php-compatibility": "^9.0", + "sensiolabs/security-checker": "^4.1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2020-12-07T18:04:37+00:00" + }, { "name": "doctrine/instantiator", "version": "1.4.0", @@ -5421,6 +5491,55 @@ }, "time": "2021-03-17T13:42:18+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "0.5.5", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "ea0b17460ec38e20d7eb64e7ec49b5d44af5d28c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/ea0b17460ec38e20d7eb64e7ec49b5d44af5d28c", + "reference": "ea0b17460ec38e20d7eb64e7ec49b5d44af5d28c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.87", + "phpstan/phpstan-strict-rules": "^0.12.5", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/0.5.5" + }, + "time": "2021-06-11T13:24:46+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.2.6", @@ -6807,6 +6926,67 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "slevomat/coding-standard", + "version": "7.0.13", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "122a9bf9a4a2195f74100f47dfb8375982f43cc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/122a9bf9a4a2195f74100f47dfb8375982f43cc9", + "reference": "122a9bf9a4a2195f74100f47dfb8375982f43cc9", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "php": "^7.1 || ^8.0", + "phpstan/phpdoc-parser": "0.5.1 - 0.5.5", + "squizlabs/php_codesniffer": "^3.6.0" + }, + "require-dev": { + "phing/phing": "2.16.4", + "php-parallel-lint/php-parallel-lint": "1.3.0", + "phpstan/phpstan": "0.12.93", + "phpstan/phpstan-deprecation-rules": "0.12.6", + "phpstan/phpstan-phpunit": "0.12.21", + "phpstan/phpstan-strict-rules": "0.12.10", + "phpunit/phpunit": "7.5.20|8.5.5|9.5.7" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/7.0.13" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2021-07-29T14:30:22+00:00" + }, { "name": "spatie/backtrace", "version": "1.2.0", diff --git a/phpcs.xml b/phpcs.xml index f1840de..5604a41 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -77,6 +77,7 @@ + diff --git a/src/CrmApiV2.php b/src/CrmApiV2.php index b4de051..c23c7e7 100644 --- a/src/CrmApiV2.php +++ b/src/CrmApiV2.php @@ -68,6 +68,17 @@ class CrmApiV2 return Customer::find($identification); } + /** + * Get all segmentation questions + * + * @param string $identification The customer hashcode or email + * @return Customer + */ + public function getQuestions(string $identification): ?Customer + { + return Customer::find($identification); + } + /** * Set the single optin status for an email * diff --git a/src/Enums/QuestionType.php b/src/Enums/QuestionType.php new file mode 100644 index 0000000..354e321 --- /dev/null +++ b/src/Enums/QuestionType.php @@ -0,0 +1,25 @@ + 1, + 'radio' => 2, + 'checkbox' => 3 + ]; + } +} diff --git a/src/Models/Answer.php b/src/Models/Answer.php new file mode 100644 index 0000000..95a11fb --- /dev/null +++ b/src/Models/Answer.php @@ -0,0 +1,114 @@ + 'required|int', + 'answer' => 'required|string|max:255' + ]; + + public function __construct(array $properties = []) + { + parent::__construct(); + $this->fill($properties); + return $this; + } + + // PUBLIC METHODS + + /** + * Get unanswered question for a specific customer + * + * @param \LDC\LDCCRM\Models\Customer $customer + * @return bool + */ + public static function setQuestionAnswers(string $hashcode, int $questionId, array $answersId): bool + { + self::assertValidHascode($hashcode); + self::assertValidPrimaryKey($questionId); + if (!count($answersId)) { + return true; + } + foreach ($answersId as $answerId) { + self::assertValidPrimaryKey($answerId); + } + static::setUpApiInterface(); // set apiInterface + $request = Utils::getResquest( + 'apiV2.customers.answers.store', + [ + 'customer' => $hashcode + ], + [ + 'question' => $questionId, + 'answers' => $answersId + ] + ); + + $response = static::$apiInterface->call($request); + return $response->status->equals(ApiStatus::OK()); + } +} diff --git a/src/Models/CrmClientModel.php b/src/Models/CrmClientModel.php index 0d050be..be997f4 100644 --- a/src/Models/CrmClientModel.php +++ b/src/Models/CrmClientModel.php @@ -272,4 +272,40 @@ abstract class CrmClientModel $refProp->setAccessible(true); return $refProp->getValue($object); } + + /** + * Assert primaryKey is valid + * + * @param int $primaryKey + * @return void + * @throws LDC\LDCCRM\Exceptions\ValidationException + */ + protected static function assertValidPrimaryKey(int $primaryKey): void + { + $validator = Utils::makeValidator( + ['id' => $primaryKey], + ['id' => 'required|int'] + ); + if ($validator->fails()) { + throw new ValidationException(implode(' ', $validator->errors()->get('id'))); + } + } + + /** + * Assert hashcode is valid + * + * @param string|null $hashcode + * @return void + * @throws LDC\LDCCRM\Exceptions\ValidationException + */ + protected static function assertValidHascode(?string $hashcode): void + { + $validator = Utils::makeValidator( + ['hashcode' => $hashcode], + ['hashcode' => 'required|uuid'] + ); + if ($validator->fails()) { + throw new ValidationException(implode(' ', $validator->errors()->get('hashcode'))); + } + } } diff --git a/src/Models/Customer.php b/src/Models/Customer.php index 01b2988..660ba64 100644 --- a/src/Models/Customer.php +++ b/src/Models/Customer.php @@ -401,7 +401,7 @@ class Customer extends CrmClientModel public static function find(string $hashcode): ?self { static::setUpApiInterface(); // set apiInterface - $request = Utils::getResquest('apiV2.customer.show', [ + $request = Utils::getResquest('apiV2.customers.show', [ 'customer' => $hashcode ]); $customer = null; @@ -460,7 +460,7 @@ class Customer extends CrmClientModel $data['password'] = $this->password; static::setUpApiInterface(); // set apiInterface - $request = Utils::getResquest('apiV2.customer.store', [], $data); + $request = Utils::getResquest('apiV2.customers.store', [], $data); $response = static::$apiInterface->call($request); if ($status = $response->status->equals(ApiStatus::OK())) { @@ -473,7 +473,7 @@ class Customer extends CrmClientModel { $this->validateValue('hashcode', $this->hashcode); static::setUpApiInterface(); // set apiInterface - $request = Utils::getResquest('apiV2.customer.update', [ + $request = Utils::getResquest('apiV2.customers.update', [ 'customer' => $this->hashcode ], $this->updatedValues); @@ -493,7 +493,7 @@ class Customer extends CrmClientModel { $this->validateValue('hashcode', $this->hashcode); static::setUpApiInterface(); // set apiInterface - $request = Utils::getResquest('apiV2.customer.destroy', [ + $request = Utils::getResquest('apiV2.customers.destroy', [ 'customer' => $this->hashcode ]); @@ -510,7 +510,7 @@ class Customer extends CrmClientModel { $this->validateValue('hashcode', $this->hashcode); static::setUpApiInterface(); // set apiInterface - $request = Utils::getResquest('apiV2.customer.activate', [ + $request = Utils::getResquest('apiV2.customers.activate', [ 'customer' => $this->hashcode ]); diff --git a/src/Models/Question.php b/src/Models/Question.php new file mode 100644 index 0000000..d98d8d6 --- /dev/null +++ b/src/Models/Question.php @@ -0,0 +1,282 @@ + 'required|int', + 'question' => 'required|string|max:255', + 'type' => ['required'], + 'answers' => 'required|array' + ]; + + public function __construct(array $properties = []) + { + parent::__construct(); + if (count(self::$validation['type']) < 2) { + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed + self::$validation['type'][] = function ($attribute, $value, $fail) { + if (!is_object($value) or !\in_array($value, QuestionType::toArray())) { + $type = \gettype($value); + $fail("Question type ($type)$value is not in QuestionType enum list"); + } + }; + } + $this->fill($properties); + return $this; + } + + public function __get(string $field) + { + $props = \array_diff( + $this->modelFields, + $this->invisibleFields + ); + if (in_array($field, $props)) { + if ($field === 'type' and !\is_object($this->{$field})) { + return QuestionType::make($this->{$field}); + } + } + return parent::__get($field); + } + + /** + * Set prop + * + * @param string $field + * @param mixed $value + * @throws TypeError if the field has an incorrect value + * @return mixed|null The input value or null is this prop does not exists + */ + public function __set(string $field, $value) + { + $out = parent::__set($field, $value); + if (in_array($field, $this->modelFields)) { + $this->updatedValues[$field] = (\is_object($value) and + get_class($value) === QuestionType::class) ? $value->value : $value; + $this->setRawProperty($this, $field, $this->updatedValues[$field]); + } + return $out; + } + + /** + * Fill data into this model + * using validation rules + * + * @param array $properties + * @return void + * @throws TypeError if any valid is not validated + */ + public function fill(array $properties) + { + if (isset($properties['type'])) { + try { + $properties['type'] = (\is_int($properties['type']) or \ctype_digit($properties['type'])) ? + QuestionType::make((int)$properties['type']) : $properties['type']; + } catch (TypeError $e) { + // cast anyway + $properties['type'] = (int)$properties['type']; + } + } + parent::fill($properties); + } + + /** + * Validate all model properties + * + * @return array the validated values + */ + protected function validateAllValues(array $ignore = []): array + { + $out = []; + if (!\in_array('type', $ignore)) { + try { + $this->validateValue('type', QuestionType::make(self::getRawProperty($this, 'type'))); + } catch (TypeError $e) { + // this to throw validation error if failure way + $this->validateValue('type', self::getRawProperty($this, 'type')); + } + $out['type'] = self::getRawProperty($this, 'type'); + } + return array_merge($out, parent::validateAllValues(array_merge(['type'], $ignore))); + } + + // PUBLIC METHODS + + /** + * Get all questions only + * + * @param string $hashcode + * @return array + * + * with \LDC\LDCCRM\Models\Question[] + */ + public static function all(): array + { + static::setUpApiInterface(); // set apiInterface + $request = Utils::getResquest('apiV2.questions.index'); + + $response = static::$apiInterface->call($request); + $out = []; + foreach ($response->data as $question) { + $answers = \array_map(function ($answer) { + return new Answer($answer); + }, $question['answers']); + $question['answers'] = $answers; + $out[] = new self($question); + } + return $out; + } + + /** + * Get all the questions with customer answers + * + * @param \LDC\LDCCRM\Models\Customer $customer + * @return array + * + * [ + * 0 => [ + * 'question' => \LDC\LDCCRM\Models\Question, + * 'customerAnswers' => \LDC\LDCCRM\Models\Answer[] + * ] + * ] + * + * with \LDC\LDCCRM\Models\Question[] + * and the customer \LDC\LDCCRM\Models\Answer[] + */ + public static function allWithCustomerAnswers(Customer $customer): array + { + static::setUpApiInterface(); // set apiInterface + $request = Utils::getResquest('apiV2.questions.indexWithResponse', [ + 'customer' => $customer->hashcode + ]); + + $response = static::$apiInterface->call($request); + $out = []; + foreach ($response->data as $question) { + $answers = \array_map(function ($answer) { + return new Answer($answer); + }, $question['answers']); + $customerAnswers = \array_map(function ($answer) { + return new Answer($answer); + }, $question['customerAnswers']); + unset($question['customerAnswers']); + $question['answers'] = $answers; + $out[] = [ + 'question' => new self($question), + 'customerAnswers' => $customerAnswers, + ]; + } + return $out; + } + + /** + * Get unanswered question for a specific customer + * + * @param \LDC\LDCCRM\Models\Customer $customer + * @return \LDC\LDCCRM\Models\Question[] + */ + public static function unansweredBy(Customer $customer): array + { + self::assertValidHascode($customer->hashcode); + static::setUpApiInterface(); // set apiInterface + $request = Utils::getResquest( + 'apiV2.questions.unAnsweredBy', + ['customer' => $customer->hashcode] + ); + + $response = static::$apiInterface->call($request); + $out = []; + foreach ($response->data as $question) { + $answers = \array_map(function ($answer) { + return new Answer($answer); + }, $question['answers']); + $question['answers'] = $answers; + $out[] = new self($question); + } + return $out; + } +} diff --git a/src/Transport/Response.php b/src/Transport/Response.php index ccc2c39..7934cca 100644 --- a/src/Transport/Response.php +++ b/src/Transport/Response.php @@ -17,6 +17,7 @@ use RecursiveIteratorIterator; * @property string $message The api message * @property array $headers The http reponse headers * @property array $body The http response body, this is parsed json + * @property ?array $data The reponse data * * @package LDCCrmClient * @subpackage transport diff --git a/src/routes.json b/src/routes.json index 802cffe..6483290 100644 --- a/src/routes.json +++ b/src/routes.json @@ -18,40 +18,46 @@ "POST" ] }, - "apiV2.customer.store": { - "uri": "api/v2/customer", + "apiV2.customers.store": { + "uri": "api/v2/customers", "methods": [ "POST" ] }, - "apiV2.customer.show": { - "uri": "api/v2/customer/{customer}", + "apiV2.customers.show": { + "uri": "api/v2/customers/{customer}", "methods": [ "GET", "HEAD" ] }, - "apiV2.customer.update": { - "uri": "api/v2/customer/{customer}", + "apiV2.customers.update": { + "uri": "api/v2/customers/{customer}", "methods": [ "PUT", "PATCH" ] }, - "apiV2.customer.destroy": { - "uri": "api/v2/customer/{customer}", + "apiV2.customers.destroy": { + "uri": "api/v2/customers/{customer}", "methods": [ "DELETE" ] }, - "apiV2.customer.authenticate": { - "uri": "api/v2/customer/authenticate", + "apiV2.customers.authenticate": { + "uri": "api/v2/customers/authenticate", "methods": [ "POST" ] }, - "apiV2.customer.activate": { - "uri": "api/v2/customer/{customer}/activate", + "apiV2.customers.activate": { + "uri": "api/v2/customers/{customer}/activate", + "methods": [ + "POST" + ] + }, + "apiV2.customers.answers.store": { + "uri": "api/v2/customers/{customer}/answers", "methods": [ "POST" ] @@ -67,5 +73,26 @@ "methods": [ "POST" ] + }, + "apiV2.questions.index": { + "uri": "api/v2/questions", + "methods": [ + "GET", + "HEAD" + ] + }, + "apiV2.questions.unAnsweredBy": { + "uri": "api/v2/questions/un-answered-by/{customer}", + "methods": [ + "GET", + "HEAD" + ] + }, + "apiV2.questions.indexWithResponse": { + "uri": "api/v2/questions/with-answers/{customer}", + "methods": [ + "GET", + "HEAD" + ] } } \ No newline at end of file