Skip to content
43 changes: 37 additions & 6 deletions .github/workflows/client-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ name: Update phplist-api-client OpenAPI
on:
push:
branches:
- main
- '**'
pull_request:

jobs:
generate-openapi:
runs-on: ubuntu-22.04
outputs:
source_branch: ${{ steps.branch.outputs.source_branch }}
steps:
- name: Determine source branch
id: branch
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "source_branch=${{ github.head_ref }}" >> "$GITHUB_OUTPUT"
else
echo "source_branch=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
fi

- name: Checkout Source Repository
uses: actions/checkout@v3

Expand Down Expand Up @@ -42,6 +53,8 @@ jobs:
update-phplist-api-client:
runs-on: ubuntu-22.04
needs: generate-openapi
env:
TARGET_BRANCH: ${{ needs.generate-openapi.outputs.source_branch }}
steps:
- name: Checkout phplist-api-client Repository
uses: actions/checkout@v3
Expand All @@ -50,6 +63,17 @@ jobs:
token: ${{ secrets.PUSH_API_CLIENT }}
fetch-depth: 0

- name: Prepare target branch
run: |
git fetch origin

if git ls-remote --exit-code --heads origin "$TARGET_BRANCH" >/dev/null 2>&1; then
git checkout "$TARGET_BRANCH"
git pull --rebase origin "$TARGET_BRANCH"
else
git checkout -b "$TARGET_BRANCH"
fi

- name: Download Generated OpenAPI JSON
uses: actions/download-artifact@v4
with:
Expand All @@ -63,24 +87,31 @@ jobs:
if [ -f openapi.json ]; then
diff openapi.json new-openapi/latest-restapi.json > openapi-diff.txt || true
if [ -s openapi-diff.txt ]; then
echo "diff=true" >> $GITHUB_OUTPUT
echo "diff=true" >> "$GITHUB_OUTPUT"
else
echo "diff=false" >> $GITHUB_OUTPUT
echo "diff=false" >> "$GITHUB_OUTPUT"
fi
else
echo "No previous openapi.json, will add."
echo "diff=true" >> $GITHUB_OUTPUT
echo "diff=true" >> "$GITHUB_OUTPUT"
fi

- name: Update and Commit OpenAPI File
if: steps.diff.outputs.diff == 'true'
run: |
set -euo pipefail
cp new-openapi/latest-restapi.json openapi.json
git config user.name "github-actions"
git config user.email "github-actions@phplist-api-client.workflow"
git add openapi.json
git commit -m "Update openapi.json from REST API workflow `date`"
git push
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update openapi.json from REST API workflow $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
git fetch origin "$TARGET_BRANCH"
git rebase "origin/$TARGET_BRANCH"
git push origin HEAD:"$TARGET_BRANCH"

- name: Skip Commit if No Changes
if: steps.diff.outputs.diff == 'false'
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
},
"require": {
"php": "^8.1",
"phplist/core": "dev-main",
"phplist/core": "dev-dev",
"friendsofsymfony/rest-bundle": "*",
"symfony/test-pack": "^1.0",
"symfony/process": "^6.4",
Expand Down
6 changes: 3 additions & 3 deletions config/services/managers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Identity\Service\SessionManager:
PhpList\Core\Domain\Identity\Service\Manager\SessionManager:
autowire: true
autoconfigure: true

Expand Down Expand Up @@ -36,7 +36,7 @@ services:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Identity\Service\AdministratorManager:
PhpList\Core\Domain\Identity\Service\Manager\AdministratorManager:
autowire: true
autoconfigure: true

Expand All @@ -56,6 +56,6 @@ services:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Identity\Service\PasswordManager:
PhpList\Core\Domain\Identity\Service\Manager\PasswordManager:
autowire: true
autoconfigure: true
7 changes: 6 additions & 1 deletion config/services/messenger_handlers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
services:
PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler:
PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\CampaignProcessorMessageHandler:
autowire: true
autoconfigure: true
public: false

PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\TestCampaignProcessorMessageHandler:
autowire: true
autoconfigure: true
public: false
6 changes: 6 additions & 0 deletions config/services/normalizers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,11 @@ services:
$classMetadataFactory: '@?serializer.mapping.class_metadata_factory'
$nameConverter: '@Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'

phplist.request_serializer:
class: Symfony\Component\Serializer\Serializer
arguments:
$normalizers:
- '@Symfony\Component\Serializer\Normalizer\ObjectNormalizer'

PhpList\RestBundle\:
resource: '../../src/*/Serializer/*'
3 changes: 1 addition & 2 deletions config/services/validators.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
services:
PhpList\RestBundle\Common\Validator\RequestValidator:
arguments:
$serializer: '@Symfony\Component\Serializer\Normalizer\ObjectNormalizer'
$serializer: '@phplist.request_serializer'
$validator: '@validator'

PhpList\RestBundle\Identity\Validator\Constraint\UniqueEmailValidator:
Expand Down Expand Up @@ -50,4 +50,3 @@ services:
autowire: true
autoconfigure: true
tags: [ 'validator.constraint_validator' ]

63 changes: 58 additions & 5 deletions src/Common/EventListener/ExceptionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Exception\ValidatorException;

class ExceptionListener
Expand All @@ -24,7 +26,6 @@ class ExceptionListener
SubscriptionCreationException::class => null,
AttributeDefinitionCreationException::class => null,
AdminAttributeCreationException::class => null,
ValidatorException::class => 400,
AccessDeniedException::class => 403,
AccessDeniedHttpException::class => 403,
AttachmentFileNotFoundException::class => 404,
Expand All @@ -36,33 +37,85 @@ public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();

if ($exception instanceof ValidationFailedException
|| $exception instanceof ValidatorException
|| $exception instanceof UnprocessableEntityHttpException
) {
$event->setResponse(
new JsonResponse([
'message' => 'Validation failed',
'errors' => $this->parseFlatValidationMessage($exception->getMessage()),
], 422)
);

return;
}

foreach (self::EXCEPTION_STATUS_MAP as $class => $statusCode) {
if ($exception instanceof $class) {
$status = $statusCode ?? $exception->getStatusCode();
$status = $statusCode ?? (
method_exists($exception, 'getStatusCode')
? $exception->getStatusCode()
: 400
);

$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
'message' => $exception->getMessage(),
], $status)
);

return;
}
}

if ($exception instanceof HttpExceptionInterface) {
$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
'message' => $exception->getMessage(),
], $exception->getStatusCode())
);

return;
}

if ($exception instanceof Exception) {
$event->setResponse(
new JsonResponse([
'message' => $exception->getMessage()
'message' => $exception->getMessage(),
], 500)
);
}
}

/**
* @return array<string, array<int, string>>
*/
private function parseFlatValidationMessage(string $message): array
{
$errors = [];
$lines = preg_split('/\r\n|\r|\n/', $message) ?: [];

foreach ($lines as $line) {
$line = trim($line);

if ($line === '') {
continue;
}

$parts = explode(':', $line, 2);

if (count($parts) !== 2) {
$errors['_global'][] = $line;
continue;
}

$field = trim($parts[0]);
$errorMessage = trim($parts[1]);

$errors[$field][] = $errorMessage;
}

return $errors;
}
}
36 changes: 34 additions & 2 deletions src/Common/SwaggerSchemasResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,22 @@

use OpenApi\Attributes as OA;

#[OA\Schema(
schema: 'ErrorDetails',
type: 'object',
example: [
'format.formatOptions[0]' => ['The value you selected is not a valid choice.'],
'schedule.repeatUntil' => ['This value is not a valid datetime.'],
'schedule.requeueUntil' => ['This value is not a valid datetime.'],
],
additionalProperties: new OA\AdditionalProperties(
type: 'array',
items: new OA\Items(type: 'string')
)
)]
#[OA\Schema(
schema: 'UnauthorizedResponse',
required: ['message'],
properties: [
new OA\Property(
property: 'message',
Expand All @@ -19,17 +33,23 @@
)]
#[OA\Schema(
schema: 'ValidationErrorResponse',
required: ['message', 'errors'],
properties: [
new OA\Property(
property: 'message',
type: 'string',
example: 'Some fields are invalid'
example: 'Validation failed'
),
new OA\Property(
property: 'errors',
ref: '#/components/schemas/ErrorDetails'
)
],
type: 'object'
)]
#[OA\Schema(
schema: 'BadRequestResponse',
required: ['message'],
properties: [
new OA\Property(
property: 'message',
Expand All @@ -41,6 +61,7 @@
)]
#[OA\Schema(
schema: 'AlreadyExistsResponse',
required: ['message'],
properties: [
new OA\Property(
property: 'message',
Expand All @@ -62,7 +83,18 @@
],
type: 'object'
)]

#[OA\Schema(
schema: 'GenericErrorResponse',
required: ['message'],
properties: [
new OA\Property(
property: 'message',
type: 'string',
example: 'An unexpected error occurred.'
)
],
type: 'object'
)]
#[OA\Schema(
schema: 'CursorPagination',
properties: [
Expand Down
3 changes: 2 additions & 1 deletion src/Identity/Serializer/AdministratorTokenNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PhpList\RestBundle\Identity\Serializer;

use DateTimeInterface;
use PhpList\Core\Domain\Identity\Model\AdministratorToken;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

Expand All @@ -21,7 +22,7 @@ public function normalize($object, string $format = null, array $context = []):
return [
'id' => $object->getId(),
'key' => $object->getKey(),
'expiry_date' => $object->getExpiry()->format('Y-m-d\TH:i:sP'),
'expiry_date' => $object->getExpiry()->format(DateTimeInterface::ATOM),
];
}

Expand Down
Loading
Loading