-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(MPDZBS-877): zmscitizenapi into next #674
base: next
Are you sure you want to change the base?
Conversation
#574) * endpoint working * fix unit tests * one working test for post-reserve * some clean up * feat(ZMS-2517): fix up response for the reserve post request * feat(ZMS-2517): add more unit tests for validations * feat(ZMS-2517): add more unit tests for validations * feat(ZMS-2517): Zmscitizenapi Refactor Part 1 of 3 move controllers back to root * feat(ZMS-2517): Zmscitizenapi Refactor Part 2 of 3 create ZmsApiFacadeService ZmsApiClientService ValidationService MapperService UtilityHelper and delete fragmented services * feat(ZMS-2517): Zmscitizenapi Refactor Part 3 of 3 clean up validations and unit tests --------- Co-authored-by: Thomas Fink <[email protected]>
…by github commit history
* (feat MPDZBS-877) readd citizenapi to htaccess * feat(MPDZBS-877 ZMS-2517): Working Appointment update with too many emails exception * feat(MPDZBS-877 ZMS-2517): Test rendering working for update appointment test * feat(MPDZBS-877 ZMS-2517): Add 63 validation unit tests for appointment update * feat(MPDZBS-877 ZMS-2517): Refactor exception appointmentNotFound * feat(MPDZBS-877 ZMS-2517): Add two new edge case exception tests * feat(MPDZBS-877 ZMS-2517): Remove error_logs --------- Co-authored-by: Thomas Fink <[email protected]>
b57e687
to
1a34881
Compare
1a34881
to
c15dea6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bitte schau nochmal wegen Validation und Error Handling, siehe Kommentare.
@coderabbitai I need a new model and schema ThinnedContact
|
ddev-zms-web | [30-Dec-2024 16:12:52] WARNING: [pool www] child 65140 said into stderr: "NOTICE: PHP message: Call to undefined method BO\Zmsentities\Collection\ProviderList::first()"
|
Tip For best results, initiate chat on the files or code changes. @ThomasAFink: The error occurs because 1) Add a helper method like
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 16
♻️ Duplicate comments (5)
zmscitizenapi/tests/Zmscitizenapi/AppointmentUpdateTest.php (1)
208-1723
: 🛠️ Refactor suggestionRefactor validation tests using data providers.
The validation test methods from line 208 onwards are highly repetitive, varying only in input parameters and expected errors. This leads to:
- Excessive code duplication
- Harder maintenance
- Reduced readability
Refactor using a PHPUnit data provider:
/** * @dataProvider validationTestProvider */ public function testParameterValidation(array $parameters, array $expectedErrors) { $response = $this->render([], $parameters, [], 'POST'); $responseBody = json_decode((string) $response->getBody(), true); $this->assertEquals(400, $response->getStatusCode()); $this->assertEqualsCanonicalizing(['errors' => $expectedErrors], $responseBody); } public function validationTestProvider(): array { return [ 'all_invalid' => [ 'parameters' => [ 'processId' => null, 'authKey' => '', 'familyName' => '', 'email' => 'invalid-email', 'telephone' => '123', 'customTextfield' => 123 ], 'expectedErrors' => [ ErrorMessages::get('invalidProcessId'), ErrorMessages::get('invalidAuthKey'), ErrorMessages::get('invalidFamilyName'), ErrorMessages::get('invalidEmail'), ErrorMessages::get('invalidTelephone'), ErrorMessages::get('invalidCustomTextfield') ] ], // Add more test cases following the same pattern ]; }Benefits:
- Reduces code duplication
- Improves maintainability
- Makes test cases more readable
- Easier to add new test cases
zmsentities/schema/citizenapi/thinnedProvider.json (3)
3-7
:⚠️ Potential issueRestrict root level schema type to object
The schema allows the root to be an array, object, or null. Since this represents a single provider entity, it should be restricted to just "object" type to ensure consistent data structures.
"type": [ - "array", - "object", - "null" - ], + "type": "object",
8-19
:⚠️ Potential issueAdd required field constraints
The
id
field appears to be mandatory but is not marked as required in the schema."properties": { "id": { "type": "string", - "description": "Provider ID" + "description": "Provider ID", + "minLength": 1 }, "name": { "type": ["string", "null"], "description": "Provider name" } }, + "required": ["id"], + "additionalProperties": false
38-97
:⚠️ Potential issueRevise contact object structure
The contact property type includes "array" which seems incorrect for an address structure.
"contact": { "title": "ThinnedContact", "description": "Represents a simplified contact object for the citizen API.", - "type": [ - "array", - "object", - "null" - ], + "type": ["object", "null"],zmscitizenapi/src/Zmscitizenapi/Services/MapperService.php (1)
262-275
: 🛠️ Refactor suggestionReplace global variable usage with dependency injection.
Using the global
App::$source_name
makes the code harder to test and maintain.
🧹 Nitpick comments (24)
zmsentities/schema/citizenapi/thinnedContact.json (1)
5-43
: Consider revising the required/nullable field strategy.Currently, all fields are marked as required but also nullable. This creates a confusing contract where fields must always be present in the JSON but can have null values. This might lead to unnecessarily verbose JSON payloads.
Consider one of these approaches:
- Make truly optional fields not required, removing them from the "required" array.
- Make required fields non-nullable if they must always have a value.
Example of approach #1 - assuming
name
is truly optional:"name": { "type": ["string", "null"], "description": "Optional name field displayed for contact." }, // ... other properties ... "required": [ "city", "country", - "name", "postalCode", "region", "street", "streetNumber" ]
zmscitizenapi/tests/Zmscitizenapi/AppointmentUpdateTest.php (1)
12-78
: Remove duplicate response structure.The
expectedResponse
array at lines 50-75 is defined with identical content as the fixture. Consider extracting this into a shared test data provider or helper method.- $expectedResponse = [ - "processId" => 101002, - "timestamp" => "1727865900", - "authKey" => "fb43", - // ... (rest of the array) - ]; + $expectedResponse = $this->getExpectedSuccessResponse();Add this helper method:
private function getExpectedSuccessResponse(): array { return [ "processId" => 101002, "timestamp" => "1727865900", "authKey" => "fb43", // ... (rest of the array) ]; }zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php (1)
21-29
: Consider preserving null values for optional fields
Currently, passingnull
into the constructor results in setting empty strings. This can blur the distinction between genuinely unknown/unspecified values and empty strings in subsequent usage. If it’s important to differentiate between “no data” (null) and “empty string,” consider storing the original null.- $this->city = $city ?? ''; - $this->country = $country ?? ''; + $this->city = $city; + $this->country = $country;zmsentities/schema/citizenapi/thinnedProvider.json (1)
38-97
: Add examples for contact fieldsConsider adding examples to the schema to illustrate the expected format and improve developer experience.
"city": { "type": ["string", "null"], - "description": "The city name." + "description": "The city name.", + "examples": ["München"] }, "country": { "type": ["string", "null"], - "description": "The country name." + "description": "The country name.", + "examples": ["DE"] }, "postalCode": { "type": ["string", "null"], - "description": "The postal (ZIP) code." + "description": "The postal (ZIP) code.", + "examples": ["80331"] }zmscitizenapi/src/Zmscitizenapi/Models/Office.php (3)
14-28
: Enhance property documentationWhile the type hints are correct, the PHPDoc comments could be more descriptive to better explain each property's purpose and constraints.
Example improvement:
- /** @var int */ + /** + * Unique identifier for the office + * @var int + */ public int $id; - /** @var string */ + /** + * Official name of the office + * @var string + */ public string $name; - /** @var array|null */ + /** + * Physical address details of the office + * @var array{street: string, city: string, postal_code: string}|null + */ public ?array $address = null;
29-48
: Add parameter validation in constructorConsider validating parameters before assignment to ensure data integrity at the point of object creation.
public function __construct(int $id, string $name, ?array $address = null, ?array $geo = null, ?ThinnedScope $scope = null) { + if ($id <= 0) { + throw new InvalidArgumentException('Office ID must be a positive integer'); + } + if (empty(trim($name))) { + throw new InvalidArgumentException('Office name cannot be empty'); + } $this->id = $id; $this->name = $name; $this->address = $address;
49-54
: Enhance validation error messageThe current error message is generic. Consider including specific validation failures to help with debugging.
private function ensureValid() { - if (!$this->testValid()) { - throw new InvalidArgumentException("The provided data is invalid according to the schema."); + $validationResult = $this->testValid(); + if ($validationResult !== true) { + $errors = json_encode($this->getErrors()); + throw new InvalidArgumentException("Schema validation failed: $errors"); } }zmscitizenapi/tests/Zmscitizenapi/OfficesListTest.php (2)
12-21
: Consider enhancing API mock setup with data providerThe test setup could benefit from using a data provider to test different scenarios (e.g., empty list, single office, multiple offices).
Example implementation:
/** * @dataProvider provideOfficeListScenarios */ public function testRendering(array $apiResponse, array $expectedResponse) { $this->setApiCalls([[ 'function' => 'readGetResult', 'url' => '/source/unittest/', 'parameters' => ['resolveReferences' => 2], 'response' => $apiResponse ]]); $response = $this->render(); $this->assertEquals(200, $response->getStatusCode()); $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true)); } public function provideOfficeListScenarios(): array { return [ 'multiple_offices' => [ 'apiResponse' => $this->readFixture("GET_SourceGet_dldb.json"), 'expectedResponse' => ['offices' => [ /* ... */ ]] ], 'empty_list' => [ 'apiResponse' => ['data' => ['providers' => []]], 'expectedResponse' => ['offices' => []] ] ]; }
102-103
: Add more specific assertions for response validationThe current assertions only check the status code and response structure. Consider adding more specific assertions to validate critical business rules.
Example:
// Validate specific business rules $this->assertNotEmpty($responseBody['offices'], 'Office list should not be empty'); foreach ($responseBody['offices'] as $office) { $this->assertArrayHasKey('scope', $office, 'Each office must have a scope'); $this->assertNotNull($office['scope']['provider'], 'Each scope must have a provider'); $this->assertEquals($office['id'], $office['scope']['provider']['id'], 'Office ID must match provider ID'); }zmscitizenapi/tests/Zmscitizenapi/fixtures/GET_SourceGet_dldb.json (2)
6-6
: Update the generated timestamp to a more recent dateThe fixture uses a static timestamp from 2019, which might not reflect current testing scenarios.
- "generated": "2019-02-08T14:45:15+01:00", + "generated": "2024-12-01T14:45:15+01:00",
81-83
: Consider expanding combinable services test dataThe
combinable
array in the request data could be enhanced to test more complex service combinations."data": { "json": "data", "key": "value", - "combinable": ["1", "2"] + "combinable": ["1", "2"], + "restrictions": { + "maxCombined": 3, + "excludedServices": ["3"], + "requiredServices": ["1"] + } }zmscitizenapi/src/Zmscitizenapi/Services/MapperService.php (3)
94-130
: Consider optimizing service mapping for better performance.While the implementation is correct, consider these optimizations:
- Pre-calculate service provider mappings
- Use array_filter for filtering common providers
public static function mapServicesWithCombinations(RequestList $requestList, RequestRelationList $relationList): ServiceList { + // Pre-calculate service provider mappings + /** @var array<string, array<int>> $servicesProviderIds */ + $servicesProviderIds = array_reduce( + iterator_to_array($relationList), + function (array $carry, $relation) { + $serviceId = $relation->request->id; + $carry[$serviceId][] = $relation->provider->id; + return $carry; + }, + [] + ); - /** @var array<string, array<int>> $servicesProviderIds */ - $servicesProviderIds = []; - foreach ($relationList as $relation) { - $serviceId = $relation->request->id; - $servicesProviderIds[$serviceId] ??= []; - $servicesProviderIds[$serviceId][] = $relation->provider->id; - }
184-230
: Optimize process mapping for better readability and performance.The process mapping logic is complex and could benefit from:
- Extracting request counting logic to a separate method
- Using null coalescing operator consistently
- Reducing nested conditionals
public static function processToThinnedProcess(Process $myProcess): ThinnedProcess { if (!$myProcess || !isset($myProcess->id)) { return new ThinnedProcess(); } + private static function countRequests(array $requests): array + { + $mainServiceId = $requests[0]->id ?? null; + $counts = ['main' => 0, 'sub' => []]; + + foreach ($requests as $request) { + if ($request->id === $mainServiceId) { + $counts['main']++; + continue; + } + + $counts['sub'][$request->id]['id'] ??= $request->id; + $counts['sub'][$request->id]['count'] ??= 0; + $counts['sub'][$request->id]['count']++; + } + + return $counts; + }
300-311
: Improve error handling in contact mapping.The method should validate input types and handle invalid data more gracefully.
public static function contactToThinnedContact($contact): ThinnedContact { + if (!is_array($contact) && !is_object($contact)) { + throw new \InvalidArgumentException('Contact must be an array or object'); + } + return new ThinnedContact( - $contact['city'] ?? $contact->city ?? '', - $contact['country'] ?? $contact->country ?? '', + self::getContactValue($contact, 'city'), + self::getContactValue($contact, 'country'), // ... similar for other fields ); } + +private static function getContactValue($contact, string $field): string +{ + if (is_array($contact)) { + return $contact[$field] ?? ''; + } + return $contact->$field ?? ''; +}zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (3)
156-161
: Address TODO comments with implementation plans.Multiple TODO comments indicate missing functionality. Please provide implementation details or create GitHub issues to track these pending features:
- getCombinableServicesByIds
- getOfficeByIds
- getOfficesThatProvideService
- preconfirmAppointment
- confirmAppointment
- cancelAppointment
Would you like me to help create GitHub issues for tracking these pending implementations?
Also applies to: 220-225, 412-417, 736-741, 744-749, 751-756
508-539
: Optimize array operations in getFreeAppointments.The current implementation uses
array_merge
in a loop which can be inefficient for large datasets.Consider pre-allocating the array:
- $requests = []; - foreach ($serviceIds as $index => $serviceId) { - $service = [ - 'id' => $serviceId, - 'source' => \App::$source_name, - 'slotCount' => $serviceCounts[$index] - ]; - $requests = array_merge($requests, array_fill(0, $service['slotCount'], $service)); - } + $totalSlots = array_sum($serviceCounts); + $requests = array_pad([], $totalSlots, []); + $currentIndex = 0; + + foreach ($serviceIds as $index => $serviceId) { + $service = [ + 'id' => $serviceId, + 'source' => \App::$source_name, + 'slotCount' => $serviceCounts[$index] + ]; + for ($i = 0; $i < $service['slotCount']; $i++) { + $requests[$currentIndex++] = $service; + } + }
586-623
: Optimize processFreeSlots method using array operations.The method can be optimized using array operations for better performance.
The current implementation using
array_reduce
withiterator_to_array
is good, but consider adding early validation:private static function processFreeSlots(ProcessList $freeSlots): ProcessFreeSlots|array { + if ($freeSlots->count() === 0) { + return new ProcessFreeSlots([]); + } + $errors = ValidationService::validateGetProcessFreeSlots($freeSlots); if (!empty($errors['errors'])) { return $errors; }zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php (3)
1-2
: Consider adding a file-level docblock or PHP type declarations.Although this is a new file, adding a short description or docblock at the file level (or using strict types by adding
declare(strict_types=1);
) can improve readability and maintainability.
9-30
: Use of public fields is acceptable, but consider an immutable or encapsulated design.The direct usage of public properties is fine for data models, especially for entities. However, opting for getters/setters (or an immutable object pattern) can offer clearer change tracking, validation, and enhanced control over property state changes.
49-54
: Validation method is concise, but consider expanding error detail.While throwing an
InvalidArgumentException
is fine, providing additional context (e.g., which fields failed validation) can help debugging. Consider expanding the exception message to identify the problematic property or validation detail.- throw new InvalidArgumentException("The provided data is invalid according to the schema."); + throw new InvalidArgumentException( + "The provided data is invalid. Please check the schema constraints for properties like 'id', 'name', etc." + );zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (1)
197-225
: Reduce duplication in email methods.Extract common URL construction logic and add parameter validation.
Consider this improvement:
+ private function constructEmailUrl(Process $process, string $type): string + { + if (!$process->id || !$process->authKey) { + throw new \InvalidArgumentException('Process must have valid ID and auth key'); + } + return "/process/{$process->id}/{$process->authKey}/{$type}/mail/"; + } public function sendConfirmationEmail(Process $process): Process { try { - $url = "/process/{$process->id}/{$process->authKey}/confirmation/mail/"; + $url = $this->constructEmailUrl($process, 'confirmation'); return \App::$http->readPostResult($url, $process)->getEntity();zmscitizenapi/src/Zmscitizenapi/Models/Service.php (1)
27-30
: Add null to the docblock for$combinable
.To remain consistent with the actual property type, the docblock should reflect
@var Combinable|null
.-/** @var Combinable */ +/** @var Combinable|null */zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php (1)
23-25
: Validate whether frequent schema checks are needed in the constructor.Calling
$this->ensureValid();
on every instantiation can be beneficial for data integrity but might introduce runtime overhead if objects are created repeatedly in performance-critical sections. If performance becomes an issue, consider providing an option to skip schema validation when the data source is already trusted or validated.zmsentities/schema/citizenapi/processFreeSlots.json (1)
7-12
: Reconcile nullable timestamps in model vs. schema.The schema allows
"type": ["array", "null"]
forappointmentTimestamps
, yet the PHP property is typed solely as an array and does not handlenull
. Consider revising either the schema or the model to avoid inconsistencies that could lead to runtime errors.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
zmscitizenapi/src/Zmscitizenapi/Models/AvailableAppointments.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/AvailableDays.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeList.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceAndRelationList.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceRelationList.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/Collections/ServiceList.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/Collections/ThinnedScopeList.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/Combinable.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/Office.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/OfficeServiceRelation.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/Service.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProcess.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/ThinnedScope.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Services/MapperService.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/AppointmentByIdTest.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/AppointmentReserveTest.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/AppointmentUpdateTest.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/OfficesByServiceListTest.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/OfficesListTest.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/OfficesServicesRelationsTest.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/ScopeByIdTest.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/ScopesListTest.php
(1 hunks)zmscitizenapi/tests/Zmscitizenapi/fixtures/GET_SourceGet_dldb.json
(1 hunks)zmsentities/schema/citizenapi/processFreeSlots.json
(1 hunks)zmsentities/schema/citizenapi/thinnedContact.json
(1 hunks)zmsentities/schema/citizenapi/thinnedProcess.json
(1 hunks)zmsentities/schema/citizenapi/thinnedProvider.json
(1 hunks)zmsentities/schema/citizenapi/thinnedScope.json
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (18)
- zmscitizenapi/tests/Zmscitizenapi/ScopeByIdTest.php
- zmscitizenapi/tests/Zmscitizenapi/ScopesListTest.php
- zmscitizenapi/src/Zmscitizenapi/Models/AvailableDays.php
- zmscitizenapi/tests/Zmscitizenapi/AppointmentReserveTest.php
- zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProcess.php
- zmscitizenapi/tests/Zmscitizenapi/OfficesServicesRelationsTest.php
- zmscitizenapi/src/Zmscitizenapi/Models/OfficeServiceRelation.php
- zmscitizenapi/tests/Zmscitizenapi/OfficesByServiceListTest.php
- zmsentities/schema/citizenapi/thinnedProcess.json
- zmsentities/schema/citizenapi/thinnedScope.json
- zmscitizenapi/src/Zmscitizenapi/Models/AvailableAppointments.php
- zmscitizenapi/src/Zmscitizenapi/Models/Collections/ThinnedScopeList.php
- zmscitizenapi/src/Zmscitizenapi/Models/Combinable.php
- zmscitizenapi/src/Zmscitizenapi/Models/Collections/ServiceList.php
- zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceRelationList.php
- zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceAndRelationList.php
- zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeList.php
- zmscitizenapi/src/Zmscitizenapi/Models/ThinnedScope.php
🔇 Additional comments (25)
zmscitizenapi/tests/Zmscitizenapi/AppointmentUpdateTest.php (2)
1-10
: LGTM! Class structure follows PSR-4 autoloading standard.
The namespace and class declaration are properly structured. Good use of importing the ErrorMessages class for centralized error handling.
80-206
: LGTM! Comprehensive error case testing.
Good coverage of important error scenarios:
- Too many appointments with same email
- Appointment not found
- Auth key mismatch
zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php (3)
8-8
: Class declaration is consistent and aligns with project naming conventions
The class name ThinnedContact
clearly conveys its purpose—a reduced or simplified representation of contact data. Inheritance from Entity
and usage of \JsonSerializable
appear consistent with the project’s design patterns.
38-50
: Ensure the JSON schema accommodates empty strings
With the current approach, fields default to ''
rather than null
. Confirm that the JSON schema in thinnedContact.json
allows empty strings even if the original intent was for null
. If it doesn’t, validation might pass/ fail unexpectedly.
Would you like me to open a new GitHub issue or add a test script to verify that the schema handles empty strings consistently?
52-69
: Use of toArray()
and jsonSerialize()
is coherent
The pattern of returning $this->toArray()
from jsonSerialize()
is standard and offers a single point of truth for marshaling data into arrays/ JSON. This makes the code more maintainable.
zmscitizenapi/src/Zmscitizenapi/Models/Office.php (2)
1-9
: LGTM!
The namespace and imports are well-organized and include all necessary dependencies.
10-12
: Update schema path to match project convention
The schema path should follow the project convention used by other models.
- public static $schema = 'citizenapi/office.json';
+ public static $schema = 'zmsentities/schema/citizenapi/office.json';
zmscitizenapi/tests/Zmscitizenapi/fixtures/GET_SourceGet_dldb.json (1)
141-179
: Verify appointment dates and availability patterns
The appointment dates are set for August 2024 with specific availability patterns. Consider:
- Adding edge cases for appointment availability
- Including non-bookable days to test different status scenarios
- Testing boundary conditions for the appointment window
Run this script to check the appointment date handling in the codebase:
✅ Verification successful
Based on the search results, I can now provide a comprehensive verification of the appointment dates and test coverage:
Test fixtures appropriately cover appointment date scenarios
The test fixtures and codebase show robust date handling and validation:
- The system has comprehensive date validation through
DateTimeFormatHelper
which validates ISO date formats and timestamps - The test fixtures include:
- Regular bookable days with varying availability (public, intern, callcenter slots)
- Proper date format validation (YYYY-MM-DD) in
ValidationService
- Error handling for invalid dates and unavailable appointments
- Edge cases are covered through:
- Validation of future dates (timestamps > current time)
- Different appointment status scenarios (bookable, not available)
- Boundary checks in appointment window validation
The test data in GET_SourceGet_dldb.json
with August 2024 dates is appropriate as:
- It provides multiple consecutive days to test date sequence handling
- It includes varying slot availability patterns
- The dates are within a reasonable future timeframe
- The schema validation is properly enforced
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Verify appointment date handling logic
# Check for date validation and processing in the codebase
# Look for date handling logic
ast-grep --pattern 'function $FUNC($$$) {
$$$
if ($DATE $OP $$$) {
$$$
}
$$$
}'
# Search for date-related validation
rg -A 3 'validateDate|isValidDate|checkDate'
Length of output: 140
Script:
#!/bin/bash
# Let's try a different approach to find date-related logic and test files
# Search for date-related patterns in test files
rg -g "*.php" -g "*.json" "(date|appointment|booking|schedule)" --type-add 'json:*.json' -A 3
# Find all test fixture files related to appointments or dates
fd -e json -e php "appointment|date|book" tests/
# Look for specific date format patterns (YYYY-MM-DD or similar)
rg -g "*.json" -g "*.php" "\d{4}[-./]\d{2}[-./]\d{2}" --type-add 'json:*.json'
Length of output: 79729
zmscitizenapi/src/Zmscitizenapi/Services/MapperService.php (1)
1-29
: LGTM! Well-organized file structure with clear namespace and import declarations.
The file follows PSR-4 autoloading standards and has a clear separation between models from different namespaces.
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (1)
1-29
: LGTM! Well-organized imports and declarations.
The file follows PSR-4 standards and has proper strict typing declaration. All necessary dependencies are clearly imported and organized by their types (Models, Collections, Services).
zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php (4)
3-8
: Namespace and imports look consistent.
The namespace BO\Zmscitizenapi\Models
is appropriate, and importing relevant classes (Entity
, InvalidArgumentException
, JsonSerializable
) aligns with best practices.
31-47
: Constructor leverages nullable typed properties effectively.
The approach of using nullable typed parameters is clear and ensures flexible creation of the object. The call to ensureValid()
in the constructor helps maintain data integrity from the start.
56-72
: Excellent use of a toArray()
helper.
This method ensures consistent serialization across the codebase. Especially beneficial if you later decide to modify object structure or property names without affecting external consumers.
73-76
: jsonSerialize()
correctly delegates to toArray()
.
Leveraging toArray()
in jsonSerialize()
is a good approach for code reuse and consistency.
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (1)
1-15
: Well-structured file with proper organization!
The file follows PSR standards with strict typing, proper namespace declaration, and well-organized imports.
zmscitizenapi/src/Zmscitizenapi/Models/Service.php (5)
13-13
: Schema path may be incorrect.
Based on earlier findings (see previous review comments), the file citizenapi/service.json
might not exist in this location. Ensure the path is correct; otherwise, update this reference to the actual schema location.
27-44
: Consider validating $maxQuantity
.
As suggested in a prior review, it would be beneficial to validate that $maxQuantity > 0
when provided, to avoid potential logical errors.
39-47
: Constructor usage of ensureValid()
looks good.
Invoking ensureValid()
at the end of construction ensures the object is validated early. This pattern promotes better data integrity.
49-54
: Good use of schema-based validation.
Using the parent class method testValid()
enforces schema compliance. Ensure the schema itself is up to date regarding optional properties like maxQuantity
and combinable
.
61-74
: Serialization approach is clean and straightforward.
Exposing an associative array and utilizing the built-in jsonSerialize()
method follows best practices for data models in PHP.
zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php (2)
28-33
: Schema-based validation appears solid.
Your validation logic aligns with the JSON schema, throwing an InvalidArgumentException
when the data doesn't match. This helps maintain robust data integrity at the model layer.
40-53
: Good use of JsonSerializable and structured serialization.
The toArray
method is clean and consistent, and returning it from jsonSerialize
ensures the data is always in a predictable format. This is a neat and maintainable approach to object serialization.
zmscitizenapi/tests/Zmscitizenapi/AppointmentByIdTest.php (3)
10-82
: Test coverage is comprehensive for the successful scenario.
It effectively handles API call setup, HTTP response rendering, and result validation. Good job ensuring the critical fields are validated (e.g., timestamp, office details, scope, etc.).
84-116
: Consolidate repetitive parameter validation tests with data providers (duplicate of a previous suggestion).
These tests for missing or invalid parameters (e.g., testMissingProcessId
, testMissingAuthKey
, testInvalidProcessId
, etc.) mostly follow the same pattern of rendering the response, parsing it, and verifying a 400 status code. This can be streamlined through PHPUnit data providers to reduce duplication and simplify maintenance.
Also applies to: 118-152, 154-168
206-238
: Confirm correctness of 406 status code upon 'authKeyMismatch'.
Typically, 401 or 403 might be used when authorization fails. Please verify that 406 aligns with the intended semantics.
Would you like to run a repository-wide search to check whether other controllers or tests also use 406 in similar situations or if there's a discrepancy?
✅ Verification successful
Let me search for more specific information about HTTP status codes usage in the codebase.
Let me check how authKey validation is handled in other parts of the codebase.
406 status code is consistently used for authKey validation failures
The codebase shows a deliberate pattern of using HTTP 406 (Not Acceptable) for authentication key mismatches:
- The
HTTP_NOT_ACCEPTABLE
(406) status code is defined inErrorMessages.php
and consistently used forauthKeyMismatch
errors - This is part of a broader error handling system where 406 is used for various validation failures, not just auth
- The implementation is consistent across multiple endpoints that handle auth key validation
While 401 or 403 might seem more semantically appropriate for auth failures, changing this would require a broader refactoring of the error handling system. The current implementation appears to be an intentional design choice that's consistently applied.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Searching for 406 usage and references to 'authKeyMismatch' across the codebase
rg -A 5 '(406|authKeyMismatch)'
Length of output: 75653
Script:
#!/bin/bash
# Search for HTTP status code constants and their usage
rg -A 2 "HTTP_NOT_ACCEPTABLE|406" zmscitizenapi/src/
Length of output: 2666
Script:
#!/bin/bash
# Search for authKey related error handling
rg -A 5 "authKeyMismatch|AuthKeyMatchFailed" zmscitizenapi/src/
Length of output: 8862
"lat": { | ||
"type": [ | ||
"number", | ||
"null" | ||
], | ||
"description": "Latitude in decimal degrees." | ||
}, | ||
"lon": { | ||
"type": [ | ||
"number", | ||
"null" | ||
], | ||
"description": "Longitude in decimal degrees." | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add coordinate range validation
The latitude and longitude fields should include range validation to ensure valid coordinates:
- Latitude: -90 to 90 degrees
- Longitude: -180 to 180 degrees
"lat": {
"type": [
"number",
"null"
],
- "description": "Latitude in decimal degrees."
+ "description": "Latitude in decimal degrees.",
+ "minimum": -90,
+ "maximum": 90
},
"lon": {
"type": [
"number",
"null"
],
- "description": "Longitude in decimal degrees."
+ "description": "Longitude in decimal degrees.",
+ "minimum": -180,
+ "maximum": 180
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
"lat": { | |
"type": [ | |
"number", | |
"null" | |
], | |
"description": "Latitude in decimal degrees." | |
}, | |
"lon": { | |
"type": [ | |
"number", | |
"null" | |
], | |
"description": "Longitude in decimal degrees." | |
}, | |
"lat": { | |
"type": [ | |
"number", | |
"null" | |
], | |
"description": "Latitude in decimal degrees.", | |
"minimum": -90, | |
"maximum": 90 | |
}, | |
"lon": { | |
"type": [ | |
"number", | |
"null" | |
], | |
"description": "Longitude in decimal degrees.", | |
"minimum": -180, | |
"maximum": 180 | |
}, |
public static function mapOfficesWithScope(ProviderList $providerList): OfficeList | ||
{ | ||
$offices = []; | ||
$scopes = ZmsApiFacadeService::getScopes(); | ||
|
||
if (is_array($scopes)) { | ||
return new OfficeList(); | ||
} | ||
|
||
foreach ($providerList as $provider) { | ||
$providerScope = self::mapScopeForProvider((int) $provider->id, $scopes); | ||
|
||
$offices[] = new Office( | ||
id: isset($provider->id) ? (int) $provider->id : 0, | ||
name: isset($provider->displayName) ? $provider->displayName : (isset($provider->name) ? $provider->name : null), | ||
address: isset($provider->data['address']) ? $provider->data['address'] : null, | ||
geo: isset($provider->data['geo']) ? $provider->data['geo'] : null, | ||
scope: isset($providerScope) && !isset($providerScope['errors']) ? new ThinnedScope( | ||
id: isset($providerScope->id) ? (int) $providerScope->id : 0, | ||
provider: isset($providerScope->provider) ? $providerScope->provider : null, | ||
shortName: isset($providerScope->shortName) ? $providerScope->shortName : null, | ||
telephoneActivated: isset($providerScope->telephoneActivated) ? (bool) $providerScope->telephoneActivated : null, | ||
telephoneRequired: isset($providerScope->telephoneRequired) ? (bool) $providerScope->telephoneRequired : null, | ||
customTextfieldActivated: isset($providerScope->customTextfieldActivated) ? (bool) $providerScope->customTextfieldActivated : null, | ||
customTextfieldRequired: isset($providerScope->customTextfieldRequired) ? (bool) $providerScope->customTextfieldRequired : null, | ||
customTextfieldLabel: isset($providerScope->customTextfieldLabel) ? $providerScope->customTextfieldLabel : null, | ||
captchaActivatedRequired: isset($providerScope->captchaActivatedRequired) ? (bool) $providerScope->captchaActivatedRequired : null, | ||
displayInfo: isset($providerScope->displayInfo) ? $providerScope->displayInfo : null | ||
) : null | ||
); | ||
} | ||
|
||
return new OfficeList($offices); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Improve type safety and error handling in office mapping.
The method has potential issues with type confusion and complex object construction.
- Replace
is_array($scopes)
check with more specific type checking - Consider extracting the office construction logic to a separate method
- Add error logging for edge cases
public static function mapOfficesWithScope(ProviderList $providerList): OfficeList
{
$offices = [];
$scopes = ZmsApiFacadeService::getScopes();
- if (is_array($scopes)) {
+ if (!$scopes instanceof ThinnedScopeList) {
+ error_log('Invalid scopes returned from ZmsApiFacadeService::getScopes()');
return new OfficeList();
}
Committable suggestion skipped: line range outside the PR's diff.
try { | ||
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList(); | ||
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList(); | ||
$offices = []; | ||
|
||
$scopeMap = []; | ||
foreach ($scopeList as $scope) { | ||
if ($scope->getProvider()) { | ||
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope; | ||
} | ||
} | ||
|
||
foreach ($providerList as $provider) { | ||
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null; | ||
|
||
$offices[] = new Office( | ||
id: (int) $provider->id, | ||
name: $provider->displayName ?? $provider->name, | ||
address: $provider->data['address'] ?? null, | ||
geo: $provider->data['geo'] ?? null, | ||
scope: $matchingScope ? new ThinnedScope( | ||
id: (int) $matchingScope->id, | ||
provider: MapperService::providerToThinnedProvider($provider), | ||
shortName: $matchingScope->getShortName(), | ||
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(), | ||
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(), | ||
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(), | ||
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(), | ||
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(), | ||
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(), | ||
displayInfo: $matchingScope->getDisplayInfo() | ||
) : null | ||
); | ||
} | ||
|
||
return new OfficeList($offices); | ||
} catch (\RuntimeException $e) { | ||
return ExceptionService::officesNotFound(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add explicit null checks for API responses.
The null coalescing operator provides a default empty collection, but we should handle API failures explicitly.
Apply this pattern:
try {
- $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
- $scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
+ $providerList = ZmsApiClientService::getOffices();
+ if ($providerList === null) {
+ \App::$log->error('Failed to retrieve offices from API');
+ return ExceptionService::officesNotFound();
+ }
+
+ $scopeList = ZmsApiClientService::getScopes();
+ if ($scopeList === null) {
+ \App::$log->error('Failed to retrieve scopes from API');
+ return ExceptionService::scopesNotFound();
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
try { | |
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList(); | |
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList(); | |
$offices = []; | |
$scopeMap = []; | |
foreach ($scopeList as $scope) { | |
if ($scope->getProvider()) { | |
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope; | |
} | |
} | |
foreach ($providerList as $provider) { | |
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null; | |
$offices[] = new Office( | |
id: (int) $provider->id, | |
name: $provider->displayName ?? $provider->name, | |
address: $provider->data['address'] ?? null, | |
geo: $provider->data['geo'] ?? null, | |
scope: $matchingScope ? new ThinnedScope( | |
id: (int) $matchingScope->id, | |
provider: MapperService::providerToThinnedProvider($provider), | |
shortName: $matchingScope->getShortName(), | |
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(), | |
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(), | |
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(), | |
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(), | |
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(), | |
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(), | |
displayInfo: $matchingScope->getDisplayInfo() | |
) : null | |
); | |
} | |
return new OfficeList($offices); | |
} catch (\RuntimeException $e) { | |
return ExceptionService::officesNotFound(); | |
} | |
try { | |
$providerList = ZmsApiClientService::getOffices(); | |
if ($providerList === null) { | |
\App::$log->error('Failed to retrieve offices from API'); | |
return ExceptionService::officesNotFound(); | |
} | |
$scopeList = ZmsApiClientService::getScopes(); | |
if ($scopeList === null) { | |
\App::$log->error('Failed to retrieve scopes from API'); | |
return ExceptionService::scopesNotFound(); | |
} | |
$offices = []; | |
$scopeMap = []; | |
foreach ($scopeList as $scope) { | |
if ($scope->getProvider()) { | |
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope; | |
} | |
} | |
foreach ($providerList as $provider) { | |
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null; | |
$offices[] = new Office( | |
id: (int) $provider->id, | |
name: $provider->displayName ?? $provider->name, | |
address: $provider->data['address'] ?? null, | |
geo: $provider->data['geo'] ?? null, | |
scope: $matchingScope ? new ThinnedScope( | |
id: (int) $matchingScope->id, | |
provider: MapperService::providerToThinnedProvider($provider), | |
shortName: $matchingScope->getShortName(), | |
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(), | |
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(), | |
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(), | |
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(), | |
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(), | |
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(), | |
displayInfo: $matchingScope->getDisplayInfo() | |
) : null | |
); | |
} | |
return new OfficeList($offices); | |
} catch (\RuntimeException $e) { | |
return ExceptionService::officesNotFound(); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai why when it already gets caught by the large try catch block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ThomasAFink The main idea behind these explicit null checks is to handle scenarios where the API might return null
without actually throwing an exception. The existing try
/catch
block will catch runtime exceptions, but if the underlying service just fails quietly and returns null
(instead of throwing), the code in catch
will never be triggered. By adding explicit null checks, we ensure that if we receive an unexpected null
result—rather than a thrown exception—we can gracefully handle that condition (log an error, return a default, or generate a specific error response).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai but why would ZmsApiClientService return null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ThomasAFink If you’ve verified in your codebase that ZmsApiClientService
never returns null
—for example, by consistently throwing exceptions on error conditions—then these extra null
checks may indeed be redundant. The suggestion to check for null
is often a safeguard against edge cases where a downstream call might fail quietly and return null
instead of throwing. If you can confirm that such a scenario can’t happen, relying on the try/catch
alone is sufficient.
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (7)
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (3)
177-183
: Track TODO comments in the issue system.There are several TODO comments for planned methods:
- getCombinableServicesByIds
- getOfficeByIds
- getOfficesThatProvideService
- preconfirmAppointment
- confirmAppointment
- cancelAppointment
Would you like me to help create GitHub issues to track these planned implementations?
Also applies to: 247-252, 451-456, 778-799
547-578
: Optimize array operations for better performance.The current implementation uses
array_merge
in a loop which can be inefficient for large datasets. Consider pre-allocating the array.Apply this diff:
- $requests = []; + $totalSlots = array_sum($serviceCounts); + $requests = array_pad([], $totalSlots, []); + $currentIndex = 0; + foreach ($serviceIds as $index => $serviceId) { $service = [ 'id' => $serviceId, 'source' => \App::$source_name, 'slotCount' => $serviceCounts[$index] ]; - $requests = array_merge($requests, array_fill(0, $service['slotCount'], $service)); + for ($i = 0; $i < $service['slotCount']; $i++) { + $requests[$currentIndex++] = $service; + } }
736-747
: Add logging for better error tracking.Add logging statements before returning error responses to help with debugging and monitoring.
Apply this pattern to all catch blocks:
} catch (\RuntimeException $e) { + \App::$log->error('Error in getThinnedProcessById: ' . $e->getMessage()); if (strpos($e->getMessage(), 'appointmentNotFound') !== false) { return ExceptionService::appointmentNotFound(); } if (strpos($e->getMessage(), 'authKeyMismatch') !== false) { return ExceptionService::authKeyMismatch(); } if (strpos($e->getMessage(), 'officesNotFound') !== false) { return ExceptionService::officesNotFound(); } return ExceptionService::internalError(); }
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (3)
30-80
: Reduce code duplication in data retrieval methods.The four data retrieval methods share similar patterns and could benefit from a more DRY approach.
Consider extracting the common pattern into a generic method:
+ private static function getEntityList<T>(string $getter, string $entityClass): T + { + try { + $sources = self::fetchSourceData(); + $result = $sources->$getter(); + + if (!$result instanceof $entityClass) { + return new $entityClass(); + } + + return $result; + } catch (\Exception $e) { + ExceptionService::handleException($e, __FUNCTION__); + } + } public static function getOffices(): ProviderList { - try { - $sources = self::fetchSourceData(); - $providerList = $sources->getProviderList() ?? new ProviderList(); - return $providerList; - } catch (\Exception $e) { - ExceptionService::handleException($e, __FUNCTION__); - } + return self::getEntityList('getProviderList', ProviderList::class); }
82-119
: Extract common calendar initialization logic.Both
getFreeDays
andgetFreeTimeslots
share calendar initialization code.Consider extracting the common code:
+ private static function initializeCalendar(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar + { + $calendar = new Calendar(); + $calendar->firstDay = $firstDay; + $calendar->lastDay = $lastDay; + $calendar->providers = $providers; + $calendar->requests = $requests; + return $calendar; + } public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar { try { - $calendar = new Calendar(); - $calendar->firstDay = $firstDay; - $calendar->lastDay = $lastDay; - $calendar->providers = $providers; - $calendar->requests = $requests; + $calendar = self::initializeCalendar($providers, $requests, $firstDay, $lastDay);
245-253
: Improve error handling in getScopesByProviderId.The method should have more specific error handling and input validation.
Apply this improvement:
public static function getScopesByProviderId(string $source, string|int $providerId): ScopeList { + if (empty($source) || empty($providerId)) { + throw new \InvalidArgumentException('Source and provider ID are required'); + } try { $scopeList = self::getScopes() ?? new ScopeList(); return $scopeList->withProviderID($source, (string) $providerId); } catch (\Exception $e) { - ExceptionService::handleException($e, __FUNCTION__); + throw new \RuntimeException( + "Failed to get scopes for provider {$providerId}: " . $e->getMessage(), + 0, + $e + ); } }zmscitizenapi/src/Zmscitizenapi/Models/Office.php (1)
49-54
: Centralize error messages to support multi-lingual usage
The error message here is hard-coded in English. If you plan to support multiple languages or a centralized error messaging system, consider retrieving this string from a localization or constants class.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
zmscitizenapi/src/Zmscitizenapi/Models/Office.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php
(1 hunks)zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php
🔇 Additional comments (9)
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (2)
1-29
: LGTM! Well-organized file structure.
The file has proper strict typing declaration and well-organized imports for all necessary dependencies.
625-662
: LGTM! Efficient implementation of slot processing.
The method uses array_reduce for efficient processing of free slots and maintains a clean structure with proper error handling.
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (2)
1-17
: LGTM! Well-structured file with proper imports.
The file follows PHP best practices with strict types, proper namespacing, and all necessary imports.
231-243
: 🛠️ Refactor suggestion
Add input validation to getProcessById.
The method should validate input parameters before making the API call.
Apply this improvement:
public static function getProcessById(int $processId, string $authKey): Process
{
+ if ($processId <= 0 || empty($authKey)) {
+ throw new \InvalidArgumentException('Invalid process ID or auth key');
+ }
try {
$resolveReferences = 2;
$process = \App::$http->readGetResult("/process/{$processId}/{$authKey}/", [
'resolveReferences' => $resolveReferences
])->getEntity();
+
+ if (!$process instanceof Process) {
+ throw new \UnexpectedValueException('Invalid response from API');
+ }
return $process;
Likely invalid or redundant comment.
zmscitizenapi/src/Zmscitizenapi/Models/Office.php (5)
12-12
: Schema path should match the established convention
This issue was previously flagged in an older comment. Currently, the $schema
property points to 'citizenapi/office.json'
, which may cause inconsistencies if your other models reference 'zmsentities/schema/citizenapi/...'
.
- public static $schema = 'citizenapi/office.json';
+ public static $schema = 'zmsentities/schema/citizenapi/office.json';
14-27
: Typed properties look clean
Your usage of typed properties (e.g. public int $id
, public string $name
) and nullable arrays for address
and geo
help keep the model clear. Consider introducing dedicated classes for address
and geo
if they become more complex in the future to enhance maintainability.
38-47
: Validate the data early in the constructor
Calling $this->ensureValid()
directly after all assignments is a good practice to catch invalid data as soon as possible. This approach helps ensure every instantiated object is valid right from initialization.
61-70
: Clean and clear serialization
The toArray()
method consistently converts your typed properties to an array, with graceful handling of a nullable $scope
. This design is straightforward and maintainable.
72-75
: Consistent JSON serialization
Deferring to toArray()
for JSON serialization is a best practice that ensures consistency. This implementation aligns with the JsonSerializable
interface expectations.
public static function getOffices(): OfficeList|array | ||
{ | ||
try { | ||
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList(); | ||
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList(); | ||
$offices = []; | ||
|
||
$scopeMap = []; | ||
foreach ($scopeList as $scope) { | ||
if ($scope->getProvider()) { | ||
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope; | ||
} | ||
} | ||
|
||
foreach ($providerList as $provider) { | ||
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null; | ||
|
||
$offices[] = new Office( | ||
id: (int) $provider->id, | ||
name: $provider->displayName ?? $provider->name, | ||
address: $provider->data['address'] ?? null, | ||
geo: $provider->data['geo'] ?? null, | ||
scope: $matchingScope ? new ThinnedScope( | ||
id: (int) $matchingScope->id, | ||
provider: MapperService::providerToThinnedProvider($provider), | ||
shortName: $matchingScope->getShortName(), | ||
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(), | ||
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(), | ||
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(), | ||
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(), | ||
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(), | ||
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(), | ||
displayInfo: $matchingScope->getDisplayInfo() | ||
) : null | ||
); | ||
} | ||
|
||
return new OfficeList($offices); | ||
} catch (\RuntimeException $e) { | ||
if (strpos($e->getMessage(), 'officesNotFound') !== false) { | ||
return ExceptionService::officesNotFound(); | ||
} | ||
if (strpos($e->getMessage(), 'scopeNotFound') !== false) { | ||
return ExceptionService::scopeNotFound(); | ||
} | ||
return ExceptionService::internalError(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enhance error handling and reduce duplication.
Two suggestions for improvement:
- Add logging before returning error responses
- Use MapperService::scopeToThinnedScope instead of inline ThinnedScope creation
Apply this diff:
} catch (\RuntimeException $e) {
+ \App::$log->error('Error in getOffices: ' . $e->getMessage());
if (strpos($e->getMessage(), 'officesNotFound') !== false) {
return ExceptionService::officesNotFound();
}
if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
return ExceptionService::scopeNotFound();
}
return ExceptionService::internalError();
}
And replace the ThinnedScope creation with:
- scope: $matchingScope ? new ThinnedScope(
- id: (int) $matchingScope->id,
- provider: MapperService::providerToThinnedProvider($provider),
- shortName: $matchingScope->getShortName(),
- telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
- telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
- customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
- customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
- customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
- captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
- displayInfo: $matchingScope->getDisplayInfo()
- ) : null
+ scope: $matchingScope ? MapperService::scopeToThinnedScope($matchingScope) : null
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public static function getOffices(): OfficeList|array | |
{ | |
try { | |
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList(); | |
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList(); | |
$offices = []; | |
$scopeMap = []; | |
foreach ($scopeList as $scope) { | |
if ($scope->getProvider()) { | |
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope; | |
} | |
} | |
foreach ($providerList as $provider) { | |
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null; | |
$offices[] = new Office( | |
id: (int) $provider->id, | |
name: $provider->displayName ?? $provider->name, | |
address: $provider->data['address'] ?? null, | |
geo: $provider->data['geo'] ?? null, | |
scope: $matchingScope ? new ThinnedScope( | |
id: (int) $matchingScope->id, | |
provider: MapperService::providerToThinnedProvider($provider), | |
shortName: $matchingScope->getShortName(), | |
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(), | |
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(), | |
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(), | |
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(), | |
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(), | |
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(), | |
displayInfo: $matchingScope->getDisplayInfo() | |
) : null | |
); | |
} | |
return new OfficeList($offices); | |
} catch (\RuntimeException $e) { | |
if (strpos($e->getMessage(), 'officesNotFound') !== false) { | |
return ExceptionService::officesNotFound(); | |
} | |
if (strpos($e->getMessage(), 'scopeNotFound') !== false) { | |
return ExceptionService::scopeNotFound(); | |
} | |
return ExceptionService::internalError(); | |
} | |
} | |
public static function getOffices(): OfficeList|array | |
{ | |
try { | |
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList(); | |
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList(); | |
$offices = []; | |
$scopeMap = []; | |
foreach ($scopeList as $scope) { | |
if ($scope->getProvider()) { | |
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope; | |
} | |
} | |
foreach ($providerList as $provider) { | |
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null; | |
$offices[] = new Office( | |
id: (int) $provider->id, | |
name: $provider->displayName ?? $provider->name, | |
address: $provider->data['address'] ?? null, | |
geo: $provider->data['geo'] ?? null, | |
scope: $matchingScope ? MapperService::scopeToThinnedScope($matchingScope) : null | |
); | |
} | |
return new OfficeList($offices); | |
} catch (\RuntimeException $e) { | |
\App::$log->error('Error in getOffices: ' . $e->getMessage()); | |
if (strpos($e->getMessage(), 'officesNotFound') !== false) { | |
return ExceptionService::officesNotFound(); | |
} | |
if (strpos($e->getMessage(), 'scopeNotFound') !== false) { | |
return ExceptionService::scopeNotFound(); | |
} | |
return ExceptionService::internalError(); | |
} | |
} |
public static function submitClientData(Process $process): Process | ||
{ | ||
$url = "/process/{$process->id}/{$process->authKey}/"; | ||
|
||
try { | ||
$result = \App::$http->readPostResult($url, $process); | ||
return $result->getEntity(); | ||
|
||
} catch (\Exception $e) { | ||
ExceptionService::handleException($e, __FUNCTION__); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add response validation to submitClientData.
The method should validate the API response before returning.
Apply this improvement:
public static function submitClientData(Process $process): Process
{
$url = "/process/{$process->id}/{$process->authKey}/";
try {
$result = \App::$http->readPostResult($url, $process);
+ if (!$result || !method_exists($result, 'getEntity')) {
+ throw new \UnexpectedValueException('Invalid response from API');
+ }
return $result->getEntity();
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public static function submitClientData(Process $process): Process | |
{ | |
$url = "/process/{$process->id}/{$process->authKey}/"; | |
try { | |
$result = \App::$http->readPostResult($url, $process); | |
return $result->getEntity(); | |
} catch (\Exception $e) { | |
ExceptionService::handleException($e, __FUNCTION__); | |
} | |
} | |
public static function submitClientData(Process $process): Process | |
{ | |
$url = "/process/{$process->id}/{$process->authKey}/"; | |
try { | |
$result = \App::$http->readPostResult($url, $process); | |
if (!$result || !method_exists($result, 'getEntity')) { | |
throw new \UnexpectedValueException('Invalid response from API'); | |
} | |
return $result->getEntity(); | |
} catch (\Exception $e) { | |
ExceptionService::handleException($e, __FUNCTION__); | |
} | |
} |
public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar | ||
{ | ||
try { | ||
$calendar = new Calendar(); | ||
$calendar->firstDay = $firstDay; | ||
$calendar->lastDay = $lastDay; | ||
$calendar->providers = $providers; | ||
$calendar->requests = $requests; | ||
|
||
$result = \App::$http->readPostResult('/calendar/', $calendar); | ||
|
||
return $result->getEntity() ?? new Calendar(); | ||
|
||
} catch (\Exception $e) { | ||
ExceptionService::handleException($e, __FUNCTION__); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add response validation to getFreeDays method.
For consistency with getFreeTimeslots
, add response validation to ensure API response is valid.
Apply this improvement:
public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
{
try {
$calendar = new Calendar();
$calendar->firstDay = $firstDay;
$calendar->lastDay = $lastDay;
$calendar->providers = $providers;
$calendar->requests = $requests;
$result = \App::$http->readPostResult('/calendar/', $calendar);
+
+ if (!$result || !method_exists($result, 'getEntity')) {
+ throw new \UnexpectedValueException('Invalid response from API');
+ }
return $result->getEntity() ?? new Calendar();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar | |
{ | |
try { | |
$calendar = new Calendar(); | |
$calendar->firstDay = $firstDay; | |
$calendar->lastDay = $lastDay; | |
$calendar->providers = $providers; | |
$calendar->requests = $requests; | |
$result = \App::$http->readPostResult('/calendar/', $calendar); | |
return $result->getEntity() ?? new Calendar(); | |
} catch (\Exception $e) { | |
ExceptionService::handleException($e, __FUNCTION__); | |
} | |
} | |
public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar | |
{ | |
try { | |
$calendar = new Calendar(); | |
$calendar->firstDay = $firstDay; | |
$calendar->lastDay = $lastDay; | |
$calendar->providers = $providers; | |
$calendar->requests = $requests; | |
$result = \App::$http->readPostResult('/calendar/', $calendar); | |
if (!$result || !method_exists($result, 'getEntity')) { | |
throw new \UnexpectedValueException('Invalid response from API'); | |
} | |
return $result->getEntity() ?? new Calendar(); | |
} catch (\Exception $e) { | |
ExceptionService::handleException($e, __FUNCTION__); | |
} | |
} |
public function preconfirmProcess(Process $process): Process | ||
{ | ||
try { | ||
$url = '/process/status/preconfirmed/'; | ||
return \App::$http->readPostResult($url, $process)->getEntity(); | ||
} catch (\Exception $e) { | ||
ExceptionService::handleException($e, __FUNCTION__); | ||
} | ||
} | ||
|
||
public function confirmProcess(Process $process): Process | ||
{ | ||
try { | ||
$url = '/process/status/confirmed/'; | ||
return \App::$http->readPostResult($url, $process)->getEntity(); | ||
} catch (\Exception $e) { | ||
ExceptionService::handleException($e, __FUNCTION__); | ||
} | ||
} | ||
|
||
public function cancelAppointment(Process $process): Process | ||
{ | ||
try { | ||
$url = "/process/{$process->id}/{$process->authKey}/"; | ||
return \App::$http->readDeleteResult($url, $process)->getEntity(); | ||
} catch (\Exception $e) { | ||
ExceptionService::handleException($e, __FUNCTION__); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add validation and reduce duplication in process status methods.
The process status methods need input validation, response validation, and could benefit from shared error handling.
Apply these improvements:
+ private function validateProcess(Process $process): void
+ {
+ if (!$process->id || !$process->authKey) {
+ throw new \InvalidArgumentException('Process must have valid ID and auth key');
+ }
+ }
+
+ private function handleProcessResponse($result): Process
+ {
+ if (!$result || !method_exists($result, 'getEntity')) {
+ throw new \UnexpectedValueException('Invalid response from API');
+ }
+ return $result->getEntity();
+ }
public function preconfirmProcess(Process $process): Process
{
try {
+ $this->validateProcess($process);
$url = '/process/status/preconfirmed/';
- return \App::$http->readPostResult($url, $process)->getEntity();
+ $result = \App::$http->readPostResult($url, $process);
+ return $this->handleProcessResponse($result);
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
Committable suggestion skipped: line range outside the PR's diff.
public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process | ||
{ | ||
try { | ||
$requests = []; | ||
|
||
foreach ($serviceIds as $index => $serviceId) { | ||
$count = intval($serviceCounts[$index]); | ||
for ($i = 0; $i < $count; $i++) { | ||
$requests[] = [ | ||
'id' => $serviceId, | ||
'source' => \App::$source_name | ||
]; | ||
} | ||
} | ||
|
||
$processEntity = new Process(); | ||
$processEntity->appointments = $appointmentProcess->appointments ?? []; | ||
$processEntity->authKey = $appointmentProcess->authKey ?? null; | ||
$processEntity->clients = $appointmentProcess->clients ?? []; | ||
$processEntity->scope = $appointmentProcess->scope ?? null; | ||
$processEntity->requests = $requests; | ||
$processEntity->lastChange = $appointmentProcess->lastChange ?? time(); | ||
$processEntity->createIP = ClientIpHelper::getClientIp(); | ||
$processEntity->createTimestamp = time(); | ||
|
||
if (isset($appointmentProcess->queue)) { | ||
$processEntity->queue = $appointmentProcess->queue; | ||
} | ||
|
||
$result = \App::$http->readPostResult('/process/status/reserved/', $processEntity); | ||
|
||
return $result->getEntity(); | ||
} catch (\Exception $e) { | ||
ExceptionService::handleException($e, __FUNCTION__); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add input validation for service arrays.
The reserveTimeslot
method should validate that serviceIds
and serviceCounts
arrays have matching lengths and contain valid data.
Apply this improvement:
public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process
{
try {
+ if (empty($serviceIds)) {
+ throw new \InvalidArgumentException('At least one service must be specified');
+ }
+ if (count($serviceIds) !== count($serviceCounts)) {
+ throw new \InvalidArgumentException('Service IDs and counts arrays must have matching lengths');
+ }
$requests = [];
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process | |
{ | |
try { | |
$requests = []; | |
foreach ($serviceIds as $index => $serviceId) { | |
$count = intval($serviceCounts[$index]); | |
for ($i = 0; $i < $count; $i++) { | |
$requests[] = [ | |
'id' => $serviceId, | |
'source' => \App::$source_name | |
]; | |
} | |
} | |
$processEntity = new Process(); | |
$processEntity->appointments = $appointmentProcess->appointments ?? []; | |
$processEntity->authKey = $appointmentProcess->authKey ?? null; | |
$processEntity->clients = $appointmentProcess->clients ?? []; | |
$processEntity->scope = $appointmentProcess->scope ?? null; | |
$processEntity->requests = $requests; | |
$processEntity->lastChange = $appointmentProcess->lastChange ?? time(); | |
$processEntity->createIP = ClientIpHelper::getClientIp(); | |
$processEntity->createTimestamp = time(); | |
if (isset($appointmentProcess->queue)) { | |
$processEntity->queue = $appointmentProcess->queue; | |
} | |
$result = \App::$http->readPostResult('/process/status/reserved/', $processEntity); | |
return $result->getEntity(); | |
} catch (\Exception $e) { | |
ExceptionService::handleException($e, __FUNCTION__); | |
} | |
} | |
public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process | |
{ | |
try { | |
if (empty($serviceIds)) { | |
throw new \InvalidArgumentException('At least one service must be specified'); | |
} | |
if (count($serviceIds) !== count($serviceCounts)) { | |
throw new \InvalidArgumentException('Service IDs and counts arrays must have matching lengths'); | |
} | |
$requests = []; | |
foreach ($serviceIds as $index => $serviceId) { | |
$count = intval($serviceCounts[$index]); | |
for ($i = 0; $i < $count; $i++) { | |
$requests[] = [ | |
'id' => $serviceId, | |
'source' => \App::$source_name | |
]; | |
} | |
} | |
$processEntity = new Process(); | |
$processEntity->appointments = $appointmentProcess->appointments ?? []; | |
$processEntity->authKey = $appointmentProcess->authKey ?? null; | |
$processEntity->clients = $appointmentProcess->clients ?? []; | |
$processEntity->scope = $appointmentProcess->scope ?? null; | |
$processEntity->requests = $requests; | |
$processEntity->lastChange = $appointmentProcess->lastChange ?? time(); | |
$processEntity->createIP = ClientIpHelper::getClientIp(); | |
$processEntity->createTimestamp = time(); | |
if (isset($appointmentProcess->queue)) { | |
$processEntity->queue = $appointmentProcess->queue; | |
} | |
$result = \App::$http->readPostResult('/process/status/reserved/', $processEntity); | |
return $result->getEntity(); | |
} catch (\Exception $e) { | |
ExceptionService::handleException($e, __FUNCTION__); | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai but validatePostAppointmentReserve already catches that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ThomasAFink, understood. If validatePostAppointmentReserve
already ensures that arrays have valid lengths and data, then adding the same checks in reserveTimeslot
could be redundant. In that case, you can safely skip the additional validation in reserveTimeslot
if the existing checks fully cover the logic you need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai can we set the types in the array with PHP? An array of ints? array $serviceIds
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ThomasAFink, at this time PHP does not offer “true” generics or fully enforced typed arrays in function parameters (e.g., array<int> $serviceIds
). You can declare a regular array type (e.g., array $serviceIds
), but PHP will not enforce the inner element types beyond verifying that it’s an array. That said, there are a few workarounds:
-
Typed Properties (PHP 7.4+):
class Example { private array $serviceIds; // Still accepts any array; no guaranteed scalar type checks on elements public function __construct(array $serviceIds) { $this->serviceIds = array_map('intval', $serviceIds); // Convert/validate values here } }
While you can require
$serviceIds
to be an array, PHP does not check that each element is an integer unless you manually enforce it (e.g., usingarray_map
or throwing exceptions for invalid element types). -
Variadic Functions:
If you know you want only integers, you can take advantage of variadic parameters:public function someMethod(int ...$serviceIds): void { // Here PHP ensures each $serviceId is an int }
But this changes the signature from an array to a variable number of int parameters.
-
Manual Checks:
To keep an array signature while filtering or validating each item, you would do:public function reserveTimeslot(array $serviceIds): void { foreach ($serviceIds as $id) { if (!is_int($id)) { throw new \InvalidArgumentException('serviceIds must contain only integers'); } } // ... }
In short, you can hint that you expect an array, but ensuring each element is an integer requires explicit checks (e.g., array_map('intval', $serviceIds)
or throwing exceptions if a mismatch is found). PHP does not yet provide built-in syntax for parametrized arrays (like array<int>
).
Description
Short description or comments
Reference
Issues #XXX
Summary by CodeRabbit
New Features
zmscitizenapi
with functionalities for managing appointments, services, and scopes.zmscitizenapi
module./zmscitizenapi
endpoint.Makefile
for development and deployment tasks.build_swagger.js
script for generating OpenAPI specifications.Bug Fixes
Documentation
Tests
Chores
.gitignore
files to streamline version control.