Статьи

Работа с AngularJS и Silex в качестве поставщика ресурсов

В эти дни я играю с AngularJS . Angular — это отличный фреймворк, когда мы создаем сложные интерфейсные приложения с помощью JavaScript. И самое приятное то, что это очень просто понять (и мне действительно нравятся простые вещи). Сегодня мы будем играть с ресурсами . Ресурсы хороши, когда нам нужно использовать ресурсы RestFull с сервера. В этом примере мы собираемся использовать Silex в бэкэнде. Давайте начнем.

Прежде всего мы должны понимать, что ресурсы не включены в основной js-файл AngularJS, и нам нужно включить angular-resource.js (он поставляется с пакетом Angular). Нам не нужны ресурсы. Мы можем создавать наши http-сервисы с AngularJS без использования этого дополнительного js-файла, но он обеспечивает очень чистую абстракцию (по крайней мере для меня), и мне это нравится.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular-resource.min.js"></script>

Мы собираемся создать простое приложение с CRUD-операциями в таблице. В этом примере мы будем использовать одну простую базу данных SqlLite (включена в репозиторий github)

CREATE TABLE main.messages (
  id INTEGER PRIMARY KEY  NOT NULL ,
  author VARCHAR NOT NULL ,
  message VARCHAR NOT NULL );

Наш основной (и только один) HTML-файл:

<!DOCTYPE html>
<html ng-app="MessageService">
<head>
    <title>Angular Resource Example</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular-resource.min.js"></script>
    <script src="js/services.js"></script>
    <script src="js/controllers.js"></script>
</head>
<body ng-controller="MessageController">
 
<h2>Message list <a href="#" ng-click="refresh()">refresh</a></h2>
<ul>
    <li ng-repeat="message in messages">
        <a href="#" ng-click="deleteMessage($index, message.id)">delete</a>
        #{{message.id}} - {{message.author}} - {{message.message}}
        <a href="#" ng-click="selectMessage($index)">edit</a>
    </li>
</ul>
 
<form>
    <input type="text" ng-model="author" placeholder="author">
    <input type="text" ng-model="message" placeholder="message">
 
    <button ng-click="add()" ng-show='addMode'>Create</button>
    <button ng-click="update()" ng-hide='addMode'>Update</button>
    <button ng-click="cancel()" ng-hide='addMode'>Cancel</button>
</form>
</body>
</html>

Как мы видим, мы будем использовать ng-app = ”MessageService”, определенный в файле js / services.js:

angular.module('MessageService', ['ngResource']).factory('Message', ['$resource', function ($resource) {
    return $resource('/api/message/resource/:id');
}]);

И наш контроллер в js / controllers.js:

function MessageController($scope, Message) {
 
    var currentResource;
    var resetForm = function () {
        $scope.addMode = true;
        $scope.author = undefined;
        $scope.message = undefined;
        $scope.selectedIndex = undefined;
    }
 
    $scope.messages = Message.query();
    $scope.addMode = true;
 
    $scope.add = function () {
        var key = {};
        var value = {author: $scope.author, message: $scope.message}
 
        Message.save(key, value, function (data) {
            $scope.messages.push(data);
            resetForm();
        });
    };
 
    $scope.update = function () {
        var key = {id: currentResource.id};
        var value = {author: $scope.author, message: $scope.message}
        Message.save(key, value, function (data) {
            currentResource.author = data.author;
            currentResource.message = data.message;
            resetForm();
        });
    }
 
    $scope.refresh = function () {
        $scope.messages = Message.query();
        resetForm();
    };
 
    $scope.deleteMessage = function (index, id) {
        Message.delete({id: id}, function () {
            $scope.messages.splice(index, 1);
            resetForm();
        });
    };
 
    $scope.selectMessage = function (index) {
        currentResource = $scope.messages[index];
        $scope.addMode = false;
        $scope.author = currentResource.author;
        $scope.message = currentResource.message;
    }
 
    $scope.cancel = function () {
        resetForm();
    }
}

Теперь часть бэкэнда. Как мы уже говорили, мы будем использовать Silex. Мы также будем использовать RouteCollections для определения наших маршрутов (вы можете прочитать об этом здесь ). Итак, наше приложение Silex будет:

<?php
require_once __DIR__ . '/vendor/autoload.php';
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
use Symfony\Component\Routing\RouteCollection;
use Silex\Application;
 
$app = new Silex\Application();
 
$app['routes'] = $app->extend('routes', function (RouteCollection $routes, Application $app) {
        $loader     = new YamlFileLoader(new FileLocator(__DIR__ . '/config'));
        $collection = $loader->load('routes.yml');
        $routes->addCollection($collection);
 
        return $routes;
    }
);
 
$app->register(
    new Silex\Provider\DoctrineServiceProvider(),
    array(
        'db.options' => array(
            'driver' => 'pdo_sqlite',
            'path'   => __DIR__ . '/db/app.db.sqlite',
        ),
    )
);
 
$app->run();

Мы определяем наши маршруты в messageResource.yml

getAll:
  path: /resource
  defaults: { _controller: 'Message\MessageController::getAllAction' }
  methods:  [GET]
 
getOne:
  path: /resource/{id}
  defaults: { _controller: 'Message\MessageController::getOneAction' }
  methods:  [GET]
 
deleteOne:
  path: /resource/{id}
  defaults: { _controller: 'Message\MessageController::deleteOneAction' }
  methods:  [DELETE]
 
addOne:
  path: /resource
  defaults: { _controller: 'Message\MessageController::addOneAction' }
  methods:  [POST]
 
editOne:
  path: /resource/{id}
  defaults: { _controller: 'Message\MessageController::editOneAction' }
  methods:  [POST]

И, наконец, наш контроллер ресурсов:

<?php
namespace Message;
 
use Silex\Application;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
 
class MessageController
{
    public function getAllAction(Application $app)
    {
        return new JsonResponse($app['db']->fetchAll("SELECT * FROM messages"));
    }
 
    public function getOneAction($id, Application $app)
    {
        return new JsonResponse($app['db']
            ->fetchAssoc("SELECT * FROM messages WHERE id=:ID", ['ID' => $id]));
    }
 
    public function deleteOneAction($id, Application $app)
    {
        return $app['db']->delete('messages', ['ID' => $id]);
    }
 
    public function addOneAction(Application $app, Request $request)
    {
        $payload = json_decode($request->getContent());;
 
        $newResource = [
            'id'      => (integer)$app['db']
                ->fetchColumn("SELECT max(id) FROM messages") + 1,
            'author'  => $payload->author,
            'message' => $payload->message,
        ];
        $app['db']->insert('messages', $newResource);
 
        return new JsonResponse($newResource);
    }
 
    public function editOneAction($id, Application $app, Request $request)
    {
        $payload = json_decode($request->getContent());;
        $resource = [
            'author'  => $payload->author,
            'message' => $payload->message,
        ];
        $app['db']->update('messages', $resource, ['id' => $id]);
 
        return new JsonResponse($resource);
    }
}

И это все. Наш прототип работает с AngularJS и Silex в качестве REST-провайдера. Мы должны позаботиться об одном. Silex и AngularJS не согласны в одном об услугах REST. AngularJS удаляет косую черту в некоторых случаях. Silex (и Symfony) возвращает HTTP 302, перемещенный временно, когда мы пытаемся получить доступ к ресурсу без конечной косой черты, но когда мы работаем со смонтированными контроллерами, мы получим страницу 404, не найденную (ошибка / функция?). Это потому, что мой REST-сервис — это / api / message / resource /: id вместо / api / message /: id. Если я выбрал второй, когда angular пытается создать новый ресурс, он будет POST / api / message вместо POST / api / message /. Мы используем смонтированные маршруты в этом примере:

messages:
  prefix: /message
  resource: messageResource.yml

С одним простым приложением Silex (без смонтированных маршрутов) в одном файле этого не происходит (мы увидим HTTP 302 и новый запрос с косой чертой). Из-за этого я использую этот небольшой хак, чтобы обойти проблему.

Вы можете увидеть полный код примера в моей учетной записи на github .