CRUD Service

XinAdmin provides an abstract BaseService base class that encapsulates common CRUD operations, data validation, and query building functionality, allowing developers to quickly build business service layers.

Basic Usage

Create a service class that extends BaseService and define the $model property pointing to the corresponding Eloquent model:

<?php

namespace App\Services;

use App\Models\User;

class UserService extends BaseService
{
    protected string $model = User::class;
}

After extending, the service class automatically has the following methods:

MethodParametersReturnDescription
findint $idarrayQuery a single record by ID
queryarray $paramsarrayPaginated list query
createarray $databoolCreate new record
updateint $id, array $databoolUpdate record
deleteint $idboolDelete record

Search Field Configuration

$searchField - Precise Search Fields

Define fields that support searching and their query operators:

protected array $searchField = [
    'status' => '=',           // Exact match
    'age' => '>',              // Greater than
    'score' => '>=',           // Greater than or equal
    'price' => '<',            // Less than
    'stock' => '<=',           // Less than or equal
    'code' => '<>',            // Not equal
    'name' => 'like',          // Fuzzy search (both sides)
    'email' => 'afterLike',    // Post fuzzy search (prefix match)
    'phone' => 'beforeLike',   // Pre fuzzy search (suffix match)
    'created_at' => 'date',           // Date query
    'updated_at' => 'betweenDate',    // Date range query
];

Operator Description:

OperatorSQL ExampleDescription
=WHERE field = valueExact match
>WHERE field > valueGreater than
>=WHERE field >= valueGreater than or equal
<WHERE field < valueLess than
<=WHERE field <= valueLess than or equal
<>WHERE field <> valueNot equal
likeWHERE field LIKE '%value%'Full fuzzy match
afterLikeWHERE field LIKE 'value%'Post fuzzy (prefix match)
beforeLikeWHERE field LIKE '%value'Pre fuzzy (suffix match)
dateWHERE DATE(field) = 'Y-m-d'Date exact match
betweenDateWHERE DATE(field) BETWEEN start AND endDate range query

$quickSearchField - Quick Search Fields

Define fields that support keyword search for global fuzzy search:

protected array $quickSearchField = ['name', 'email', 'phone'];

When the frontend passes the keywordSearch parameter, it will perform fuzzy matching across all quick search fields:

// Generated SQL
WHERE (name LIKE '%keyword%' OR email LIKE '%keyword%' OR phone LIKE '%keyword%')

Query Parameters

The query() method supports the following request parameters:

Pagination Parameters

ParameterTypeDefaultDescription
pageSizeint10Number of items per page

filter - Filter Conditions

Used for multi-value filtering, supports JSON string or array format:

{
    "filter": {
        "status": [1, 2],
        "type": ["admin", "user"]
    }
}

Generated SQL:

WHERE status IN (1, 2) AND type IN ('admin', 'user')

Performs global fuzzy search in fields defined by $quickSearchField:

{
    "keywordSearch": "John"
}

sorter - Sorting

Supports single field sorting, values are ascend (ascending) or descend (descending):

{
    "sorter": {
        "created_at": "descend"
    }
}

Search Field Parameters

Pass corresponding parameters based on fields defined in $searchField:

{
    "status": 1,
    "name": "John",
    "created_at": "2024-01-15",
    "updated_at": ["2024-01-01", "2024-01-31"]
}

Data Validation

Define Validation Rules

Override the rules() method to define validation rules:

protected function rules(): array
{
    return [
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email',
        'password' => 'required|string|min:6|confirmed',
        'status' => 'integer|in:0,1',
    ];
}

Define Validation Messages

Override the messages() method to define custom error messages:

protected function messages(): array
{
    return [
        'name.required' => 'Name is required',
        'email.required' => 'Email is required',
        'email.email' => 'Invalid email format',
        'email.unique' => 'Email already in use',
        'password.min' => 'Password must be at least 6 characters',
    ];
}

Distinguish Create and Update Validation

Use the isUpdate() method to check if it's an update request:

protected function rules(): array
{
    $rules = [
        'name' => 'required|string|max:255',
        'status' => 'integer|in:0,1',
    ];

    if ($this->isUpdate()) {
        $rules['email'] = 'required|email|unique:users,email,' . request()->route('id');
    } else {
        $rules['email'] = 'required|email|unique:users,email';
        $rules['password'] = 'required|string|min:6';
    }

    return $rules;
}

Complete Example

<?php

namespace App\Services;

use App\Models\User;
use App\Services\BaseService;

class UserService extends BaseService
{
    protected string $model = User::class;

    protected array $searchField = [
        'status' => '=',
        'role_id' => '=',
        'name' => 'like',
        'email' => 'like',
        'created_at' => 'betweenDate',
    ];

    protected array $quickSearchField = ['name', 'email', 'phone'];

    protected function rules(): array
    {
        $rules = [
            'name' => 'required|string|max:255',
            'email' => 'required|email',
            'phone' => 'nullable|string|max:20',
            'status' => 'integer|in:0,1',
            'role_id' => 'required|exists:roles,id',
        ];

        if ($this->isUpdate()) {
            $rules['email'] .= '|unique:users,email,' . request()->route('id');
        } else {
            $rules['email'] .= '|unique:users,email';
            $rules['password'] = 'required|string|min:6';
        }

        return $rules;
    }

    protected function messages(): array
    {
        return [
            'name.required' => 'Name is required',
            'email.required' => 'Email is required',
            'email.email' => 'Invalid email format',
            'email.unique' => 'Email already in use',
            'password.required' => 'Password is required',
            'password.min' => 'Password must be at least 6 characters',
            'role_id.required' => 'Please select a role',
            'role_id.exists' => 'Role does not exist',
        ];
    }
}

Tree Data

The getTreeData() method is used to build tree-structured data:

public function tree(): array
{
    $list = $this->model::query()
        ->orderBy('sort', 'asc')
        ->get()
        ->toArray();

    return $this->getTreeData($list);
}

Data Requirements:

  • Data must contain id and parent_id fields
  • Root node's parent_id defaults to 0
  • Generated child nodes are stored in the children field

Exception Handling

BaseService uses RepositoryException to throw exceptions:

ScenarioException Message
Record not foundModel not found
Validation failedValidation failed: {error message}
Empty create dataValidation failed: empty data

Exceptions are caught by the global exception handler and return unified JSON responses.

Extending Methods

You can extend or override parent methods in subclasses:

class UserService extends BaseService
{
    public function query(array $params): array
    {
        $query = $this->model::query()->with(['role', 'department']);
        $result = $this->buildSearch($params, $query)
            ->paginate($params['pageSize'] ?? 10)
            ->toArray();

        return $result;
    }

    public function create(array $data): bool
    {
        $data['password'] = bcrypt($data['password']);
        return parent::create($data);
    }
}