From b955713e7250cdf24aca25d7a48591f0fb7843d3 Mon Sep 17 00:00:00 2001 From: Daniel Kurowski Date: Fri, 7 May 2021 09:57:39 +0200 Subject: [PATCH 1/8] composer: upgrade to latest --- composer.json | 2 +- src/Enums/Program.php | 29 +++++++ src/Enums/TargetGroup.php | 25 ++++++ src/Request/Parameters.php | 51 ------------ .../Event/{ => Organizer}/Organizer.php | 0 .../OrganizerOrganizationalUnit.php | 0 src/Response/Event/Program.php | 77 ------------------- src/Response/Event/ProgramType.php | 29 ------- src/Response/Event/TargetGroup.php | 72 ----------------- 9 files changed, 55 insertions(+), 230 deletions(-) create mode 100644 src/Enums/Program.php create mode 100644 src/Enums/TargetGroup.php delete mode 100644 src/Request/Parameters.php rename src/Response/Event/{ => Organizer}/Organizer.php (100%) rename src/Response/Event/{ => Organizer}/OrganizerOrganizationalUnit.php (100%) delete mode 100644 src/Response/Event/Program.php delete mode 100644 src/Response/Event/ProgramType.php delete mode 100644 src/Response/Event/TargetGroup.php diff --git a/composer.json b/composer.json index 0718a46..61cc736 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": ">=8.0", "ext-dom": "*", "grifart/enum": "^0.2.1", - "guzzlehttp/guzzle": "^6.3" + "guzzlehttp/guzzle": "^7.3" }, "require-dev": { "phpstan/phpstan": "^0.12.88", diff --git a/src/Enums/Program.php b/src/Enums/Program.php new file mode 100644 index 0000000..353feb4 --- /dev/null +++ b/src/Enums/Program.php @@ -0,0 +1,29 @@ +params = $params; - } - - - public function getAll(): array - { - return $this->params; - } - - - public function getQueryString(): string - { - return \http_build_query($this->params); - } - - - public function setCredentials(string $username, string $password): static - { - $this->params[self::PARAM_USERNAME] = $username; - $this->params[self::PARAM_PASSWORD] = $password; - - return $this; - } - -} diff --git a/src/Response/Event/Organizer.php b/src/Response/Event/Organizer/Organizer.php similarity index 100% rename from src/Response/Event/Organizer.php rename to src/Response/Event/Organizer/Organizer.php diff --git a/src/Response/Event/OrganizerOrganizationalUnit.php b/src/Response/Event/Organizer/OrganizerOrganizationalUnit.php similarity index 100% rename from src/Response/Event/OrganizerOrganizationalUnit.php rename to src/Response/Event/Organizer/OrganizerOrganizationalUnit.php diff --git a/src/Response/Event/Program.php b/src/Response/Event/Program.php deleted file mode 100644 index cbf4746..0000000 --- a/src/Response/Event/Program.php +++ /dev/null @@ -1,77 +0,0 @@ -slug->toScalar(); - } - - - public function getName(): ?string - { - return $this->name; - } - - - public function isNotSelected(): bool - { - return $this->slug->equals(ProgramType::NONE()); - } - - - public function isOfTypeNature(): bool - { - return $this->slug->equals(ProgramType::NATURE()); - } - - - public function isOfTypeSights(): bool - { - return $this->slug->equals(ProgramType::SIGHTS()); - } - - - public function isOfTypeBrdo(): bool - { - return $this->slug->equals(ProgramType::BRDO()); - } - - - public function isOfTypeEkostan(): bool - { - return $this->slug->equals(ProgramType::EKOSTAN()); - } - - - public function isOfTypePsb(): bool - { - return $this->slug->equals(ProgramType::PSB()); - } - - - public function isOfTypeEducation(): bool - { - return $this->slug->equals(ProgramType::EDUCATION()); - } - -} diff --git a/src/Response/Event/ProgramType.php b/src/Response/Event/ProgramType.php deleted file mode 100644 index df29adf..0000000 --- a/src/Response/Event/ProgramType.php +++ /dev/null @@ -1,29 +0,0 @@ -id = $id; - } - - - public static function from(int $id): self - { - return new self($id); - } - - - - public function isOfTypeEveryone(): bool - { - return $this->id === self::EVERYONE; - } - - - public function isOfTypeAdults(): bool - { - return $this->id === self::ADULTS; - } - - - public function isOfTypeChildren(): bool - { - return $this->id === self::CHILDREN; - } - - - public function isOfTypeFamilies(): bool - { - return $this->id === self::FAMILIES; - } - - - public function isOfTypeFirstTimeAttendees(): bool - { - return $this->id === self::FIRST_TIME_ATTENDEES; - } - -} From 68fc33c482e90430329e0a0d197e7aab7996b92b Mon Sep 17 00:00:00 2001 From: Daniel Kurowski Date: Tue, 18 May 2021 11:06:41 +0200 Subject: [PATCH 2/8] Added tests folder --- tests/.gitignore | 1 + tests/bootstrap.php | 21 +++++++++++++++ tests/index.php | 56 +++++++++++++++++++++++++++++++++++++++ tests/secret.template.php | 6 +++++ 4 files changed, 84 insertions(+) create mode 100644 tests/.gitignore create mode 100644 tests/bootstrap.php create mode 100644 tests/index.php create mode 100644 tests/secret.template.php diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..d8dd038 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +secret.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..cd71a01 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,21 @@ + $clientId, 'clientSecret' => $clientSecret] = $secret; + + return (new BisClientFactory( + $clientId, + $clientSecret, + ))->create(); +})(); diff --git a/tests/index.php b/tests/index.php new file mode 100644 index 0000000..b826b9c --- /dev/null +++ b/tests/index.php @@ -0,0 +1,56 @@ +addAttendee(new EventAttendee( + $eventId, + 'Jan', + 'Novák', + DateTimeImmutable::createFromFormat('Y-m-d', '2000-05-01'), + '123 456 789', + 'jan.novak@example.com', + 'prosím, abych tam měl nachystanou teplou peřinu', + ['odpověď č. 1', '', 'odpověď č. 3'], + )); +}; +// uncomment if you need to test it otherwise it would post on every page load +//$addAttendee(eventId: 13703); // ⚠ do not forget to customize event id not to pollute real events with testing data + + +// ----------------------------- +// retrieving information test +// ----------------------------- + +echo '
'; + + echo '
'; + echo '

Event

'; + dump($client->getEvent(13063)); + echo '
'; + + echo '
'; + echo '

Events

'; + + foreach ($client->getEvents() as $event) { + dump($event); + } + echo '
'; + + echo '
'; + echo '

Organizational units

'; + + foreach ($client->getOrganizationalUnits() as $unit) { + dump($unit); + } + echo '
'; + +echo '
'; diff --git a/tests/secret.template.php b/tests/secret.template.php new file mode 100644 index 0000000..286de78 --- /dev/null +++ b/tests/secret.template.php @@ -0,0 +1,6 @@ + '', + 'clientSecret' => '', +]; From 560d20c8785f5cee8a07660572d7787ae9ebdb38 Mon Sep 17 00:00:00 2001 From: Daniel Kurowski Date: Wed, 26 May 2021 15:30:09 +0200 Subject: [PATCH 3/8] Implemented new API and refactored completely --- README.md | 197 ++++++++++++ composer.json | 7 +- src/Authenticator.php | 65 ++++ src/AuthorizationToken.php | 28 ++ src/BisClient.php | 90 ++++++ src/BisClientFactory.php | 46 +++ src/Client.php | 260 --------------- src/Endpoint.php | 36 +++ src/Enums/EventType.php | 49 +++ src/Enums/Program.php | 4 +- src/Enums/TargetGroup.php | 2 +- src/HttpClient.php | 79 +++++ src/Request/Adoption.php | 50 ++- src/Request/Event/EventAttendee.php | 48 +++ src/Request/Event/EventParameters.php | 176 ++++++++++ src/Request/Event/Ordering.php | 19 ++ src/Request/EventAttendee.php | 46 --- src/Request/EventParameters.php | 304 ------------------ src/Request/OrganizationalUnitParameters.php | 44 --- src/Request/ToArray.php | 11 + src/Response/Coordinates.php | 32 ++ src/Response/Event/Event.php | 228 ++++--------- src/Response/Event/Invitation/Food.php | 10 +- src/Response/Event/Invitation/Invitation.php | 20 +- src/Response/Event/Invitation/Photo.php | 2 +- .../Event/Invitation/Presentation.php | 13 +- .../Event/Organizer/ContactPerson.php | 43 +++ src/Response/Event/Organizer/Organizer.php | 36 +-- .../Organizer/OrganizerOrganizationalUnit.php | 21 +- src/Response/Event/Place.php | 21 +- .../Registration/RegistrationQuestion.php | 2 +- .../Event/Registration/RegistrationType.php | 63 ++-- .../Registration/RegistrationTypeEnum.php | 12 +- .../OrganizationalUnit/OrganizationalUnit.php | 65 ++-- .../OrganizationalUnitType.php | 10 +- .../OrganizationalUnit/exceptions.php | 7 - src/Response/Response.php | 63 ---- src/Response/exceptions.php | 70 ---- src/exceptions.php | 83 +++-- 39 files changed, 1161 insertions(+), 1201 deletions(-) create mode 100644 README.md create mode 100644 src/Authenticator.php create mode 100644 src/AuthorizationToken.php create mode 100644 src/BisClient.php create mode 100644 src/BisClientFactory.php delete mode 100644 src/Client.php create mode 100644 src/Endpoint.php create mode 100644 src/Enums/EventType.php create mode 100644 src/HttpClient.php create mode 100644 src/Request/Event/EventAttendee.php create mode 100644 src/Request/Event/EventParameters.php create mode 100644 src/Request/Event/Ordering.php delete mode 100644 src/Request/EventAttendee.php delete mode 100644 src/Request/EventParameters.php delete mode 100644 src/Request/OrganizationalUnitParameters.php create mode 100644 src/Request/ToArray.php create mode 100644 src/Response/Coordinates.php create mode 100644 src/Response/Event/Organizer/ContactPerson.php delete mode 100644 src/Response/OrganizationalUnit/exceptions.php delete mode 100644 src/Response/Response.php delete mode 100644 src/Response/exceptions.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..cdc124f --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +This library allows you to easily communicate with BIS API. + +# Installation + +## Composer + +Add path to `repositories`: + +``` +{ + "url": "https://github.com/hnuti-brontosaurus/php-bis-api-client.git", + "type": "vcs" +} +``` + +and install package: +``` +composer require hnuti-brontosaurus/php-bis-api-client +``` + +## Manually + +Download latest version from [github](https://github.com/hnuti-brontosaurus/php-bis-api-client/releases) to your computer. + + +# Usage + +First you need to create client instance. Note that you need to obtain client ID and secret from BIS administrator +to be able to authenticate against BIS. + +```php +$client = (new BisClientFactory( + 'clientId', + 'clientSecret', +))->create(); +``` + +Now you can perform any of available operations. + +## Events + +### Single event + +Retrieve all information about single event: + +```php +$event = $client->getEvent($id); + +// examples of reading data +$event->getName(); +$event->getOrganizer()->getResponsiblePerson(); +$event->getRegistrationType()->isOfTypeCustomWebpage(); +$event->getPlace()->getCoordinates(); +``` + +### More events + +Retrieve all information about multiple events. + +Basic usage: + +```php +$parameters = new \HnutiBrontosaurus\BisClient\Request\Event\EventParameters(); +$events = $client->getEvents($parameters); // $parameters are optional + +// example of reading data +foreach ($events as $event) { + $event->getName(); +} +``` + +#### Filters + +You can filter in many ways: + +```php +$parameters = new \HnutiBrontosaurus\BisClient\Request\Event\EventParameters(); + +// only events of "voluntary" type +$parameters->setType(\HnutiBrontosaurus\BisClient\Enums\EventType::VOLUNTARY()); + +// only events of "PsB" program +$parameters->setProgram(\HnutiBrontosaurus\BisClient\Enums\Program::PSB()); + +// only events of "first time attendees" target group +$parameters->setTargetGroup(\HnutiBrontosaurus\BisClient\Enums\TargetGroup::FIRST_TIME_ATTENDEES()); + +// only events organized by organizational unit with ID 123 +$parameters->setOrganizedBy(123); + +// excludes running events +$parameters->excludeRunning(); + +$events = $client->getEvents($parameters); +``` + +For type, program and target group, you can set more values at once: + +```php +$parameters = new \HnutiBrontosaurus\BisClient\Request\Event\EventParameters(); + +$parameters->setTypes([ + \HnutiBrontosaurus\BisClient\Enums\EventType::VOLUNTARY(), + \HnutiBrontosaurus\BisClient\Enums\EventType::SPORT(), +]); + +$events = $client->getEvents($parameters); +``` + +Note that each method call rewrites the previous one: + +```php +$parameters = new \HnutiBrontosaurus\BisClient\Request\Event\EventParameters(); + +// sets "voluntary" type +$parameters->setType(\HnutiBrontosaurus\BisClient\Enums\EventType::VOLUNTARY()); +// rewrites type with "sport" +$parameters->setType(\HnutiBrontosaurus\BisClient\Enums\EventType::SPORT()); + +$events = $client->getEvents($parameters); +``` + +#### Sorting + +You can even use some basic sorting options: + +```php +$parameters = new \HnutiBrontosaurus\BisClient\Request\Event\EventParameters(); + +// sort events by date from or date to +$parameters->orderByDateFrom(); +$parameters->orderByDateTo(); // default + +$events = $client->getEvents($parameters); +``` + +### Adding attendee + +You can add attendee to an event: + +```php +$client->addAttendee(new \HnutiBrontosaurus\BisClient\Request\Event\EventAttendee( + 123, // event ID + 'Jan', // first name + 'Novák', // last name + '12.3.2004', // birth date + '123 456 789', // phone number + 'jan@novak.cz', // e-mail address + 'poznámka', // note + ['odpověď na otázku č. 1', '', 'odpověď na otázku č. 3'], // answers to optional questions (optional) +)); +``` + +## Organizational units + +Retrieve all information about all organizational units: + +```php +$organizationalUnits = $client->getOrganizationalUnits(); + +// example of reading data +foreach ($organizationalUnits as $organizationalUnit) { + $organizationalUnit->getName(); + $organizationalUnit->getCity(); + $organizationalUnit->getChairman(); + $organizationalUnit->getCoordinates(); +} +``` + +# Development + +## Installation + +``` +composer install +``` + +## Structure + +- `docs` – instruction on how connection between brontoweb and BIS works (todo: move to brontoweb repo) +- `src` – source code + - `Enums` – basic enum types + - `Request` – request-related value objects + - `Response` – request-related value objects and exceptions + - `BisClient` – client itself, serves for making requests to BIS API + - `BisClientFactory` – collects configuration data, ensures authentication against BIS and returns `BisClient` + - `HttpClient` – wrapper around Guzzle client which adds BIS API specific pieces into the request +- `tests` – test code + +**Note that this library bundles Guzzle HTTP client as we can not rely on having composer in user's codebase.** + +## Tests + +This library has just `tests/index.php` which – if run on a webserver – will +pass or fail visually – no error and results output or an exception. + +Note that you have to obtain client ID and secret as well to be able to run the test. Ask BIS administrator to get it, copy `tests/secret.template.php` to `tests/secret.php` and insert credentials there. diff --git a/composer.json b/composer.json index 61cc736..0fc5db9 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,6 @@ "minimum-stability": "stable", "require": { "php": ">=8.0", - "ext-dom": "*", "grifart/enum": "^0.2.1", "guzzlehttp/guzzle": "^7.3" }, @@ -21,12 +20,10 @@ }, "autoload": { "psr-4": { - "HnutiBrontosaurus\\BisApiClient\\": "src/" + "HnutiBrontosaurus\\BisClient\\": "src/" }, "classmap": [ - "src/exceptions.php", - "src/Response/exceptions.php", - "src/Response/OrganizationalUnit/exceptions.php" + "src/exceptions.php" ] }, "config": { diff --git a/src/Authenticator.php b/src/Authenticator.php new file mode 100644 index 0000000..9a96927 --- /dev/null +++ b/src/Authenticator.php @@ -0,0 +1,65 @@ +token = null; // not yet authenticated + } + + + /** + * @throws UnableToAuthorize + * @throws ConnectionToBisFailed + */ + public function authenticate(): AuthorizationToken + { + if ($this->token !== null) { + return $this->token; + } + + try { + $response = $this->httpClient->send(new Request( + 'POST', + Endpoint::AUTHENTICATION(), + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + \json_encode([ + 'grant_type' => 'client_credentials', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ]), + )); + } catch (ClientException $e) { + if ($e->getCode() === 401) { + throw UnableToAuthorize::withPrevious($e); + } + + throw ConnectionToBisFailed::withPrevious($e); + + } catch (GuzzleException $e) { + throw ConnectionToBisFailed::withPrevious($e); + } + + $payload = \json_decode($response->getBody()->getContents()); + $this->token = AuthorizationToken::from($payload->access_token); + return $this->token; + } + +} diff --git a/src/AuthorizationToken.php b/src/AuthorizationToken.php new file mode 100644 index 0000000..35b6c71 --- /dev/null +++ b/src/AuthorizationToken.php @@ -0,0 +1,28 @@ +value; + } + + public function __toString(): string + { + return $this->toString(); + } + +} diff --git a/src/BisClient.php b/src/BisClient.php new file mode 100644 index 0000000..74c1247 --- /dev/null +++ b/src/BisClient.php @@ -0,0 +1,90 @@ +httpClient->send('GET', Endpoint::EVENT($id)); + + \assert($data instanceof \stdClass); + return Event::fromResponseData($data); + } + + + /** + * @return Event[] + * @throws ConnectionToBisFailed + */ + public function getEvents(?EventParameters $params = null): array + { + $data = $this->httpClient->send( + 'GET', Endpoint::EVENTS(), + $params !== null + ? $params + : new EventParameters(), + ); + + if ($data === null) { + return []; + } + + \assert(\is_array($data)); + return \array_map(Event::class . '::fromResponseData', $data); + } + + + /** + * @throws ConnectionToBisFailed + */ + public function addAttendee(EventAttendee $eventAttendee): void + { + $this->httpClient->send('POST', Endpoint::ADD_ATTENDEE_TO_EVENT(), null, $eventAttendee); + } + + + // organizational units + + /** + * @return OrganizationalUnit[] + * @throws ConnectionToBisFailed + */ + public function getOrganizationalUnits(): array + { + $data = $this->httpClient->send('GET', Endpoint::ADMINISTRATIVE_UNITS()); + + if ($data === null) { + return []; + } + + \assert(\is_array($data)); + return \array_map(OrganizationalUnit::class . '::fromResponseData', $data); + } + + + // adoption + + // not yet implemented +// public function saveRequestForAdoption(Adoption $adoption): void +// {} + +} diff --git a/src/BisClientFactory.php b/src/BisClientFactory.php new file mode 100644 index 0000000..a5dcf33 --- /dev/null +++ b/src/BisClientFactory.php @@ -0,0 +1,46 @@ +httpClient = new Client(); + $this->bisAuthenticator = new Authenticator( + $this->clientId, + $this->clientSecret, + $this->httpClient, + ); + } + + + /** + * @throws UnableToAuthorize + * @throws ConnectionToBisFailed + */ + public function create(): BisClient + { + $token = $this->bisAuthenticator->authenticate(); + return new BisClient(new HttpClient( + $token, + $this->httpClient, + )); + } + +} diff --git a/src/Client.php b/src/Client.php deleted file mode 100644 index 9c3c237..0000000 --- a/src/Client.php +++ /dev/null @@ -1,260 +0,0 @@ -url = \rtrim($url, '/'); - $this->username = $username; - $this->password = $password; - $this->httpClient = $httpClient; - } - - - // events - - /** - * @throws NotFoundException - * @throws TransferErrorException - * @throws ResponseErrorException - */ - public function getEvent(int $id, EventParameters $params = null): Event - { - $params = ($params !== null ? $params : new EventParameters()); - $params->setId($id); - $response = $this->processRequest($params); - - $data = $response->getData(); - - if (\count($data) === 0) { - throw new NotFoundException('No result for event with id `' . $id . '`.'); - } - - return Event::fromResponseData(\reset($data)); - } - - - /** - * @return Event[] - * @throws NotFoundException - * @throws TransferErrorException - * @throws ResponseErrorException - */ - public function getEvents(EventParameters $params = null): array - { - $response = $this->processRequest($params !== null ? $params : new EventParameters()); - $data = $response->getData(); - - if ($data === null) { - return []; - } - - return \array_map(Event::class . '::fromResponseData', $data); - } - - - /** - * @throws ResponseErrorException - */ - public function addAttendeeToEvent(EventAttendee $eventAttendee): void - { - $eventAttendee->setCredentials($this->username, $this->password); - $response = $this->httpClient->send($this->createRequest($eventAttendee)); - - $this->checkForResponseContentType($response); - - $domDocument = $this->generateDOM($response); - - $this->checkForResponseErrors($domDocument); - } - - - // organizational units - - /** - * @return OrganizationalUnit[] - * @throws NotFoundException - * @throws TransferErrorException - * @throws ResponseErrorException - */ - public function getOrganizationalUnits(OrganizationalUnitParameters $params = null): array - { - $response = $this->processRequest($params !== null ? $params : new OrganizationalUnitParameters()); - - $organizationalUnits = []; - foreach ($response->getData() as $organizationalUnit) { - try { - $organizationalUnits[] = OrganizationalUnit::fromResponseData($organizationalUnit); - - } catch (UnknownOrganizationUnitTypeException $e) { - continue; // In case of unknown type - just ignore it. - - } - } - - return $organizationalUnits; - } - - - // adoption - - /** - * @throws ResponseErrorException - */ - public function saveRequestForAdoption(Adoption $adoption): void - { - $adoption->setCredentials($this->username, $this->password); - - $response = $this->httpClient->send($this->createRequest($adoption)); - - $this->checkForResponseContentType($response); - - $domDocument = $this->generateDOM($response); - - $this->checkForResponseErrors($domDocument); - } - - - /** - * @throws NotFoundException - * @throws TransferErrorException - * @throws ResponseErrorException - */ - private function processRequest(Parameters $requestParameters): Response - { - $requestParameters->setCredentials($this->username, $this->password); - - try { - $response = $this->httpClient->send($this->createRequest($requestParameters)); - - } catch (ClientException $e) { - throw new NotFoundException('Bis client could not find the queried resource.', 0, $e); - - } catch (GuzzleException $e) { - throw new TransferErrorException('Unable to process request: transfer error.', 0, $e); - } - - $this->checkForResponseContentType($response); - - $domDocument = $this->generateDOM($response); - $this->checkForResponseErrors($domDocument); - - return new Response($response, $domDocument); - } - - - /** - * @throws InvalidContentTypeException - */ - private function checkForResponseContentType(ResponseInterface $response): void - { - if (\strncmp($response->getHeaderLine('Content-Type'), 'text/xml', \strlen('text/xml')) !== 0) { - throw new InvalidContentTypeException('Unable to process response: the response Content-Type is invalid or missing.'); - } - } - - - /** - * @throws InvalidXMLStructureException - */ - private function generateDOM(ResponseInterface $response): \DOMDocument - { - try { - $domDocument = new \DOMDocument(); - $domDocument->loadXML($response->getBody()->getContents()); - - } catch (\Exception $e) { - throw new InvalidXMLStructureException('Unable to process response: response body contains invalid XML.', 0, $e); - } - - return $domDocument; - } - - - /** - * @throws ResponseErrorException - */ - private function checkForResponseErrors(\DOMDocument $domDocument): void - { - $resultNode = $domDocument->getElementsByTagName(Response::TAG_RESULT)->item(0); - \assert($resultNode instanceof \DOMElement); - - if ($resultNode->hasAttribute(Response::TAG_RESULT_ATTRIBUTE_ERROR)) { - switch ($resultNode->getAttribute(Response::TAG_RESULT_ATTRIBUTE_ERROR)) { - case 'success': // In case of POST request with form data, BIS returns `` for some reason... Let's pretend that there is no error in such case because... you know... there is no error! - break; - - case 'user': - throw new InvalidUserInputException($resultNode); - - case 'forbidden': - throw new UnauthorizedAccessException(); - - case 'params': - throw new InvalidParametersException(); - - default: - throw new UnknownErrorException($resultNode->getAttribute(Response::TAG_RESULT_ATTRIBUTE_ERROR)); - } - } - } - - - private function createRequest(Parameters $parameters): Request - { - return new Request( - 'POST', - $this->url, - [ - 'Content-Type' => 'application/x-www-form-urlencoded', - ], - \http_build_query($parameters->getAll()) - ); - } - -} diff --git a/src/Endpoint.php b/src/Endpoint.php new file mode 100644 index 0000000..c0b430f --- /dev/null +++ b/src/Endpoint.php @@ -0,0 +1,36 @@ +toArray()) + : ''; + + // see Guzzle exceptions docs: https://docs.guzzlephp.org/en/stable/quickstart.html#exceptions + try { + $response = $this->client->send(new Request( + $method, + $endpoint . $queryString, + [ + 'Authorization' => 'Bearer ' . $this->authorizationToken->toString(), + 'Content-Type' => 'application/json', + ], + \json_encode($data !== null ? $data->toArray() : []), + )); + + } catch (ClientException $e) { // 4xx errors + if ($e->getCode() === 400) { + throw UnableToProcessRequest::withPrevious($e); + } + + if ($e->getCode() === 404) { + throw NotFound::withPrevious($e); + } + + throw ConnectionToBisFailed::withPrevious($e); + + } catch (ServerException $e) { // 5xx errors + throw ConnectionToBisFailed::withPrevious($e); + + } catch (TooManyRedirectsException $e) { + throw ConnectionToBisFailed::withPrevious($e); + + } catch (NetworkExceptionInterface $e) { // problem with connection + throw ConnectionToBisFailed::withPrevious($e); + + } catch (GuzzleException $e) { // fallback catch-all exception + throw ConnectionToBisFailed::withPrevious($e); + } + + return \json_decode($response->getBody()->getContents()); + } + +} diff --git a/src/Request/Adoption.php b/src/Request/Adoption.php index 9ccc658..6c84ba1 100644 --- a/src/Request/Adoption.php +++ b/src/Request/Adoption.php @@ -1,38 +1,34 @@ 'adopce', - 'f_jmeno' => $firstName, - 'f_prijmeni' => $lastName, - 'f_ulice' => $streetAddress . ' ' . $streetNumber, - 'f_mesto' => $city, - 'f_psc' => $postalCode, - 'f_email' => $emailAddress, - 'f_pohlavi' => null, // not required, but accepted by BIS (values muz/zena) - 'f_uvest_v_seznamu' => $excludeFromPublic ? 'off' : 'on', - 'f_clanek' => $preferredUnitOfTypeBase, - 'f_rc' => $preferredUnitOfTypeRegional, - 'f_castka' => $amount, - ]); + throw new UsageException('This is not implemented yet.'); } + + public function toArray(): array + { + return []; + } + + } diff --git a/src/Request/Event/EventAttendee.php b/src/Request/Event/EventAttendee.php new file mode 100644 index 0000000..3973143 --- /dev/null +++ b/src/Request/Event/EventAttendee.php @@ -0,0 +1,48 @@ +questionAnswers as $questionAnswer) { + $questionAnswers['additional_question_' . $i] = $questionAnswer; + $i++; + } + + return \array_merge([ + 'event' => $this->eventId, + 'first_name' => $this->firstName, + 'last_name' => $this->lastName, + 'telephone' => $this->phoneNumber, + 'email' => $this->emailAddress, + 'age_group' => $this->birthDate->format('Y'), + 'birth_month' => $this->birthDate->format('n'), + 'birth_day' => $this->birthDate->format('j'), + 'note' => $this->note, + ], $questionAnswers); + } + +} diff --git a/src/Request/Event/EventParameters.php b/src/Request/Event/EventParameters.php new file mode 100644 index 0000000..65f1def --- /dev/null +++ b/src/Request/Event/EventParameters.php @@ -0,0 +1,176 @@ +orderByDateTo(); + } + + + + // filter + + public const FILTER_ACTIONS_ONLY = 'action'; + public const FILTER_CAMPS_ONLY = 'camp'; + + private string $filter = ''; + + public function setFilter($filter): self + { + if ( ! \in_array($filter, [ + self::FILTER_ACTIONS_ONLY, + self::FILTER_CAMPS_ONLY, + ])) { + throw new UsageException('Value `' . $filter . '` is not of valid filters'); + } + + $this->filter = $filter; + return $this; + } + + + // type + + /** @var EventType[] */ + private array $types = []; + + public function setType(EventType $type): self + { + $this->types = [$type]; + return $this; + } + + /** + * @param EventType[] $types + */ + public function setTypes(array $types): self + { + $this->types = $types; + return $this; + } + + + // program + + /** @var Program[] */ + private array $programs = []; + + public function setProgram(Program $program): self + { + $this->programs = [$program]; + return $this; + } + + /** + * @param Program[] $programs + */ + public function setPrograms(array $programs): self + { + $this->programs = $programs; + return $this; + } + + + // target group + + /** @var TargetGroup[] */ + private array $targetGroups = []; + + public function setTargetGroup(TargetGroup $targetGroup): self + { + $this->targetGroups = [$targetGroup]; + return $this; + } + + /** + * @param TargetGroup[] $targetGroups + */ + public function setTargetGroups(array $targetGroups): self + { + $this->targetGroups = $targetGroups; + return $this; + } + + + + // miscellaneous + + private \DateTimeImmutable $dateFromGreaterThan; + + /** + * Excludes events which are running (started, but not yet ended). Defaults to include them. + */ + public function excludeRunning(): self + { + $this->dateFromGreaterThan = new \DateTimeImmutable(); + return $this; + } + + + public function orderByDateFrom(): self + { + $this->ordering = Ordering::DATE_FROM(); + return $this; + } + + public function orderByDateTo(): self + { + $this->ordering = Ordering::DATE_TO(); + return $this; + } + + + /** @var int[] */ + private array $organizedBy = []; + + /** + * @param int|int[] $unitIds + */ + public function setOrganizedBy(array|int $unitIds): self + { + // If just single value, wrap it into an array. + if ( ! \is_array($unitIds)) { + $this->organizedBy = [$unitIds]; + return $this; + } + + $this->organizedBy = $unitIds; + return $this; + } + + + + // getters + + public function toArray(): array + { + $array = [ + 'basic_purpose' => $this->filter, + 'event_type_array' => \implode(',', $this->types), + 'program_array' => \implode(',', $this->programs), + 'indended_for_array' => \implode(',', $this->targetGroups), + 'ordering' => $this->ordering, + 'administrative_unit' => \implode(',', $this->organizedBy), + ]; + + if (isset($this->dateFromGreaterThan)) { + $array['date_from__gte'] = $this->dateFromGreaterThan->format('Y-m-d'); + } + + return $array; + } + +} diff --git a/src/Request/Event/Ordering.php b/src/Request/Event/Ordering.php new file mode 100644 index 0000000..2666107 --- /dev/null +++ b/src/Request/Event/Ordering.php @@ -0,0 +1,19 @@ + 'prihlaska', - 'akce' => $eventId, - 'jmeno' => $firstName, - 'prijmeni' => $lastName, - 'telefon' => $phoneNumber, - 'email' => $emailAddress, - 'datum_narozeni' => $birthDate, - 'poznamka' => $note, - ]); - - - if (\count($questionAnswers) === 0) { - return; - } - - // currently 3 questions are supported - $i = 1; - foreach ($questionAnswers as $questionAnswer) { - $this->params['add_info' . ($i >= 2 ? '_' . $i : '')] = $questionAnswer; // key syntax is `add_info` for first key and `add_info_X` for any other - $i++; - } - } - -} diff --git a/src/Request/EventParameters.php b/src/Request/EventParameters.php deleted file mode 100644 index 56e2dd9..0000000 --- a/src/Request/EventParameters.php +++ /dev/null @@ -1,304 +0,0 @@ - 'akce', - self::PARAM_DISPLAY_ALREADY_STARTED_KEY => self::PARAM_DISPLAY_ALREADY_STARTED_VALUE, - self::PARAM_ORDER_BY_KEY => self::PARAM_ORDER_BY_END_DATE, - ]); - } - - - public function setId(int $id): static - { - $this->params['id'] = (int) $id; - return $this; - } - - - // filter - - const FILTER_CLUB = 1; - const FILTER_WEEKEND = 2; - const FILTER_CAMP = 4; - const FILTER_EKOSTAN = 8; - - /** - * This parameter serves as combinator for multiple conditions, which can not be achieved with concatenating type, program, target group or any other available parameters. - * For example you can not make an union among different parameters. Let's say you want all events which are of type=ohb or of program=brdo. This is not possible with API parameters. - * Thus you can take advantage of preset filters which are documented here: https://bis.brontosaurus.cz/myr.php - * - * Beside standard constant usage as a parameter, you can pass bitwise operation argument, e.g. `EventParameters::FILTER_WEEKEND|EventParameters::FILTER_CAMP`. - * - * @throws InvalidArgumentException - */ - public function setFilter(int $filter): static - { - $keys = [ - self::FILTER_CLUB => 'klub', - self::FILTER_WEEKEND => 'vik', - self::FILTER_CAMP => 'tabor', - self::FILTER_EKOSTAN => 'ekostan', - ]; - - switch ($filter) { - case self::FILTER_CLUB: - case self::FILTER_WEEKEND: - case self::FILTER_CAMP: - case self::FILTER_EKOSTAN: - $param = $keys[$filter]; - break; - - case self::FILTER_WEEKEND | self::FILTER_CAMP: - $param = $keys[self::FILTER_WEEKEND] . $keys[self::FILTER_CAMP]; - break; - - case self::FILTER_WEEKEND | self::FILTER_EKOSTAN: - $param = $keys[self::FILTER_WEEKEND] . $keys[self::FILTER_EKOSTAN]; - break; - - default: - throw new InvalidArgumentException('Value `' . $filter . '` is not of valid types and their combinations for `filter` parameter. Only `weekend+camp` and `weekend+ekostan` can be combined.'); - break; - } - - $this->params['filter'] = $param; - - return $this; - } - - - // type - - const TYPE_VOLUNTARY = 'pracovni'; // dobrovolnická - const TYPE_EXPERIENCE = 'prozitkova'; // zážitková - const TYPE_SPORT = 'sportovni'; - - const TYPE_EDUCATIONAL_TALK = 'prednaska'; // vzdělávací - přednášky - const TYPE_EDUCATIONAL_COURSES = 'vzdelavaci'; // vzdělávací - kurzy, školení - const TYPE_EDUCATIONAL_OHB = 'ohb'; // vzdělávací - kurz ohb - const TYPE_LEARNING_PROGRAM = 'vyuka'; // výukový program - const TYPE_RESIDENTIAL_LEARNING_PROGRAM = 'pobyt'; // pobytový výukový program - - const TYPE_CLUB_MEETUP = 'klub'; // klub - setkání - const TYPE_CLUB_TALK = 'klub-predn'; // klub - přednáška - const TYPE_FOR_PUBLIC = 'verejnost'; // akce pro veřejnost - const TYPE_EKOSTAN = 'ekostan'; - const TYPE_EXHIBITION = 'vystava'; - const TYPE_ACTION_GROUP = 'akcni'; // akční skupina - const TYPE_INTERNAL = 'jina'; // interní akce (VH a jiné) - const TYPE_GROUP_MEETING = 'schuzka'; // oddílová, družinová schůzka - - /** - * @throws InvalidArgumentException - */ - public function setType(string $type): static - { - if ( ! \in_array($type, [ - self::TYPE_VOLUNTARY, - self::TYPE_EXPERIENCE, - self::TYPE_SPORT, - - self::TYPE_EDUCATIONAL_TALK, - self::TYPE_EDUCATIONAL_COURSES, - self::TYPE_EDUCATIONAL_OHB, - self::TYPE_LEARNING_PROGRAM, - self::TYPE_RESIDENTIAL_LEARNING_PROGRAM, - - self::TYPE_CLUB_MEETUP, - self::TYPE_CLUB_TALK, - self::TYPE_FOR_PUBLIC, - self::TYPE_EKOSTAN, - self::TYPE_EXHIBITION, - self::TYPE_ACTION_GROUP, - self::TYPE_INTERNAL, - self::TYPE_GROUP_MEETING, - ], true)) { - throw new InvalidArgumentException('Value `' . $type . '` is not of valid types for `type` parameter.'); - } - - $this->params['typ'][] = $type; - return $this; - } - - /** - * @param string[] $types - */ - public function setTypes(array $types): static - { - foreach ($types as $type) { - $this->setType($type); - } - - return $this; - } - - - // program - - const PROGRAM_NOT_SELECTED = 'none'; - const PROGRAM_NATURE = 'ap'; - const PROGRAM_SIGHTS = 'pamatky'; - const PROGRAM_BRDO = 'brdo'; - const PROGRAM_EKOSTAN = 'ekostan'; - const PROGRAM_PSB = 'psb'; - const PROGRAM_EDUCATION = 'vzdelavani'; - - /** - * @throws InvalidArgumentException - */ - public function setProgram(string $program): static - { - if ( ! \in_array($program, [ - self::PROGRAM_NOT_SELECTED, - self::PROGRAM_NATURE, - self::PROGRAM_SIGHTS, - self::PROGRAM_BRDO, - self::PROGRAM_EKOSTAN, - self::PROGRAM_PSB, - self::PROGRAM_EDUCATION, - ], true)) { - throw new InvalidArgumentException('Value `' . $program . '` is not of valid types for `program` parameter.'); - } - - $this->params['program'][] = $program; - return $this; - } - - /** - * @param string[] $programs - */ - public function setPrograms(array $programs): static - { - foreach ($programs as $program) { - $this->setProgram($program); - } - - return $this; - } - - - // target group - - const TARGET_GROUP_EVERYONE = 'vsichni'; - const TARGET_GROUP_ADULTS = 'dospeli'; - const TARGET_GROUP_CHILDREN = 'deti'; - const TARGET_GROUP_FAMILIES = 'detirodice'; - const TARGET_GROUP_FIRST_TIME_ATTENDEES = 'prvouc'; - - /** - * @throws InvalidArgumentException - */ - public function setTargetGroup(string $targetGroup): static - { - if ( ! \in_array($targetGroup, [ - self::TARGET_GROUP_EVERYONE, - self::TARGET_GROUP_ADULTS, - self::TARGET_GROUP_CHILDREN, - self::TARGET_GROUP_FAMILIES, - self::TARGET_GROUP_FIRST_TIME_ATTENDEES, - ], true)) { - throw new InvalidArgumentException('Value `' . $targetGroup . '` is not of valid types for `for` parameter.'); - } - - $this->params['pro'][] = $targetGroup; - return $this; - } - - /** - * @param string[] $targetGroups - */ - public function setTargetGroups(array $targetGroups): static - { - foreach ($targetGroups as $targetGroup) { - $this->setTargetGroup($targetGroup); - } - - return $this; - } - - - // date constraints - - const PARAM_DATE_FORMAT = 'Y-m-d'; - - public function setFrom(\DateTimeImmutable $dateFrom): static - { - $this->params['od'] = $dateFrom->format(self::PARAM_DATE_FORMAT); - return $this; - } - - public function setUntil(\DateTimeImmutable $dateFrom): static - { - $this->params['do'] = $dateFrom->format(self::PARAM_DATE_FORMAT); - return $this; - } - - public function setYear(int $year): static - { - $this->params['rok'] = (int) $year; - return $this; - } - - public function hideTheseAlreadyStarted(): static - { - unset($this->params[self::PARAM_DISPLAY_ALREADY_STARTED_KEY]); - return $this; - } - - - // miscellaneous - - public function orderByStartDate(): static - { - unset($this->params[self::PARAM_ORDER_BY_KEY]); - return $this; - } - - /** - * @param int|int[] $unitIds - */ - public function setOrganizedBy(array|int $unitIds): static - { - $organizedByKey = 'zc'; - - // If just single value, wrap it into an array. - if ( ! \is_array($unitIds)) { - $unitIds = [$unitIds]; - } - - foreach ($unitIds as $unitId) { - // If such value is not present yet, initialize it with an empty array. - if ( ! \is_array($this->params[$organizedByKey])) { - $this->params[$organizedByKey] = []; - } - - $this->params[$organizedByKey][] = (int) $unitId; - } - - return $this; - } - - public function includeNonPublic(): static - { - $this->params['vse'] = 1; - return $this; - } - -} diff --git a/src/Request/OrganizationalUnitParameters.php b/src/Request/OrganizationalUnitParameters.php deleted file mode 100644 index 011bced..0000000 --- a/src/Request/OrganizationalUnitParameters.php +++ /dev/null @@ -1,44 +0,0 @@ - 'zc' - ]); - } - - - /** - * @throws InvalidArgumentException - */ - public function setType(string $type): static - { - if (!\in_array($type, [ - self::TYPE_CLUB, - self::TYPE_BASE, - self::TYPE_REGIONAL, - ], true)) { - throw new InvalidArgumentException('Type `' . $type . '` is not of valid types.'); - } - - $this->params[self::PARAM_FILTER] = $type; - - return $this; - } - -} diff --git a/src/Request/ToArray.php b/src/Request/ToArray.php new file mode 100644 index 0000000..b36e372 --- /dev/null +++ b/src/Request/ToArray.php @@ -0,0 +1,11 @@ +latitude; + } + + + public function getLongitude(): float + { + return $this->longitude; + } + +} diff --git a/src/Response/Event/Event.php b/src/Response/Event/Event.php index 7c37fa9..001c1ec 100644 --- a/src/Response/Event/Event.php +++ b/src/Response/Event/Event.php @@ -1,17 +1,20 @@ contact_person_email; + $registrationCustomUrl = $data->entry_form_url; $registrationQuestions = \array_filter([ // exclude all null items - $webRegistrationQuestion1, - $webRegistrationQuestion2, - $webRegistrationQuestion3, - ], fn($v, $k) => $v !== null, \ARRAY_FILTER_USE_BOTH); + $data->additional_question_1, + $data->additional_question_2, + $data->additional_question_3, + ], fn($v, $k) => $v !== '', \ARRAY_FILTER_USE_BOTH); $registrationType = RegistrationType::from( - $registrationType, + RegistrationTypeEnum::fromScalar($data->registration_method), \array_map(fn(string $question) => RegistrationQuestion::from($question), $registrationQuestions), $contactEmail, $registrationCustomUrl, ); - // price - $price = 0; - if (isset($data['poplatek']) && $data['poplatek'] !== '') { - $price = $data['poplatek']; - - if (\preg_match('|^[0-9]+$|', $price)) { - $price = (int) $price; - } - } // organizers - $organizationalUnitId = (isset($data['porada_id']) && $data['porada_id'] !== '') ? ((int) $data['porada_id']) : null; - $organizationalUnitName= (isset($data['porada']) && $data['porada'] !== '') ? $data['porada'] : null; - $organizers = (isset($data['org']) && $data['org'] !== '') ? $data['org'] : null; - $contactPersonName = (isset($data['kontakt']) && $data['kontakt'] !== '') ? $data['kontakt'] : null; - $contactPhone = $data['kontakt_telefon']; - $responsiblePerson = (isset($data['odpovedna']) && $data['odpovedna'] !== '') ? $data['odpovedna'] : null; + $organizationalUnitName = $data->administrative_unit_name !== '' ? $data->administrative_unit_name : null; + $organizationalUnitWebsite = $data->administrative_unit_web_url !== '' ? $data->administrative_unit_web_url : null; + $organizers = $data->looking_forward_to_you !== '' ? $data->looking_forward_to_you : null; + $responsiblePerson = $data->responsible_person !== '' ? $data->responsible_person : null; $organizer = Organizer::from( - ($organizationalUnitId !== null && $organizationalUnitName !== null) - ? OrganizerOrganizationalUnit::from($organizationalUnitId, $organizationalUnitName) + ($organizationalUnitName !== null) + ? OrganizerOrganizationalUnit::from($organizationalUnitName, $organizationalUnitWebsite) : null, $responsiblePerson, $organizers, - $contactPersonName, - $contactPhone, - $contactEmail, + ContactPerson::from( + $data->contact_person_name, + $contactEmail, + $data->contact_person_telephone, + ), ); // invitation - // BIS API returns "0", "1", "2" etc. for real options and "" when nothing is set - $food = (isset($data['strava']) && $data['strava'] !== '') - ? Food::fromScalar((int) $data['strava']) + $food = $data->diet !== null + ? Food::fromScalar($data->diet) : Food::NOT_LISTED(); /** @var Photo[] $invitationPresentationPhotos */ $invitationPresentationPhotos = []; for ($i = 1; $i <= 6; $i++) { - if (isset($data['ochutnavka_' . $i]) && $data['ochutnavka_' . $i] !== '') { - $invitationPresentationPhotos[] = Photo::from($data['ochutnavka_' . $i]); + $photo = $data->{'additional_photo_' . $i}; + if ($photo !== null) { + $invitationPresentationPhotos[] = Photo::from($photo); } } - $invitationOrganizationalInformation = (isset($data['text_info']) && $data['text_info'] !== '') ? $data['text_info'] : ''; // this will not be needed in BIS but now it has to be there as somehow obligatory fields are not required anymore in old BIS - $invitationIntroduction = (isset($data['text_uvod']) && $data['text_uvod'] !== '') ? $data['text_uvod'] : ''; // this will not be needed in BIS but now it has to be there as somehow obligatory fields are not required anymore in old BIS - $invitationPresentationText = (isset($data['text_mnam']) && $data['text_mnam'] !== '') ? $data['text_mnam'] : null; - $invitationWorkDescription = (isset($data['text_dobr']) && $data['text_dobr'] !== '') ? $data['text_dobr'] : null; - $workHoursPerDay = (isset($data['pracovni_doba']) && $data['pracovni_doba'] !== '') ? ((int) $data['pracovni_doba']) : null; - $accommodation = (isset($data['ubytovani']) && $data['ubytovani'] !== '') ? $data['ubytovani'] : null; + $invitationPresentationText = $data->invitation_text_4; $invitation = Invitation::from( - $invitationIntroduction, - $invitationOrganizationalInformation, - $accommodation, + $data->invitation_text_1, + $data->invitation_text_2, + $data->accommodation !== '' ? $data->accommodation : null, $food, - $invitationWorkDescription, - $workHoursPerDay, + $data->invitation_text_3, + $data->working_hours, ($invitationPresentationText !== null || \count($invitationPresentationPhotos) > 0) ? Presentation::from($invitationPresentationText, $invitationPresentationPhotos) : null, @@ -159,7 +107,7 @@ public static function fromResponseData(array $data): static // related website - $relatedWebsite = (isset($data['web']) && $data['web'] !== '') ? $data['web'] : null; + $relatedWebsite = $data->web_url; $_relatedWebsite = null; if ($relatedWebsite !== null) { if ( ! self::startsWith($relatedWebsite, 'http')) { // count with no protocol typed URLs @@ -170,24 +118,26 @@ public static function fromResponseData(array $data): static } return new self( - (int) $data['id'], - $data['nazev'], - (isset($data['foto_hlavni']) && $data['foto_hlavni'] !== '') ? $data['foto_hlavni'] : null, - \DateTimeImmutable::createFromFormat('Y-m-d', $data['od']), - \DateTimeImmutable::createFromFormat('Y-m-d', $data['do']), - $data['typ'], - $program, - $place, + $data->id, + $data->name, + $data->main_photo, + \DateTimeImmutable::createFromFormat('Y-m-d', $data->date_from), + \DateTimeImmutable::createFromFormat('Y-m-d', $data->date_to), + Program::fromScalar($data->program), + Place::from( + $data->location->name, + $data->location->gps_latitude !== null && $data->location->gps_longitude !== null + ? Coordinates::from($data->location->gps_latitude, $data->location->gps_longitude) + : null, + ), $registrationType, - (isset($data['vek_od']) && $data['vek_od'] !== '') ? ((int) $data['vek_od']) : null, - (isset($data['vek_do']) && $data['vek_do'] !== '') ? ((int) $data['vek_do']) : null, - $price, + $data->age_from, + $data->age_to, + $data->participation_fee !== null ? $data->participation_fee : null, $organizer, - TargetGroup::from((int) $data['prokoho']), + TargetGroup::fromScalar($data->indended_for), $invitation, - (isset($data['sraz']) && $data['sraz'] !== '') ? $data['sraz'] : null, - (isset($data['popis_programu']) && $data['popis_programu'] !== '') ? $data['popis_programu'] : null, - (isset($data['jak_se_prihlasit']) && $data['jak_se_prihlasit'] !== '') ? $data['jak_se_prihlasit'] : null, + new \DateTimeImmutable($data->start_date), $_relatedWebsite, ); } @@ -211,12 +161,6 @@ public function getCoverPhotoPath(): ?string } - public function hasCoverPhoto(): bool - { - return $this->coverPhotoPath !== null; - } - - public function getDateFrom(): \DateTimeImmutable { return $this->dateFrom; @@ -229,12 +173,6 @@ public function getDateUntil(): \DateTimeImmutable } - public function getType(): string - { - return $this->type; - } - - public function getProgram(): Program { return $this->program; @@ -265,18 +203,12 @@ public function getAgeUntil(): ?int } - public function getPrice(): int|string + public function getPrice(): ?string { return $this->price; } - public function isPaid(): bool - { - return $this->price !== 0; - } - - public function getOrganizer(): Organizer { return $this->organizer; @@ -295,33 +227,9 @@ public function getInvitation(): Invitation } - public function hasTimeFrom(): bool - { - return $this->timeFrom !== null; - } - - - public function getTimeFrom(): ?string - { - return $this->timeFrom; - } - - - public function getProgramDescription(): ?string - { - return $this->programDescription; - } - - - public function getNotes(): ?string - { - return $this->notes; - } - - - public function hasRelatedWebsite(): bool + public function getStartDate(): ?\DateTimeImmutable { - return $this->relatedWebsite !== null; + return $this->startDate; } diff --git a/src/Response/Event/Invitation/Food.php b/src/Response/Event/Invitation/Food.php index 49a2c3b..5fe93f1 100644 --- a/src/Response/Event/Invitation/Food.php +++ b/src/Response/Event/Invitation/Food.php @@ -1,6 +1,6 @@ accommodation !== null; - } - - public function getAccommodation(): ?string { return $this->accommodation; @@ -75,24 +69,12 @@ public function getWorkDescription(): ?string } - public function areWorkHoursPerDayListed(): bool - { - return $this->workHoursPerDay !== null; - } - - public function getWorkHoursPerDay(): ?int { return $this->workHoursPerDay; } - public function hasPresentation(): bool - { - return $this->presentation !== null; - } - - public function getPresentation(): ?Presentation { return $this->presentation; diff --git a/src/Response/Event/Invitation/Photo.php b/src/Response/Event/Invitation/Photo.php index 582c3be..df44878 100644 --- a/src/Response/Event/Invitation/Photo.php +++ b/src/Response/Event/Invitation/Photo.php @@ -1,6 +1,6 @@ text !== null; - } - - public function getText(): ?string { return $this->text; } - public function hasAnyPhotos(): bool - { - return \count($this->photos) > 0; - } - /** * @return Photo[] */ diff --git a/src/Response/Event/Organizer/ContactPerson.php b/src/Response/Event/Organizer/ContactPerson.php new file mode 100644 index 0000000..b8cb186 --- /dev/null +++ b/src/Response/Event/Organizer/ContactPerson.php @@ -0,0 +1,43 @@ +name; + } + + + public function getEmailAddress(): string + { + return $this->emailAddress; + } + + + public function getPhoneNumber(): string + { + return $this->phoneNumber; + } + +} diff --git a/src/Response/Event/Organizer/Organizer.php b/src/Response/Event/Organizer/Organizer.php index 7c4c436..454802a 100644 --- a/src/Response/Event/Organizer/Organizer.php +++ b/src/Response/Event/Organizer/Organizer.php @@ -1,6 +1,6 @@ organizers !== null; - } - - public function getOrganizers(): ?string { return $this->organizers; } - public function getContactPersonName(): ?string - { - return $this->contactPersonName; - } - - - public function getContactPhone(): string - { - return $this->contactPhone; - } - - - public function getContactEmail(): string + public function getContactPerson(): ContactPerson { - return $this->contactEmail; + return $this->contactPerson; } } diff --git a/src/Response/Event/Organizer/OrganizerOrganizationalUnit.php b/src/Response/Event/Organizer/OrganizerOrganizationalUnit.php index b94883a..0564d57 100644 --- a/src/Response/Event/Organizer/OrganizerOrganizationalUnit.php +++ b/src/Response/Event/Organizer/OrganizerOrganizationalUnit.php @@ -1,37 +1,32 @@ id; + return $this->name; } - public function getName(): string + public function getWebsite(): ?string { - return $this->name; + return $this->website; } } diff --git a/src/Response/Event/Place.php b/src/Response/Event/Place.php index 7cdc0ae..c625757 100644 --- a/src/Response/Event/Place.php +++ b/src/Response/Event/Place.php @@ -1,6 +1,8 @@ coordinates !== null; - } - - - public function getCoordinates(): ?string + public function getCoordinates(): ?Coordinates { return $this->coordinates; } diff --git a/src/Response/Event/Registration/RegistrationQuestion.php b/src/Response/Event/Registration/RegistrationQuestion.php index a2687fa..2751ccb 100644 --- a/src/Response/Event/Registration/RegistrationQuestion.php +++ b/src/Response/Event/Registration/RegistrationQuestion.php @@ -1,6 +1,6 @@ hasValidData = match (true) { - $type->equals(RegistrationTypeEnum::EMAIL()) && $email === null, - $type->equals(RegistrationTypeEnum::EXTERNAL_WEBPAGE()) && $url === null, - => false, - default => true - }; - } + ) {} /** * @param RegistrationQuestion[] $questions @@ -51,11 +41,6 @@ public function isOfTypeBrontoWeb(): bool } - public function areAnyQuestions(): bool - { - return \count($this->questions) > 0; - } - /** * @return RegistrationQuestion[] */ @@ -72,18 +57,19 @@ public function isOfTypeEmail(): bool return $this->type->equals(RegistrationTypeEnum::EMAIL()); } - /** - * @throws RegistrationTypeException - * @throws BadUsageException - */ public function getEmail(): ?string { - if ( ! $this->hasValidData()) { - throw RegistrationTypeException::missingAdditionalData('email', $this->type); + if ( ! $this->isOfTypeEmail()) { + throw new UsageException('This method can not be called when the registration is not of `via e-mail` type.'); } - if ( ! $this->isOfTypeEmail()) { - throw new BadUsageException('This method can not be called when the registration is not of `via e-mail` type.'); + if ($this->email === null) { + /* + * Ideally, this should not happen, but we can not rely on it. If it happens, we want to know about it -> + * assert() is not enough. We can just log it, but that would lead user into clicking a button which does nothing. + * Thus rendering error page covers both – logging and preventing user from accessing non-working page. + */ + throw new RuntimeException('E-mail must not be null in case of registration via e-mail.'); } return $this->email; @@ -97,18 +83,19 @@ public function isOfTypeCustomWebpage(): bool return $this->type->equals(RegistrationTypeEnum::EXTERNAL_WEBPAGE()); } - /** - * @throws RegistrationTypeException - * @throws BadUsageException - */ public function getUrl(): ?string { - if ( ! $this->hasValidData()) { - throw RegistrationTypeException::missingAdditionalData('url', $this->type); + if ( ! $this->isOfTypeCustomWebpage()) { + throw new UsageException('This method can not be called when the registration is not of `via custom webpage` type.'); } - if ( ! $this->isOfTypeCustomWebpage()) { - throw new BadUsageException('This method can not be called when the registration is not of `via custom webpage` type.'); + if ($this->url === null) { + /* + * Ideally, this should not happen, but we can not rely on it. If it happens, we want to know about it -> + * assert() is not enough. We can just log it, but that would lead user into clicking a button which does nothing. + * Thus rendering error page covers both – logging and preventing user from accessing non-working page. + */ + throw new RuntimeException('URL must not be null in case of registration via custom webpage.'); } return $this->url; @@ -130,10 +117,4 @@ public function isOfTypeDisabled(): bool return $this->type->equals(RegistrationTypeEnum::DISABLED()); } - - public function hasValidData(): bool - { - return $this->hasValidData; - } - } diff --git a/src/Response/Event/Registration/RegistrationTypeEnum.php b/src/Response/Event/Registration/RegistrationTypeEnum.php index a974aa5..8966b69 100644 --- a/src/Response/Event/Registration/RegistrationTypeEnum.php +++ b/src/Response/Event/Registration/RegistrationTypeEnum.php @@ -1,6 +1,6 @@ id, + $data->name, + $data->street, + $data->city, + $data->zip_code, + $data->gps_latitude !== null && $data->gps_longitude !== null + ? Coordinates::from($data->gps_latitude, $data->gps_longitude) + : null, + $data->telephone, + $data->from_email_address, + $data->web_url, + OrganizationalUnitType::fromScalar($data->level), + $data->president_name, + $data->manager_name, ); } @@ -82,13 +75,19 @@ public function getPostCode(): string } - public function getPhone(): ?string + public function getCoordinates(): ?Coordinates + { + return $this->coordinates; + } + + + public function getPhone(): string { return $this->phone; } - public function getEmail(): ?string + public function getEmail(): string { return $this->email; } @@ -100,13 +99,13 @@ public function getWebsite(): ?string } - public function getChairman(): ?string + public function getChairman(): string { return $this->chairman; } - public function getManager(): ?string + public function getManager(): string { return $this->manager; } diff --git a/src/Response/OrganizationalUnit/OrganizationalUnitType.php b/src/Response/OrganizationalUnit/OrganizationalUnitType.php index e294b73..617bf63 100644 --- a/src/Response/OrganizationalUnit/OrganizationalUnitType.php +++ b/src/Response/OrganizationalUnit/OrganizationalUnitType.php @@ -1,6 +1,6 @@ httpResponse = $httpResponse; - - $this->parseDom($domDocument); - } - - - private function parseDom(\DOMDocument $domDocument): void - { - $domFinder = new \DOMXPath($domDocument); - $rowNodeList = $domFinder->query('*', $domDocument->getElementsByTagName(self::TAG_RESULT)->item(0)); - - $this->data = []; - foreach ($rowNodeList as $rowNode) { - \assert($rowNode instanceof \DOMElement); - - $row = []; - foreach ($domFinder->query('*', $rowNode) as $node) { - \assert($node instanceof \DOMElement); - - // if there is an ID attribute, use this one a the value as it is numeric representation (thus more technically reliable) of element's content - $row[$node->nodeName] = $node->hasAttribute(self::TAG_ATTRIBUTE_ID) ? - $node->getAttribute(self::TAG_ATTRIBUTE_ID) - : - $node->nodeValue; - } - - $this->data[] = $row; - } - } - - - public function getHttpResponse(): ResponseInterface - { - return $this->httpResponse; - } - - - public function getData(): array - { - return $this->data; - } - -} diff --git a/src/Response/exceptions.php b/src/Response/exceptions.php deleted file mode 100644 index a08b202..0000000 --- a/src/Response/exceptions.php +++ /dev/null @@ -1,70 +0,0 @@ -nodeValue); - } - -} - -final class UnauthorizedAccessException extends ResponseErrorException -{ - - public function __construct() - { - parent::__construct('You are not authorized to make such request with given credentials. Or you have simply wrong credentials. :-)'); - } - -} - -final class UnknownErrorException extends ResponseErrorException -{ - - /** - * @param string $unknownErrorTypeKey - */ - public function __construct($unknownErrorTypeKey) - { - parent::__construct('Unknown error. Error type returned from BIS: ' . $unknownErrorTypeKey); - } - -} - -final class RegistrationTypeException extends ResponseErrorException -{ - - public static function missingAdditionalData($key, $type) - { - return new self(\sprintf('Missing additional data `%s` for selected type %d.', $key, $type)); - } - -} diff --git a/src/exceptions.php b/src/exceptions.php index d9e64d9..b3605ae 100644 --- a/src/exceptions.php +++ b/src/exceptions.php @@ -1,33 +1,54 @@ getMessage(), 0, $previous); + } +} + +final class UnableToAuthorize extends BisClientRuntimeException +{ + public static function withPrevious(\Throwable $previous): self + { + return new self("You are not authorized to make such request with given secrets.\nCheck that you passed correct secrets or that you have access to the resource you requested.", 0, $previous); + } +} + +final class NotFound extends BisClientRuntimeException +{ + public static function withPrevious(\Throwable $previous): self + { + return new self('The target you requested was not found. Check again that you\'ve typed correct URL or that the resource exists.', 0, $previous); + } +} +final class ConnectionToBisFailed extends BisClientRuntimeException +{ + public static function withPrevious(\Throwable $previous): self + { + return new self($previous->getMessage(), 0, $previous); + } +} From 2de35f55c12091abcfde8e47500a14a2e65e56b7 Mon Sep 17 00:00:00 2001 From: Daniel Kurowski Date: Fri, 8 Oct 2021 09:11:00 +0200 Subject: [PATCH 4/8] Parametrized API url --- README.md | 3 ++- src/BisClientFactory.php | 3 ++- src/Endpoint.php | 12 +++++------- tests/bootstrap.php | 3 ++- tests/secret.template.php | 1 + 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cdc124f..1d83373 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,12 @@ Download latest version from [github](https://github.com/hnuti-brontosaurus/php- # Usage -First you need to create client instance. Note that you need to obtain client ID and secret from BIS administrator +First you need to create client instance. Note that you need to know the API URL and obtain client ID and secret from BIS administrator to be able to authenticate against BIS. ```php $client = (new BisClientFactory( + 'apiUrl', 'clientId', 'clientSecret', ))->create(); diff --git a/src/BisClientFactory.php b/src/BisClientFactory.php index a5dcf33..551a140 100644 --- a/src/BisClientFactory.php +++ b/src/BisClientFactory.php @@ -18,10 +18,11 @@ final class BisClientFactory public function __construct( + string $apiUrl, private string $clientId, private string $clientSecret, ) { - $this->httpClient = new Client(); + $this->httpClient = new Client(['base_uri' => \rtrim($apiUrl, '/') . '/']); $this->bisAuthenticator = new Authenticator( $this->clientId, $this->clientSecret, diff --git a/src/Endpoint.php b/src/Endpoint.php index c0b430f..83f21d8 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -5,32 +5,30 @@ final class Endpoint { - private const ENDPOINT_BASE_URL = 'https://klub-diakonie-devel.herokuapp.com/api'; - public static function AUTHENTICATION(): string { - return \sprintf('%s/o/token/', self::ENDPOINT_BASE_URL); + return 'o/token/'; } public static function EVENT(int $id): string { - return \sprintf('%s/bronto/event/%d/', self::ENDPOINT_BASE_URL, $id); + return \sprintf('bronto/event/%d/', $id); } public static function EVENTS(): string { - return \sprintf('%s/bronto/event/', self::ENDPOINT_BASE_URL); + return 'bronto/event/'; } public static function ADMINISTRATIVE_UNITS(): string { - return \sprintf('%s/bronto/administrative_unit/', self::ENDPOINT_BASE_URL); + return 'bronto/administrative_unit/'; } public static function ADD_ATTENDEE_TO_EVENT(): string { - return \sprintf('%s/bronto/register_userprofile_interaction/', self::ENDPOINT_BASE_URL); + return 'bronto/register_userprofile_interaction/'; } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index cd71a01..3ab7d73 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,9 +12,10 @@ // wrapped in IIFE not to pollute script with client* variables return (function () { $secret = require_once __DIR__ . '/secret.php'; - ['clientId' => $clientId, 'clientSecret' => $clientSecret] = $secret; + ['apiUrl' => $apiUrl, 'clientId' => $clientId, 'clientSecret' => $clientSecret] = $secret; return (new BisClientFactory( + $apiUrl, $clientId, $clientSecret, ))->create(); diff --git a/tests/secret.template.php b/tests/secret.template.php index 286de78..dcafece 100644 --- a/tests/secret.template.php +++ b/tests/secret.template.php @@ -1,6 +1,7 @@ '', 'clientId' => '', 'clientSecret' => '', ]; From 161bf5f59cef30b944829e002ea530bb5c812088 Mon Sep 17 00:00:00 2001 From: Daniel Kurowski Date: Fri, 17 Dec 2021 07:21:54 +0100 Subject: [PATCH 5/8] Support multiple diets --- src/Response/Event/Event.php | 6 +----- src/Response/Event/Invitation/Food.php | 16 ++++++++++------ src/Response/Event/Invitation/Invitation.php | 15 ++++++++++++--- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Response/Event/Event.php b/src/Response/Event/Event.php index 001c1ec..f8c2420 100644 --- a/src/Response/Event/Event.php +++ b/src/Response/Event/Event.php @@ -79,10 +79,6 @@ public static function fromResponseData(\stdClass $data): self // invitation - $food = $data->diet !== null - ? Food::fromScalar($data->diet) - : Food::NOT_LISTED(); - /** @var Photo[] $invitationPresentationPhotos */ $invitationPresentationPhotos = []; for ($i = 1; $i <= 6; $i++) { @@ -97,7 +93,7 @@ public static function fromResponseData(\stdClass $data): self $data->invitation_text_1, $data->invitation_text_2, $data->accommodation !== '' ? $data->accommodation : null, - $food, + \array_map(static fn($diet) => Food::fromScalar($diet), $data->diet), $data->invitation_text_3, $data->working_hours, ($invitationPresentationText !== null || \count($invitationPresentationPhotos) > 0) diff --git a/src/Response/Event/Invitation/Food.php b/src/Response/Event/Invitation/Food.php index 5fe93f1..cdea814 100644 --- a/src/Response/Event/Invitation/Food.php +++ b/src/Response/Event/Invitation/Food.php @@ -7,17 +7,21 @@ /** - * @method static Food NOT_LISTED() - * @method static Food CHOOSEABLE() * @method static Food VEGETARIAN() * @method static Food NON_VEGETARIAN() + * @method static Food VEGAN() + * @method static Food KOSHER() + * @method static Food HALAL() + * @method static Food GLUTEN_FREE() */ final class Food extends Enum { use AutoInstances; - protected const NOT_LISTED = ''; - protected const CHOOSEABLE = 'can_choose'; - protected const VEGETARIAN = 'non_vegetarian'; - protected const NON_VEGETARIAN = 'vegetarian'; + protected const VEGETARIAN = 'vegetarian'; + protected const NON_VEGETARIAN = 'non_vegetarian'; + protected const VEGAN = 'vegan'; + protected const KOSHER = 'kosher'; + protected const HALAL = 'halal'; + protected const GLUTEN_FREE = 'gluten_free'; } diff --git a/src/Response/Event/Invitation/Invitation.php b/src/Response/Event/Invitation/Invitation.php index 3e32a9f..d9920a1 100644 --- a/src/Response/Event/Invitation/Invitation.php +++ b/src/Response/Event/Invitation/Invitation.php @@ -6,22 +6,28 @@ final class Invitation { + /** + * @param Food[] $food + */ private function __construct( private string $introduction, private string $organizationalInformation, private ?string $accommodation, - private Food $food, + private array $food, private ?string $workDescription, private ?int $workHoursPerDay, private ?Presentation $presentation, ) {} + /** + * @param Food[] $food + */ public static function from( string $introduction, string $organizationalInformation, ?string $accommodation, - Food $food, + array $food, ?string $workDescription, ?int $workHoursPerDay, ?Presentation $presentation, @@ -57,7 +63,10 @@ public function getAccommodation(): ?string } - public function getFood(): Food + /** + * @return Food[] + */ + public function getFood(): array { return $this->food; } From 82d5f93cedbf17e182cf123d2a87f4e74431ff26 Mon Sep 17 00:00:00 2001 From: Daniel Kurowski Date: Fri, 17 Dec 2021 07:29:37 +0100 Subject: [PATCH 6/8] Allow nullable for start date --- src/Response/Event/Event.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Response/Event/Event.php b/src/Response/Event/Event.php index f8c2420..9a0c898 100644 --- a/src/Response/Event/Event.php +++ b/src/Response/Event/Event.php @@ -35,7 +35,7 @@ private function __construct( private Organizer $organizer, private TargetGroup $targetGroup, private Invitation $invitation, - private \DateTimeImmutable $startDate, + private ?\DateTimeImmutable $startDate, private ?string $relatedWebsite, ) {} @@ -133,7 +133,9 @@ public static function fromResponseData(\stdClass $data): self $organizer, TargetGroup::fromScalar($data->indended_for), $invitation, - new \DateTimeImmutable($data->start_date), + $data->start_date !== null + ? new \DateTimeImmutable($data->start_date) + : null, $_relatedWebsite, ); } From 526ded78d5bbe072718f68b489a6ef3f2552359d Mon Sep 17 00:00:00 2001 From: Daniel Kurowski Date: Fri, 17 Dec 2021 07:30:12 +0100 Subject: [PATCH 7/8] Allow nullable for dateFrom, dateTo and place temporarily --- src/Response/Event/Event.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Response/Event/Event.php b/src/Response/Event/Event.php index 9a0c898..0e8d41c 100644 --- a/src/Response/Event/Event.php +++ b/src/Response/Event/Event.php @@ -117,10 +117,16 @@ public static function fromResponseData(\stdClass $data): self $data->id, $data->name, $data->main_photo, - \DateTimeImmutable::createFromFormat('Y-m-d', $data->date_from), - \DateTimeImmutable::createFromFormat('Y-m-d', $data->date_to), + $data->date_from === null + ? \DateTimeImmutable::createFromFormat('Y-m-d', '1970-01-01') // todo temp, until there are nullable dates coming from API + : \DateTimeImmutable::createFromFormat('Y-m-d', $data->date_from), + $data->date_to === null + ? \DateTimeImmutable::createFromFormat('Y-m-d', '1970-01-02') // todo temp, until there are nullable dates coming from API + : \DateTimeImmutable::createFromFormat('Y-m-d', $data->date_to), Program::fromScalar($data->program), - Place::from( + $data->location === null + ? Place::from('nezadáno', null) // todo temp, until there are nullable locations coming from API + : Place::from( $data->location->name, $data->location->gps_latitude !== null && $data->location->gps_longitude !== null ? Coordinates::from($data->location->gps_latitude, $data->location->gps_longitude) From 4093d102579f21e841ae97efdecd039d2353e24a Mon Sep 17 00:00:00 2001 From: Daniel Kurowski Date: Fri, 17 Dec 2021 07:31:50 +0100 Subject: [PATCH 8/8] update test --- tests/index.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/index.php b/tests/index.php index b826b9c..46cf922 100644 --- a/tests/index.php +++ b/tests/index.php @@ -23,9 +23,8 @@ )); }; // uncomment if you need to test it otherwise it would post on every page load -//$addAttendee(eventId: 13703); // ⚠ do not forget to customize event id not to pollute real events with testing data - - +//$addAttendee(eventId: 9513); // ⚠ do not forget to customize event id not to pollute real events with testing data +//exit; // ----------------------------- // retrieving information test // -----------------------------