کلاس پیکربندی Dorapi
در این فصل، نحوه ایجاد و تنظیم کلاس Dorapi برای هر مدل را بررسی خواهیم کرد. این کلاس نقش مهمی در تعریف قوانین مجاز برای فیلدها، روابط، فیلترها و مرتبسازیها ایفا میکند.
ایجاد کلاس Dorapi
برای هر مدل، یک کلاس Dorapi ایجاد میشود که قوانین مجاز را تعریف میکند. این کلاس با دستور زیر ساخته میشود:
php artisan dornica:make-dorapi User
<?php
namespace App\Dorapi;
use Dornica\APIKit\Dorapi\Builders\Field;
use Dornica\APIKit\Dorapi\Builders\Relation;
use Dornica\APIKit\Dorapi\Dorapi;
use Dornica\APIKit\Dorapi\DorapiProperty;
use Dornica\APIKit\Dorapi\Enums\FilterOperator;
use Dornica\APIKit\Dorapi\Enums\SortDirection;
class UserDorapi extends Dorapi
{
/**
* Define the default fields for the API response
*
* @return Field
*/
public function fields(): Field
{
return DorapiProperty::field()
->addDefault('id')
->addDefault('created_at')
->addDefault(
column: fn() => UserRole::query()
->select('name')
->whereColumn('users.role_id', 'user_roles.id')
->limit(1),
alias: 'user_role_name'
)
->add(
column: fn() => Comment::query()
->whereColumn('comments.user_id', 'users.id')
->selectRaw('count(*)'),
alias: 'user_comment_count'
);
}
/**
* Define the filterable fields and their allowed operators
*
* @return array
*/
public function filters(): array
{
return [
DorapiProperty::filter()
->make('id')
->allowedOperators([
FilterOperator::EQUAL,
FilterOperator::IN
]),
];
}
/**
* Define the available relations for eager loading
*
* @return Relation
*/
public function relations(): Relation
{
return DorapiProperty::relation();
}
/**
* Define sortable fields
*
* Example usage in query string:
* - Regular sort: sort[0]=id:asc
* - Relation-based sort: sort[0]=relation.field:desc
*
* @return Sort
*/
public function sorts(): Sort
{
return DorapiProperty::sort()
->add('id')
->add('user_comment_count');
}
}
این کلاس شامل تعریفهای زیر است:
- فیلدهای قابل انتخاب
- فیلترهای مجاز
- مرتبسازیها
- روابط قابل populate
است.
فیلدها (Fields)
تعریف فیلدهای مجاز
public function fields(): Field
{
return DorapiProperty::field()
->addDefault('code')
->addDefault('title')
->addDefault('is_active')
->addDefault(
column: fn() => Comment::query()
->whereColumn('comments.user_id', 'users.id')
->selectRaw('count(*)'),
alias: 'user_comment_count'
)
->add('description');
}
رفتار سیستم
- اگر
fieldsارسال نشود، فقط فیلدهای تعریفشده به عنوان پیش فرض (Default) برگردانده میشوند - فیلد
idهمیشه بهصورت خودکار اضافه میشود - درخواست فیلد تعریفنشده → نادیده گرفته میشود (بدون خطا)
- فیلدهای محاسباتی (Closure) نیز دقیقاً مانند سایر فیلدها قابل درخواست هستند
در این مثال:
addDefaultفیلدهایی را تعریف میکند که به صورت پیشفرض در پاسخ API برگردانده میشوندaddفیلدهایی را تعریف میکند که مجاز هستند ولی فقط در صورت درخواست کاربر در خروجی قرار میگیرند- برای فیلدهای محاسباتی (Subquery)، میتوان یک
Closureبه جای نام ستون پاس داد — در این حالت پارامترaliasالزامی است
فیلدهای محاسباتی (Closure Field)
زمانی که مقدار فیلد مستقیماً در جدول وجود ندارد و باید با یک Query محاسبه شود، میتوان یک Closure به عنوان column پاس داد:
->add(
column: fn() => Comment::query()
->whereColumn('comments.user_id', 'users.id')
->selectRaw('count(*)'),
alias: 'user_comment_count'
)
یا با addDefault اگر میخواهید به صورت پیشفرض برگردانده شود:
->addDefault(
column: fn() => Province::query()
->select('name')
->whereColumn('provinces.id', 'cities.province_id')
->limit(1),
alias: 'province_name'
)
نکات مهم:
- پارامتر
aliasنام فیلدی است که در خروجی API نمایش داده میشود — بدونaliasاکسپشن پرتاب میشود Closureباید یک Query Builder برگرداند- DorAPI این Query را به صورت SubQuery در بخش SELECT قرار میدهد
- پس از تعریف، میتوان از
aliasدر بخشsortsنیز برای مرتبسازی استفاده کرد
استفاده در Request
GET /api/products?fields[0]=title&fields[1]=price&fields[2]=user_comment_count
روابط (Populate)
تعریف روابط مجاز
public function relations(): Relation
{
return DorapiProperty::relation()
->addDefault('parent')
->add('createdBy')
->add('updatedBy');
}
قواعد مهم
- اگر رابطهای تعریف نشده باشد، قابل populate نیست
- اگر
populateارسال نشود، فقط موارد تعریفشده به عنوان پیش فرض (Default) برگردانده میشوند - populate پیشفرض lazy نیست، فقط با درخواست کاربر اجرا میشود
- حداکثر آیتم هر رابطه: 10 (قابل تنظیم در config)
مثال
GET /api/products?populate[0]=category
فیلترها (Filters)
تعریف فیلترها
public function filters(): array
{
return [
DorapiProperty::filter()
->make('is_active')
->allowedOperators([
FilterOperator::EQUAL,
FilterOperator::IN,
])
->default(1)
->customQuery(fn($query, $value, $operator) =>
$query->where('is_active', $operator, $value)
)
->labelResolver(fn($value) => is_array($value) ?
array_map(fn($val) => IsActive::from($val)->label(), $value) :
IsActive::from($value)->label()),
];
}
در مثال بالا:
makeنام فیلدی که قرار است فیلتر شود را مشخص میکندallowedOperatorsاپراتورهایی را مشخص میکند که کاربر اجازه دارد در درخواست استفاده کندdefaultمقدار پیشفرض فیلتر را تعیین میکند؛ اگر کاربر فیلتر ارسال نکند این مقدار اعمال میشودcustomQueryامکان تعریف منطق دلخواه برای اعمال فیلتر روی Query را فراهم میکندlabelResolverمقدار فیلتر را به یک برچسب قابل نمایش تبدیل میکند که در meta پاسخ API قرار میگیرد
وقتی از اپراتور IN استفاده میشود، مقدار فیلتر به صورت آرایه ارسال میشود، نه یک مقدار تکی. در نتیجه labelResolver باید بتواند هر دو حالت را مدیریت کند: مقدار تکی و آرایهای.
در حالت مقدار تکی، تبدیل مقدار به برچسب به سادگی انجام میشود:
IsActive::from($value)->label()
اما زمانی که اپراتور IN استفاده شده باشد، مقدار به شکل آرایهای از مقادیر خواهد بود (مثلاً [1,0]). در این حالت باید برای هر مقدار داخل آرایه برچسب متناظر تولید شود. برای این کار از array_map استفاده میکنیم تا تابع تبدیل روی تمام مقادیر اعمال شود.
به همین دلیل در labelResolver ابتدا بررسی میشود که مقدار آرایه است یا نه:
->labelResolver(fn($value) => is_array($value)
? array_map(fn($val) => IsActive::from($val)->label(), $value)
: IsActive::from($value)->label()
)
در این پیادهسازی:
اگر مقدار تکی باشد → مستقیماً به label تبدیل میشود.
اگر مقدار آرایهای باشد (مانند اپراتور IN) → با array_map هر مقدار به label متناظر تبدیل میشود.
به این ترتیب خروجی meta همیشه شامل برچسبهای قابل نمایش برای تمام مقادیر فیلتر خواهد بود.
در مثال بالا که برای labelResolver استفاده شد، منظور از IsActive همین enum زیر است:
enum IsActive: int
{
use EnumTools;
case YES = 1;
case NO = 0;
/**
* @return string
*/
public function label(): string
{
return match ($this) {
self::YES => __('validation.bool.yes'),
self::NO => __('validation.bool.no'),
default => __('validation.enum', ['attribute' => 'is_active']),
};
}
}
اپراتورهای فیلتر
کلاس Enum برای تعیین اپراتورها:
use Dornica\APIKit\Dorapi\Enums\FilterOperator;
این کلاس شامل اپراتورهای زیر است:
case EQUAL = '$eq';
case NOT_EQUAL = '$ne';
case LESS_THAN = '$lt';
case LESS_THAN_EQUAL = '$lte';
case GREATER_THAN = '$gt';
case GREATER_THAN_EQUAL = '$gte';
case LIKE = '$contains';
case IN = '$in';
case NOT_IN = '$notIn';
case NULL = '$null';
case NOT_NULL = '$notNull';
اپراتورهای پشتیبانیشده
| Operator | توضیح |
|---|---|
$eq | برابر |
$ne | نابرابر |
$lt / $lte | کمتر از |
$gt / $gte | بیشتر از |
$in / $notIn | در لیست |
$contains | جستجوی متنی |
$null / $notNull | بررسی null |
$between | بازه |
فیلتر پیشفرض
با استفاده از متد default میتوان مقدار پیشفرض برای فیلتر تعریف کرد.
اگر کاربر در Request فیلتر مربوطه را ارسال نکند، این مقدار به صورت خودکار اعمال میشود.
DorapiProperty::filter()
->make('status')
->allowedOperators(FilterOperator::EQUAL)
->default(1)
پارامترها
مقدار پیشفرض فیلتر است. اگر کاربر در Request این فیلتر را ارسال نکند، این مقدار اعمال میشود. نوع مقدار میتواند باشد:
- int
- string
- bool
- array (برای اپراتورهایی مثل
$in) - null
- callable (برای مقدار داینامیک)
اپراتوری که برای مقدار پیشفرض استفاده میشود و باید از FilterOperator باشد.
مثال ساده
DorapiProperty::filter()
->make('status')
->allowedOperators([
FilterOperator::EQUAL,
FilterOperator::IN,
])
->default(1, FilterOperator::EQUAL);
در این حالت اگر کاربر هیچ فیلتری ارسال نکند، این شرط اعمال میشود:
status = 1
مثال با IN
->default([1,2,3], FilterOperator::IN)
معادل:
status IN (1,2,3)
مثال با مقدار داینامیک (callable)
گاهی مقدار پیشفرض باید در زمان اجرا محاسبه شود:
->default(fn() => auth()->id(), FilterOperator::EQUAL)
در این حالت فیلتر به شکل زیر اعمال میشود:
user_id = current_user_id
نکته مهم
default فقط زمانی اعمال میشود که کاربر آن فیلتر را در Request ارسال نکرده باشد.
اگر کاربر فیلتر را ارسال کند، مقدار پیشفرض کاملاً نادیده گرفته میشود.
Custom Query در فیلتر
در برخی موارد فیلتر کردن مستقیم روی ستون جدول ممکن نیست (مثلاً هنگام استفاده از relation یا subquery).
در این شرایط میتوان از customQuery استفاده کرد.
DorapiProperty::filter()
->make('province_name')
->allowedOperators(FilterOperator::EQUAL)
->customQuery(function ($query, $value, $operator) {
$query->whereHas('province', function ($query) use ($value) {
$query->where('name', $value);
});
})
در این مثال فیلتر روی نام استان اعمال میشود، در حالی که ستون province_name مستقیماً در جدول وجود ندارد.
برچسبهای قابل نمایش
با استفاده از این قابلیت، میتوانید مقادیر فنی را به برچسبهای قابل نمایش تبدیل کنید. این برچسبها در متا دیتای پاسخ API قرار میگیرند.
DorapiProperty::filter()
->make('is_active')
->allowedOperators([
FilterOperator::EQUAL,
FilterOperator::IN,
])
->labelResolver(fn($value) => is_array($value) ?
array_map(fn($val) => IsActive::from($val)->label(), $value) :
IsActive::from($value)->label()),
خروجی در meta:
"meta": {
"code": 1,
"message": null,
"data": [...],
"filters": [
{
"key": "is_active",
"value": "1",
"label": "فعال",
"operator": "$eq"
},
{
"key": "is_active",
"value": "1",
"label": "فعال",
"operator": "$in"
},
{
"key": "is_active",
"value": "0",
"label": "غیر فعال",
"operator": "$in"
}
]
}
مثال
GET /api/products?filters[price][$gte]=100000&filters[price][$lte]=500000
مرتبسازی (Sort)
تعریف Sort
use Dornica\APIKit\Dorapi\Enums\SortDirection;
public function sorts(): Sort
{
return DorapiProperty::sort()
->add('created_at')
->addDefault('price')
->addDefault('price', SortDirection::DESC);
}
متد های مرتبسازی
addDefault(string $column, SortDirection $direction = SortDirection::ASC): static
add(string $column, bool $isDefault = false, SortDirection $direction = SortDirection::ASC): static
مرتبسازی روی فیلدهای محاسباتی (Closure Fields)
فیلدهایی که با Closure در بخش fields تعریف میشوند نیز میتوانند در مرتبسازی استفاده شوند.
از آنجا که DorAPI این فیلدها را به صورت SubQuery در بخش SELECT اضافه میکند، در صورت داشتن alias میتوان آنها را مانند یک ستون معمولی در sort استفاده کرد.
مثال تعریف فیلد محاسباتی:
public function fields(): Field
{
return DorapiProperty::field()
->addDefault('title')
->add(
column: fn() => Comment::query()
->whereColumn('comments.user_id', 'users.id')
->selectRaw('count(*)'),
alias: 'user_comment_count'
);
}
در این مثال فیلد user_comment_count به خروجی اضافه میشود.
اکنون میتوان آن را در sorts نیز تعریف کرد:
public function sorts(): Sort
{
return DorapiProperty::sort()
->add('user_comment_count')
->add('created_at');
}
استفاده
GET /api/products?sort[0]=created_at:desc&sort[1]=user_comment_count:desc
Relation-based Sorting
Dorapi از مرتبسازی روی فیلدهای relation هم پشتیبانی میکند.
برای این کار کافی است نام relation و ستون مورد نظر را با فرمت زیر داخل sorts() تعریف کنید:
relation_name.column_name
مثال:
public function sorts(): Sort
{
return DorapiProperty::sort()
->addDefault('province.name');
}
در مثال بالا:
->addDefault('province.name')
به Dorapi اعلام میکند که امکان مرتبسازی کردن City ها بر اساس ستون name از relation province وجود دارد.
نحوه استفاده در Request
کاربر میتواند به شکل زیر درخواست مرتبسازی ارسال کند:
GET /cities?sort[0]=province.name:asc
یا:
GET /cities?sort[0]=province.name:desc
پیشنیازها
برای استفاده از relation sort باید:
- Relation داخل model وجود داشته باشد
public function province(): BelongsTo
{
return $this->belongsTo(Province::class);
}
- Relation داخل
relations()تعریف شده باشد
public function relations(): Relation
{
return DorapiProperty::relation()
->addDefault('province');
}
- فیلد sortable داخل
sorts()ثبت شده باشد
public function sorts(): Sort
{
return DorapiProperty::sort()
->addDefault('province.name');
}
قواعد
- اگر جهت مشخص نشود،
ascپیشفرض است - مرتبسازی روی فیلد تعریفنشده → نادیده گرفته میشود (بدون خطا)
- اگر
sortارسال نشود، فقط موارد تعریفشده به عنوان پیش فرض (Default) مرتبسازی میشوند - در حال حاضر relation sort فقط برای relation های
BelongsToوHasOneپشتیبانی میشود
صفحهبندی (Pagination)
Page-based (پیشفرض)
pagination[page]=1
pagination[pageSize]=15
Offset-based
pagination[start]=0
pagination[limit]=20
قوانین
- عدم امکان استفاده همزمان page و start
- page از 1 شروع میشود
- start از 0 شروع میشود
صفحه بندی به صورت پیش فرض فعال است و نیازی به تنظیم در کلاس Dorapi نیست.
Anti-Patterns (موارد ممنوع)
- استفاده از DorAPI بدون تعریف whitelist
- فعالسازی populate برای روابط سنگین بدون limit
- ارسال queryهای پیچیده با GET در داشبوردهای سنگین
جمعبندی
DorAPI یک ابزار Query سطح بالا است، نه یک ORM جدید. اگر قوانین آن رعایت شود:
- API قابل نگهداریتر میشود
- Frontend مستقلتر عمل میکند
- امنیت و performance کنترلشده باقی میماند