biig/melodiia
Melodiia is a Symfony toolkit for building JSON:API-compliant REST APIs without magic: Swagger documentation endpoint, CRUD controllers, Symfony Form input, error handling, and standardized API responses. Requires PHP 8.0+.
Requirements (as of 0.12.2)
pagerfanta/doctrine-orm-adapter ^4 arrived in 0.12.2.Installation
composer require swag-industries/melodiia
This auto-generates melodiia.yaml in config/.
First Use Case: Basic CRUD Endpoint
PostType):
// src/Form/PostType.php
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class PostType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('title', TextType::class)
->add('content', TextareaType::class);
}
}
#[Melodiia\Resource]:
// src/Entity/Post.php
use Melodiia\Resource;
#[Resource(formType: PostType::class)]
class Post {}
/api/posts).Test the API
/api/docs for Swagger UI.POST to /api/posts with JSON payload:
{"title": "Hello", "content": "World"}
Note for Laravel developers: Melodiia is a Symfony bundle, not a Laravel package. You cannot
composer requireit into a Laravel app and expectmelodiia.yaml, auto-routed controllers, or Symfony Forms to work under Laravel's container. Day-to-day leverage only applies if you maintain a Symfony service (or a hybrid stack with a Symfony API boundary) alongside your Laravel code. If your stack is Laravel-only, treat this assessment as a reference for the JSON:API/Form-driven pattern, not as droppable code.
// src/Form/UserType.php
class UserType extends AbstractType {
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'csrf_protection' => false, // Disable for APIs
'validation_groups' => ['Default', 'api'],
]);
}
}
validation_groups to separate API-specific rules (e.g., api).// src/EventSubscriber/CustomResponseSubscriber.php
use Melodiia\Event\ResourceEvent;
class CustomResponseSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [
ResourceEvent::PRE_SERIALIZE => 'onPreSerialize',
];
}
public function onPreSerialize(ResourceEvent $event) {
$event->getData()->setAttribute('custom_field', 'value');
}
}
services.yaml:
services:
App\EventSubscriber\CustomResponseSubscriber:
tags: ['melodiia.event_subscriber']
#[Resource]:
#[Resource(
formType: PostType::class,
collectionOperations: ['get', 'post'],
itemOperations: ['get', 'put', 'delete'],
subresources: ['comments'] // Nested route: /api/posts/{id}/comments
)]
class Post {}
CommentType form and annotate Comment similarly.# melodiia.yaml
security:
enabled: true
provider: app_user_provider
roles:
POST: ROLE_USER
PUT: ROLE_ADMIN
melodiia.security.firewall.Melodiia\Test\ApiTestCase:
use Melodiia\Test\ApiTestCase;
class PostTest extends ApiTestCase {
public function testCreatePost() {
$response = $this->postJson('/api/posts', [
'title' => 'Test',
'content' => 'Content',
]);
$this->assertResponseSuccess();
$this->assertJsonApiResponse($response);
}
}
doctrine/orm:^2.pagerfanta/doctrine-orm-adapter:^4 — since pagination goes through the adapter:
composer require doctrine/orm:^3.0 pagerfanta/doctrine-orm-adapter:^4 --with-all-dependencies
Paginator/Pagerfanta adapter wiring before rolling it out. Melodiia's own adapter support does not shield you from ORM's own removals.Doctrine\ORM\Proxy\Proxy or rely on removed EntityManager methods, fix those first — Melodiia resource endpoints will otherwise fail at hydration time rather than at boot.Form Validation Mismatch
Default group but API expects api.validation_groups in formType options (see Implementation Patterns).CSRF Protection
formType options:
$builder->setCsrfProtection(false);
JSON:API Compliance
setAttribute() for top-level data or nest under data.attributes.Route Overrides
melodiia.yaml:
routing:
auto_generate: false
Then define routes manually in config/routes.yaml.Pagination
melodiia.yaml:
pagination:
enabled: true
items_per_page: 20
max_items_per_page: 100
Dependency Constraints (0.12.2)
composer require swag-industries/melodiia:^0.12.2
composer why-not doctrine/orm 3.0
GET /api/posts?page=2) — pagination is where a broken adapter surfaces first.Symfony 8 Compatibility
^0.12.2; run bin/console debug:container melodiia --show-private to confirm the bundle's services register cleanly after the upgrade.Enable Verbose Logging
# melodiia.yaml
debug: true
Logs events to var/log/melodiia.log.
Check Event Dispatching
Melodiia\Event\ResourceEvent listeners to inspect data before serialization.Validate JSON:API Responses
data, errors, meta.links.related or data arrays.Isolate ORM 3 Failures from Melodiia Failures
Custom Serializers
Melodiia\Serializer\SerializerInterface and bind in services.yaml:
services:
App\Serializer\CustomSerializer:
tags: ['melodiia.serializer']
Dynamic Forms
FormBuilderInterface callbacks to conditionally add fields:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$data = $event->getData();
if ($data->isAdmin()) {
$event->getForm()->add('admin_field');
}
});
Webhook Integration
Melodiia\Event\ResourceEvent to trigger webhooks on POST/PUT:
public function onPostCreate(ResourceEvent $event) {
if ($event->getOperation() === 'post') {
$this->webhookClient->send($event->getData());
}
}
GraphQL-like Queries
melodiia.yaml to enable partial responses:
serialization:
include_nulls: false
groups: ['api']
?fields[posts]=title,content.How can I help you explore Laravel packages today?