API Platform: إطار عمل الـ API لـ Symfony
API Platform هو إطار العمل المرجعي لبناء واجهات API حديثة مع Symfony. يولّد تلقائياً API من نوع REST وGraphQL انطلاقاً من كياناتك.
عملياً، تصف مواردك بسمات PHP (attributes): أي العمليات مكشوفة، ومن يحق له الوصول إليها، وأي الحقول قابلة للقراءة أو التعديل. ويتكفل API Platform بالباقي: التوجيه، والتسلسل (serialization)، والتحقق، والترقيم، والمرشِّحات، والتفاوض على المحتوى، وتوثيق OpenAPI. والوقت الموفَّر في هذه الشيفرة المتكررة يمكن تخصيصه لقواعد الأعمال. يغطي هذا المقال إعداد مورد كامل، ثم النقاط التي تصنع الفرق في الإنتاج: مجموعات التسلسل، والأمان، ومنطق الأعمال المخصص، والاختبارات.
التثبيت والإعداد
composer require api
# Automatically creates the API Platform configuration
بفضل Symfony Flex، يثبّت الاسم المستعار api حزمة API Platform ووصفتها: الملف config/packages/api_platform.yaml، والمسارات تحت البادئة /api، وإعداد CORS. بعد تشغيل الخادم، يعرض /api توثيقاً تفاعلياً (Swagger UI) مولَّداً من مواردك. ويتيح الإعداد العام تحديد قيم افتراضية لكل الموارد:
# config/packages/api_platform.yaml
api_platform:
title: 'Catalogue API'
version: '1.0.0'
formats:
jsonld: ['application/ld+json']
json: ['application/json']
defaults:
pagination_items_per_page: 20
pagination_maximum_items_per_page: 100
pagination_client_items_per_page: true
مع pagination_client_items_per_page، يستطيع العميل اختيار حجم الصفحة عبر المعامل itemsPerPage، لكن دون تجاوز الحد الأقصى المحدد أبداً. فبدون هذا الحد، قد يطلب طلب واحد الجدول بأكمله.
إنشاء مورد API
المورد صنف موسوم بـ #[ApiResource]. وغالباً ما يكون كياناً في Doctrine كما هنا، وإن لم يكن ذلك إلزامياً. تصرّح القائمة operations صراحةً بالعمليات المكشوفة: كل ما ليس فيها لا وجود له في الـ API.
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
],
paginationItemsPerPage: 20,
)]
#[ORM\Entity]
class Product
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
private string $name;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
private string $price;
// Getters and setters...
}
هذا الملف وحده يُنتج GET /api/products (مجموعة مرقَّمة) وGET /api/products/{id} وPOST /api/products المحصور بالمديرين. يُخزَّن السعر كـ decimal ويُعالَج كسلسلة نصية في PHP: وهذا مقصود لتجنّب أخطاء تقريب الأعداد العشرية في المبالغ. وتُطبَّق قيود التحقق (#[Assert\NotBlank]) تلقائياً عند الكتابة: البيانات غير الصالحة تُنتج استجابة 422 تفصّل كل مخالفة، حقلاً بحقل.
المرشِّحات والبحث
تضيف المرشِّحات (filters) معاملات استعلام إلى المجموعات دون كتابة سطر SQL واحد:
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial'])]
#[ApiFilter(RangeFilter::class, properties: ['price'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'price'])]
class Product
{
// ...
}
يستطيع العميل حينها استدعاء /api/products?name=clavier&price[lt]=100&order[price]=asc. ويظهر كل مرشِّح أيضاً في توثيق OpenAPI. احتياطان: المرشِّح partial يُترجَم إلى LIKE '%…%' لا يستفيد من فهرس B-tree عادي ويصبح مكلفاً على الجداول الكبيرة؛ ولا تكشف المرشِّحات إلا على الحقول المفيدة فعلاً، فكل مرشِّح مساحة استعلام إضافية. وتقترح الإصدارات الحديثة من API Platform أيضاً مقاربة قائمة على المعاملات (QueryParameter) تُعلَن مباشرة على العملية.
مجموعات التسلسل (Serialization Groups)
افتراضياً، تُكشف كل الخصائص المتاحة للقراءة والكتابة معاً. ونادراً ما يكون هذا المطلوب: فالمعرّف يجب ألا يكون قابلاً للتعديل، وبعض الحقول الداخلية يجب ألا تخرج أبداً. تفصل مجموعات التسلسل بين ما تقرؤه الـ API وما تقبله:
use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
normalizationContext: ['groups' => ['product:read']],
denormalizationContext: ['groups' => ['product:write']],
)]
class Product
{
#[Groups(['product:read'])]
private ?int $id = null;
#[Groups(['product:read', 'product:write'])]
private string $name;
#[Groups(['product:read', 'product:write'])]
private string $price;
}
الخاصية التي لا تنتمي إلى أي مجموعة غير مرئية للـ API. إنها أبسط حماية من الكشف غير المقصود لحقل يُضاف إلى الكيان لاحقاً، مثل سعر الشراء أو ملاحظة داخلية. اعتمد اصطلاح تسمية (resource:read وresource:write) والتزم به.
الأمان لكل عملية
يقبل الخيار security تعبيراً يُقيَّم قبل العملية. وفي العمليات على عنصر واحد، يتيح المتغير object الوصول إلى المورد المعني، ما يسمح بقواعد الملكية:
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Patch;
#[ApiResource(
operations: [
new Patch(security: "is_granted('ROLE_ADMIN') or object.getOwner() == user"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
)]
للقواعد الأغنى، فوِّض القرار إلى Voter في Symfony عبر is_granted('PRODUCT_EDIT', object): يبقى منطق الصلاحيات قابلاً للاختبار وإعادة الاستخدام خارج الـ API.
منطق الأعمال: State Processors
افتراضياً، تُحفَظ عمليات الكتابة مباشرة عبر Doctrine. ولإضافة سلوك (إرسال رسالة، حساب، استدعاء خارجي)، يستخدم API Platform ما يسمى State Providers للقراءة وState Processors للكتابة. والنهج الأكثر شيوعاً هو تزيين الـ processor الخاص بـ Doctrine:
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Product;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/** @implements ProcessorInterface<Product, Product> */
final class ProductProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
private ProcessorInterface $persistProcessor,
private LoggerInterface $logger,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Product
{
$product = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
$this->logger->info('Product saved', ['id' => $product->getId()]);
return $product;
}
}
ثم نفعّله على العملية المعنية: new Post(processor: ProductProcessor::class). والآلية نفسها تسمح بكشف موارد ليست كيانات، مثل DTO تغذّيه API خارجية، عبر provider وprocessor مخصصين. وهذا غالباً أنظف من كشف نموذج قاعدة البيانات مباشرة.
اختبار الـ API
يوفر API Platform الصنف ApiTestCase، وهو صنف اختبار مبني على عميل HTTP في Symfony، مع تأكيدات (assertions) مناسبة لاستجابات JSON:
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
final class ProductApiTest extends ApiTestCase
{
public function testCollectionIsPublic(): void
{
static::createClient()->request('GET', '/api/products');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
}
public function testUnknownProductReturns404(): void
{
static::createClient()->request('GET', '/api/products/999999');
$this->assertResponseStatusCodeSame(404);
}
}
أضف على الأقل اختباراً لكل قاعدة أمان: يجب أن يتلقى المستخدم الذي لا يملك الصلاحيات خطأً، وأن ينجح المدير. فهناك تختبئ أخطر حالات التراجع.
المزايا
- توثيق OpenAPI مولَّد تلقائياً
- دعم JSON-LD وHydra
- ترقيم ومرشِّحات وفرز مدمجة
- تحقق تلقائي عبر قيود Symfony
- دعم أصلي لـ GraphQL
يتطلب دعم GraphQL تثبيت حزمة إضافية، ثم يعيد استخدام الموارد والمجموعات وقواعد الأمان نفسها التي تستخدمها API من نوع REST. ويمكن تصدير مواصفة OpenAPI عبر php bin/console api:openapi:export لتوليد العملاء أو تغذية CI لاختبار العقود.
تستخدم منصات مثل CCM Benchmark حزمة API Platform لكشف خدماتها الداخلية عبر واجهات API موحَّدة المعايير.
مزالق يجب تجنّبها
- كشف الكيان كما هو: دون مجموعات تسلسل، تصبح كل خاصية جديدة عامة. عرِّف المجموعات دائماً، أو استخدم كائنات DTO.
- نسيان N+1: قد تُطلق مجموعةٌ تُسلسِل علاقاتها استعلاماً لكل عنصر. راقب محلّل Symfony وأضف روابط (joins) عند الحاجة، عبر امتداد Doctrine خاص بـ API Platform.
- ترك كل العمليات الافتراضية: دون قائمة
operations، يكشف API Platform أيضاًPUTوPATCHوDELETE. صرّح بوضوح بما تريده. - كتابة الأمان في المتحكمات (controllers): أبقِه في سمات
securityوفي الـ Voters، حيث يكون مرئياً وقابلاً للاختبار.
ليس API Platform الخيار الأفضل لـ API مكوّنة من بضع نقاط نهاية شديدة الخصوصية لا صلة لها بنموذج موارد: فالمتحكم التقليدي في Symfony سيكون أبسط حينها. لكن متى تعلّق الأمر بكشف نموذج أعمال بعمليات CRUD مع الترقيم والمرشِّحات والأمان والتوثيق، فهو من أكثر الطرق إنتاجية لتحقيق ذلك في PHP.