如何在Laravel中制作一个基于REST API的Web应用程序

30

我想在Laravel中创建一个API优先的应用程序。我不知道最好的方法是什么,我会解释一下我想做的事情,但请随意提供其他方法。

我不希望所有前端都是用javascript编写的,并使用angular.js或类似的东西解析API的JSON输出。我希望我的Laravel应用程序生成HTML视图。我正在尝试采用有两个控制器的方式,一个用于API,另一个用于Web。对于show User action,我的routes.php如下所示:

# the web controller
Route::controller('user', 'WebUserController');

# the api controller 
Route::group(array('prefix' => 'api'), function() {
    Route::resource('user', 'UserController');
});

所以/user将带我到WebUserController,而/api/user将带我到UserController。现在我想把所有逻辑放在API UserController中,并从WebUserController调用其操作。以下是它们的代码:

class UserController extends BaseController 
{
    public function show($id)
    {
        $user = User::find($id);
        return Response::json(array('success'=>true,'user'=>$user->toArray()));
    }
}

class WebUserController extends UserController 
{
    public function getView($id) 
    {
         # call the show method of the API's User Controller
         $response =  $this->show($id);
         return View::make('user.view')->with('data', $response->getData());
    }
}
WebUserController 中,我可以使用 getData() 方法获取响应的 JSON 内容,但是我无法获取头信息和状态码(它们是 Illuminate\Http\JsonResponse 的受保护属性)。
我认为我的方法可能不是最好的,所以我乐意听取建议,如何改进这个应用程序。 编辑: 如何获取响应的头信息和状态已经被Drew Lewis回答,但我仍然认为可能有更好的设计方式。

你好,Martin。我在Laravel 5.1中也需要解决同样的问题。那么,你是怎么实现的呢?你使用了仓库模式吗? - Ashish
@Ashish,我采用了Nyan的答案,当我问这个问题时。它似乎是最简单的解决方案,并且做到了我需要的。不过我还没有使用Laraval 5.1,不知道有什么变化。 - Martin Taleski
你是否为 Web 和 API 创建了不同的控制器?如果是,你如何避免代码重复?我猜想使用仓储设计模式可以将数据库逻辑从控制器中分离出来。 - Ashish
6个回答

44
你应该使用仓库/网关设计模式:请查看这里的答案。
例如,当处理用户模型时,首先创建一个用户仓库。用户仓库的唯一职责是与数据库通信(执行CRUD操作)。此用户仓库扩展了一个常见的基础仓库并实现了包含您所需所有方法的接口。
class EloquentUserRepository extends BaseRepository implements UserRepository
{
    public function __construct(User $user) {
        $this->user = $user;
    }


    public function all() {
        return $this->user->all();
    }

    public function get($id){}

    public function create(array $data){}

    public function update(array $data){}

    public function delete($id){}

    // Any other methods you need go here (getRecent, deleteWhere, etc)

}

接下来,创建一个服务提供者,将用户存储库接口绑定到您的Eloquent用户存储库。每当您需要用户存储库(通过IoC容器解析它或在构造函数中注入依赖项),Laravel自动为您提供刚刚创建的Eloquent用户存储库的实例。这样,如果您更改ORM以使用除Eloquent之外的其他东西,只需更改此服务提供者即可,无需对代码库进行其他更改:

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider {

    public function register() {
        $this->app->bind(
            'lib\Repositories\UserRepository',        // Assuming you used these
            'lib\Repositories\EloquentUserRepository' // namespaces
        );
    }

}

接下来,创建一个用户网关,其目的是与任意数量的存储库进行通信并执行应用程序的任何业务逻辑:

use lib\Repositories\UserRepository;

class UserGateway {

    protected $userRepository;

    public function __construct(UserRepository $userRepository) {
        $this->userRepository = $userRepository;
    }

        public function createUser(array $input)
        {
            // perform any sort of validation first
            return $this->userRepository->create($input);
        }

}

最后,创建您的用户Web控制器。该控制器与您的用户网关进行通信:

class UserController extends BaseController 
{
    public function __construct(UserGatway $userGateway)
    {
        $this->userGateway = $userGateway;
    }

    public function create()
    {
        $user = $this->userGateway->createUser(Input::all());

    }
}

通过以这种方式构建您的应用程序设计,您将获得以下几个好处:您将实现非常清晰的关注点分离,因为您的应用程序将遵循单一职责原则(通过将业务逻辑与数据库逻辑分离)。这使您能够更轻松地执行单元测试和集成测试,使您的控制器尽可能简洁,并允许您在将来轻松地切换到任何其他数据库,而无需使用 Eloquent。
例如,如果从 Eloquent 切换到 Mongo,则您需要更改的仅是服务提供程序绑定以及创建一个实现 UserRepository 接口的 MongoUserRepository。这是因为存储库是唯一与您的数据库通信的东西-它没有其他任何知识。因此,新的 MongoUserRepository 可能看起来像这样:
class MongoUserRepository extends BaseRepository implements UserRepository
{
    public function __construct(MongoUser $user) {
        $this->user = $user;
    }


    public function all() {
        // Retrieve all users from the mongo db
    }

    ...

}

服务提供者现在将UserRepository接口绑定到新的MongoUserRepository。
 $this->app->bind(
        'lib\Repositories\UserRepository',       
        'lib\Repositories\MongoUserRepository'
);

在所有网关中,您一直在引用UserRepository,所以通过进行这个更改,您基本上是告诉Laravel使用新的MongoUserRepository而不是旧的Eloquent。不需要进行其他更改。

1
谢谢,这个设计比Nyan的答案更复杂...你能否解释一下它的好处。例如,如果您要将Eloquent更改为另一个ORM,则需要更改EloquentUserRepository,但还需要更改所有网关。 - Martin Taleski
请重新阅读我的回答 - 我添加了更多信息。如果更改到另一个ORM,您不需要更改任何网关,只需更改服务提供程序和存储库即可。 - seeARMS
7
针对一个不太可能的情况,使用过多的样板代码来更换ORM并不值得。 - malhal
1
你好,我已经根据建议制作了一个模板。有一件事我无法解决,那就是如何处理验证。您能否建议如何将验证服务与其集成?代码:https://github.com/octabrain/Laravel4-Patterns - user361697
1
终于成功把所有碎片组合在一起了。代码请访问 https://github.com/octabrain/Laravel4-Patterns - user361697
你会如何处理关系?假设我想创建一个文档,其中包含一些关系,例如创建者、客户和行。你会将这些关系传递给 $documentRepository->create() 方法吗?假设至少创建者和客户不能为空。 - Pedro Moreira

9
您应该使用 Repository 来实现这个设计。
例如 -
//UserRepository Class
class UserRepository {
    public function getById($id)
    {
      return User::find($id);
    }
}

// WebUser Controller
class WebUserController extends BaseController {
    protected $user;

    public function __construct(UserRepository $user)
    {
        $this->user = $user;
    }

    public function show($id)
    {
        return View::make('user.view')->with('data', $this->user->getById($id));
    }
}

// APIUser Controller
class UserController extends BaseController {
    protected $user;

    public function __construct(UserRepository $user)
    {
        $this->user = $user;
    }

    public function show($id)
    {
        $data =>$this->user->getById($id);
        return Response::json(array('success'=>true,'user'= $data->toArray()));
    }
}

2
这看起来很好,很简单,每个模型只有一个额外的类。 - Martin Taleski

2
这是Jeffrey Way制作的视频,他是较为优秀的Laravel开发者之一。在本教程中,他将一个BackboneJS应用程序连接到他在Laravel中设置的RESTful服务中。没有比这更好的了。我可以给你写很多样板代码,但最好还是通过观看一个不错的视频并喝杯咖啡来学习它。 ;) https://www.youtube.com/watch?v=uykzCfu1RiQ

嗯,这个似乎展示了如何在Laravel中制作一个后端JSON API,并使用Backbone作为前端,但这并不完全是我要找的。 - Martin Taleski
只需使用json_decode(),您就可以得到一个php数组,并忽略它是为BackboneJS而设计的。它是RESTful的,因此调用API的方式并不重要。它返回JSON格式的数据非常棒,因为任何编程语言都可以处理JSON! - sidneydobber
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Rafael Bugajewski

2

我已经完成了那个教程... 它没有回答我的问题,即如何将逻辑保留在API控制器中,并为调用API控制器的Web应用程序创建单独的控制器/视图。 - Martin Taleski

1
我建议使用一个代码库。 试图从另一个控制器调用一个控制器会导致落入一个称为HMVC(分层模型-视图-控制器)的模式中。 这意味着您的整个应用程序依赖于较低的模块。 在这种情况下,您的API将作为数据存储库(起初并不是最糟糕的事情)。
然而,当您修改API返回数据的结构时,所有依赖它的其他内容都必须知道如何响应。 假设您想要进行授权检查以查看登录用户是否能够查看返回的用户详细信息,并且出现了错误。
在API中,您将返回一个带有403禁止代码和一些元数据的响应对象。 您的HTML控制器必须知道如何处理此问题。
相比之下,存储库可以抛出异常。
public function findById ($id)
{
    $user = User::findOrFail($id);

    if (Auth::user->hasAccessTo($user)) {
        return $user;
    } else {
        throw new UnauthorizedAccessException('you do not have sufficient access to this resource');
    }
}

你的API控制器将更像这样:

public function show($id)
{
    try {
        return $this->user->findById($id);
    } catch (UnauthorizedAccessException $e) {
        $message = $e->getMessage();
        return Response::json('403', ['meta' => ['message' => $message]]));
    }
}

你的HTML控制器将会像这样:

public function show($id)
{
    try {
        $user = $this->user->findById($id);
    } catch (UnauthorizedAccessException $e) {
        Session::flash('error', $e->getMessage());
        // Redirect wherever you would like
        return Response::redirect('/');
    }
}

这样可以让你的代码更易重用,使你能够独立地更改控制器实现,而不必担心更改其他行为。 我在此帖子中更详细地介绍了如何实现存储库模式:如果您愿意,可以忽略接口并直接跳到实现部分。

1
我对你遇到的响应问题有所回应。你可以从响应中获取头部、状态码和数据。
// your data
$response->getData();

// the status code of the Response
$response->getStatusCode(); 

// array of headers
$response->headers->all();

// array of headers with preserved case
$response->headers->allPreserveCase(); 
$response->headers 是一个继承自 Symfony\Component\HttpFoundation\HeaderBag 的 Symfony\Component\HttpFoundation\ResponseHeaderBag。

是的,那获取了我需要的所有东西。那么就总体来说呢,你能想到更好的方法吗? - Martin Taleski
@martin 我在考虑一种方法,只使用一个控制器来处理两者。构建响应数据并将其传递给一个中间层,根据它是否来自API路由,将返回正确的响应。但这取决于您期望Api@show与Web@getView在数据方面有多大不同。 - lagbox

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接