Skip to content

Service 标准示例 — Article

源文件:server/app/service/article/ArticleService.php(生成代码必须模仿本示例的结构与风格)

本示例为手写风格范本,用于学习结构与分层;经 make:crud 生成的骨架方法名以骨架为准,做业务增量时不要为对齐本示例而改名。

完整代码

php
<?php
/* ============================================================
 * 项目:元点Admin
 * 官网:https://www.dev007.cn
 * Slogan:提供高质量行业系统源码,帮助中小企业快速搭建专属应用
 * Author:mashanglai Team
 * ============================================================ */
declare(strict_types=1);

namespace app\service\article;

use app\model\article\Article;
use app\repository\article\ArticleRepository;
use core\base\Service;

class ArticleService extends Service
{
    protected ArticleRepository $articleRepository;

    /**
     * 获取文章列表(管理端)
     */
    public function getArticleList(array $params): array
    {
        [$page, $limit] = $this->extractPagination($params);
        return $this->articleRepository->getSearchList($params, $page, $limit);
    }

    /**
     * 获取文章详情(带分类名称)
     */
    public function getArticleDetail(int $id): ?array
    {
        $article = $this->articleRepository->findWithCategory($id);
        if (!$article) {
            $this->throwBusinessException(lang('business.record_not_found'));
        }

        // 递增浏览量
        $this->articleRepository->incrementViewCount($id);

        return $article;
    }

    /**
     * 创建文章
     */
    public function createArticle(array $data): array
    {
        // 如果状态为已发布,设置发布时间
        if (isset($data['status']) && (int) $data['status'] === Article::STATUS_PUBLISHED) {
            $data['publish_at'] = $data['publish_at'] ?? date('Y-m-d H:i:s');
        }

        $article = $this->articleRepository->create($data);

        $this->trigger('article.created', [
            'article_id' => $article['id'],
            'title'      => $data['title'],
        ]);

        return $article;
    }

    /**
     * 更新文章
     */
    public function updateArticle(int $id, array $data): bool
    {
        $article = $this->findOrFail($this->articleRepository, $id);

        // 如果从草稿变为已发布且没有发布时间,设置发布时间
        if (isset($data['status']) && (int) $data['status'] === Article::STATUS_PUBLISHED
            && (int) $article['status'] === Article::STATUS_DRAFT) {
            $data['publish_at'] = $data['publish_at'] ?? date('Y-m-d H:i:s');
        }

        return $this->articleRepository->update($id, $data);
    }

    /**
     * 删除文章(软删除)
     */
    public function deleteArticle(int $id): bool
    {
        $this->findOrFail($this->articleRepository, $id);
        return $this->articleRepository->delete($id);
    }

    /**
     * 更新文章状态
     */
    public function updateStatus(int $id, int $status): bool
    {
        $article = $this->findOrFail($this->articleRepository, $id);

        $updateData = ['status' => $status];

        // 发布时设置发布时间
        if ($status === Article::STATUS_PUBLISHED && (int) $article['status'] === Article::STATUS_DRAFT) {
            $updateData['publish_at'] = date('Y-m-d H:i:s');
        }

        return $this->articleRepository->update($id, $updateData);
    }

    /**
     * 获取已发布的文章列表(C端)
     */
    public function getPublishedList(array $params): array
    {
        [$page, $limit] = $this->extractPagination($params);
        $categoryId = (int) ($params['category_id'] ?? 0);
        return $this->articleRepository->getPublishedList($page, $limit, $categoryId);
    }
}

要点注解

  • 第 18 行 protected ArticleRepository $articleRepository; 是唯一的依赖属性声明,由 core\base\Service 基类自动 DI 注入;Service 全程只调用 $this->articleRepository->xxx(),禁止出现 Db::table()Article::where()/Article::find()/Article::create() 等 Model 静态查询。
  • 第 34、40 行 getArticleDetail() 中所有数据访问(findWithCategoryincrementViewCount)都委托给 Repository 方法,Service 本身不拼装任何查询条件,只做"判空 → 抛异常 → 编排调用顺序"的业务流程控制。
  • 第 36 行 $this->throwBusinessException(lang('business.record_not_found')):找不到记录时统一用基类提供的异常抛出方法 + 多语言键,不直接 throw new Exception('xxx')
  • 第 70、86、95 行 $this->findOrFail($this->articleRepository, $id):更新/删除/改状态前先用基类通用方法校验记录存在,避免每个方法各自重复 if (!$xxx) { throw ... } 判断逻辑。
  • 第 57-60 行 $this->trigger('article.created', [...]):创建成功后的副作用(如通知、日志、缓存清理)通过事件触发交给 Listener 处理,Service 内部不写内联的副作用代码(如直接记录操作日志)。
  • 第 51、73-75、100 行状态相关判断使用 Article::STATUS_PUBLISHED / Article::STATUS_DRAFT 常量而非魔法数字 1/0,业务分支(发布时间自动填充)体现"从草稿到已发布"的状态机编排逻辑,属于 Service 该管的业务规则。
  • 第 25、112 行 [$page, $limit] = $this->extractPagination($params);:分页参数提取统一走基类方法,不在 Service 内手写 (int)($params['page_no'] ?? 1) 等重复代码。
  • 本文件全程没有 Db::startTrans()/commit()/rollback(),因为单表单步操作不需要事务;若涉及多表联动写入才需要在 Service 方法内包裹事务,事务边界只能出现在 Service 层,不能下沉到 Repository。

基于 MIT 许可发布