Русский
Русский
English
Статистика
Реклама

Symfony framework

Перевод Развертывание приложения Symfony в AWS Lambda

11.06.2021 18:10:18 | Автор: admin

Сначала давайте разберемся, что такое бессерверная архитектура и когда она нужна.

Бессерверная архитектура позволяет выполнять фрагменты кода без мороки с инфраструктурой: в этом случае управлением веб-сервером, физическим оборудованием и администрированием занимается облачный провайдер, позволяя вам сосредоточиться исключительно на коде.

AWS Lambda обеспечивает высокую доступность, причем плата взимается только за фактически затрачиваемое время вычислений. Этот сервис может быть весьма полезен для таких задач, как запуск cron-заданий, отправка уведомлений в режиме реального времени, предоставление доступа к API, обработка каких-нибудь событий при выполнении различных операций и т.д. В сети можно найти массу примеров использования сервиса.

Наш сценарий использования

Предоставить доступ к API, созданному с помощью Symfony, который публикует сообщения в LinkedIn. Процесс разработки будет включать этапы от написания до развертывания кода.

Пишем код

В Symfony 5-й версии появился новый компонент под названием Notifier, который дает возможность отправлять уведомления через разные сервисы (Slack, Twitter, Twilio и др.).

В Symfony нет встроенной поддержки LinkedIn, поэтому несколько месяцев назад я создал поверх Notifier шлюз для публикации контента в этой социальной сети. Исходный код шлюза можно посмотреть по ссылке, его же мы будем использовать и в этой демонстрации.

Приступаем

$ symfony new --full aws-lambda-linkedin-notifier$ cd aws-lambda-linkedin-notifier$ composer require eniams/linkedin-notifier

Включаем шлюз (см. документацию)

<?php// config/bundles.phpreturn [// others bundles,Eniams\Notifier\LinkedIn\LinkedInNotifierBundle::class => ['all' => true]];// .envLINKEDIN_DSN=

Логика публикации контента

<?phpclass PostContentController{    /**     * @Route("/contents", name="post_content", methods="POST")     */    public function __invoke(NotifierInterface $notifier, Request $request)    {        if(null !== $message = (\json_decode($request->getContent(), true)['message'] ?? null)) {            $notifier->send(new Notification($message, ['chat/linkedin']));            return new JsonResponse('message posted with success', 201);        }        throw new BadRequestException('Missing "message" in body');    }}

Логика проста: мы предоставляем доступ к API через маршрут /contents, который принимает запрос POST с сообщением message в его теле.

В 11-й строке мы отправляем публикуемое сообщение в LinkedIn благодаря Symfony и шлюзу это делается очень просто!

Будучи профессиональными разработчиками, покроем этот код автотестами:

<?phpclass PostContentControllerTest extends WebTestCase    /**     * @dataProvider methodProvider     */    public function testANoPostRequestShouldReturnA405(string $method)    {        $client = static::createClient();        $client->request($method, '/contents');        self::assertEquals(405, $client->getResponse()->getStatusCode());    }    public function testAPostRequestWithoutAMessageInBodyShouldReturnA400()    {        $client = static::createClient();        $client->request('POST', '/contents');        self::assertEquals(400, $client->getResponse()->getStatusCode());    }    public function testAPostRequestWithAMessageInBodyShouldReturnA201()    {        $request = new Request([],[],[],[],[],[], json_encode(['message' => 'Hello World']));        $notifier = new class implements NotifierInterface {            public function send(Notification $notification, Recipient ...$recipients): void            {            }        };        $controller = new PostContentController();        $response = $controller->__invoke($notifier, $request);        self::assertEquals(201, $response->getStatusCode());    }    public function methodProvider()    {        return [            ['GET'],            ['PUT'],            ['DELETE'],        ];    }view rawTestPostContentController.php hosted with  by GitHub

Код готов! Пришло время его развернуть

Стоп! Что?! AWS Lambda не поддерживает PHP!

Именно так: AWS Lambda поддерживает не все языки программирования, а только некоторые, в том числе Go, Java, Python, Ruby, NodeJS и .NET.

Теперь надо учить новый язык и переписывать код? Надеюсь, нет!

Ничего переписывать не придется благодаря Матье Напполи (Matthieu Nappoli), создателю Bref.sh, и замечательной команде, помогающей ему поддерживать этот проект с открытым исходным кодом.

Bref позволяет развертывать PHP-приложения в AWS и запускать их на AWS Lambda.

Конфигурация развертывания

Добавим в kernel.php обработку логов:

<?php    // Kernel.php    public function getLogDir(): string    {        if (getenv('LAMBDA_TASK_ROOT') !== false) {            return '/tmp/log/';        }        return parent::getLogDir();    }    public function getCacheDir()    {        if (getenv('LAMBDA_TASK_ROOT') !== false) {            return '/tmp/cache/'.$this->environment;        }        return parent::getCacheDir();    }

Подготовим фреймворк Serverless (см. документацию):

$ npm install -g serverless$ serverless config credentials --provider aws --key  --secret$ composer require bref/bref

Зададим конфигурацию в файле serverless.yaml:

service: notifier-linkedin-apiprovider:    name: aws    region: eu-west-3    runtime: provided    environment: # env vars        APP_ENV: prod        LINKEDIN_DSN: YOUR_DSNplugins:    - ./vendor/bref/breffunctions:    website:        handler: public/index.php # bootstrap         layers:            - ${bref:layer.php-73-fpm} # https://bref.sh/docs/runtimes/index.html#usage         timeout: 28 # Timeout to stop the compute time        events:            - http: 'POST /contents' # Only POST to /contents are allowedpackage:    exclude:        - 'tests/**'view rawserverless.yaml hosted with  by GitHub

Развертываем!

$ serverless deployServerless: Packaging service...Serverless: Excluding development dependencies...Serverless: Uploading CloudFormation file to S3...Serverless: Uploading artifacts...Serverless: Uploading service notifier-linkedin-api.zip file to S3 (10.05 MB)...Serverless: Validating template...Serverless: Updating Stack...Serverless: Checking Stack update progress.......................Serverless: Stack update finished...Service Informationservice: notifier-linkedin-apistage: devregion: eu-west-3stack: notifier-linkedin-api-devresources: 15api keys:  Noneendpoints:  POST - https://xxx.execute-api.eu-west-3.amazonaws.com/dev/contentsfunctions:  website: notifier-linkedin-api-dev-websitelayers:  NoneServerless: Removing old service artifacts from S3...Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.

Теперь наш код развернут в AWS Lambda, а API доступен по адресу https://xxx.execute-api.eu-west-3.amazonaws.com/dev/contents.

Попробуем:


Перевод материала подготовлен в рамках курса "Symfony Framework". Если интересно узнать о курсе больше, приглашаем на день открытых дверей онлайн, где преподаватель расскажет о формате и программе обучения.

Подробнее..

Перевод REST API в Symfony (без FosRestBundle) с использованием JWT аутентификации. Часть 1

06.07.2020 18:16:50 | Автор: admin

Перевод статьи подготовлен в преддверии старта курса Symfony Framework.





В первой части статьи мы рассмотрим самый простой способ реализации REST API в проекте Symfony без использования FosRestBundle. Во второй части, которую я опубликую следом, мы рассмотрим JWT аутентификацию. Прежде чем мы начнем, сперва мы должны понять, что на самом деле означает REST.


Что означает Rest?


REST (Representational State Transfer передача состояния представления) это архитектурный стиль разработки веб-сервисов, который невозможно игнорировать, потому что в современной экосистеме существует большая потребность в создании Restful-приложений. Это может быть связано с уверенным подъемом позиций JavaScript и связанных фреймворков.


REST API использует протокол HTTP. Это означает, что когда клиент делает какой-либо запрос к такому веб-сервису, он может использовать любой из стандартных HTTP-глаголов: GET, POST, DELETE и PUT. Ниже описано, что произойдет, если клиент укажет соответствующий глагол.


  • GET: будет использоваться для получения списка ресурсов или сведений о них.
  • POST: будет использоваться для создания нового ресурса.
  • PUT: будет использоваться для обновления существующего ресурса.
  • DELETE: будет использоваться для удаления существующего ресурса.

REST не имеет состояний (state), и это означает, что на стороне сервера тоже нет никаких состояний запроса. Состояния остаются на стороне клиента (пример использование JWT для аутентификации, с помощью которого мы собираемся засекьюрить наш REST API). Таким образом, при использовании аутентификации в REST API нам нужно отправить аутентификационный заголовок, чтобы получить правильный ответ без хранения состояния.


Создание проекта Symfony:


Во-первых, мы предполагаем, что вы уже установили PHP и менеджер пакетов Сomposer для создания нового проекта Symfony. С этим всем в наличии создайте новый проект с помощью следующей команды в терминале:


composer create-project symfony/skeleton demo_rest_api


Создание проекта Symfony


Мы используем базовый скелет Symfony, который рекомендуется для микросервисов и API. Вот как выглядит структура каталогов:



Структура проекта


Config: содержит все настройки бандла и список бандлов в bundle.php.
Public: предоставляет доступ к приложению через index.php.
Src: содержит все контроллеры, модели и сервисы
Var: содержит системные логи и файлы кэша.
Vendor: содержит все внешние пакеты.


Теперь давайте установим некоторые необходимые пакеты с помощью Сomposer:


composer require symfony/orm-packcomposer require sensio/framework-extra-bundle

Мы установили sensio/framework-extra-bundle, который поможет нам упростить код, используя аннотации для определения наших маршрутов.


Нам также необходимо установить symphony/orm-pack для интеграции с Doctrine ORM, чтобы соединиться с базой данных. Ниже приведена конфигурация созданной мной базы данных, которая может быть задана в файле .env.



.env файл конфигурации


Теперь давайте создадим нашу первую сущность. Создайте новый файл с именем Post.php в папке src/Entity.


<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /**  * @ORM\Entity  * @ORM\Table(name="post")  * @ORM\HasLifecycleCallbacks()  */ class Post implements \JsonSerializable {  /**   * @ORM\Column(type="integer")   * @ORM\Id   * @ORM\GeneratedValue(strategy="AUTO")   */  private $id;  /**   * @ORM\Column(type="string", length=100   *   */  private $name;  /**   * @ORM\Column(type="text")   */  private $description;  /**   * @ORM\Column(type="datetime")   */  private $create_date;  /**   * @return mixed   */  public function getId()  {   return $this->id;  }  /**   * @param mixed $id   */  public function setId($id)  {   $this->id = $id;  }  /**   * @return mixed   */  public function getName()  {   return $this->name;  }  /**   * @param mixed $name   */  public function setName($name)  {   $this->name = $name;  }  /**   * @return mixed   */  public function getDescription()  {   return $this->description;  }  /**   * @param mixed $description   */  public function setDescription($description)  {   $this->description = $description;  }  /**   * @return mixed   */  public function getCreateDate(): ?\DateTime  {   return $this->create_date;  }  /**   * @param \DateTime $create_date   * @return Post   */  public function setCreateDate(\DateTime $create_date): self  {   $this->create_date = $create_date;   return $this;  }  /**   * @throws \Exception   * @ORM\PrePersist()   */  public function beforeSave(){   $this->create_date = new \DateTime('now', new \DateTimeZone('Africa/Casablanca'));  }  /**   * Specify data which should be serialized to JSON   * @link https://php.net/manual/en/jsonserializable.jsonserialize.php   * @return mixed data which can be serialized by <b>json_encode</b>,   * which is a value of any type other than a resource.   * @since 5.4.0   */  public function jsonSerialize()  {   return [    "name" => $this->getName(),    "description" => $this->getDescription()   ];  } }

И после этого выполните команду: php bin/console doctrine:schema:create для создания таблицы базы данных в соответствии с нашей сущностью Post.


Теперь давайте создадим PostController.php, куда мы добавим все методы, взаимодействующие с API. Он должен быть помещен в папку src/Controller.


<?php /**  * Created by PhpStorm.  * User: hicham benkachoud  * Date: 02/01/2020  * Time: 22:44  */ namespace App\Controller; use App\Entity\Post; use App\Repository\PostRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; /**  * Class PostController  * @package App\Controller  * @Route("/api", name="post_api")  */ class PostController extends AbstractController {  /**   * @param PostRepository $postRepository   * @return JsonResponse   * @Route("/posts", name="posts", methods={"GET"})   */  public function getPosts(PostRepository $postRepository){   $data = $postRepository->findAll();   return $this->response($data);  }  /**   * @param Request $request   * @param EntityManagerInterface $entityManager   * @param PostRepository $postRepository   * @return JsonResponse   * @throws \Exception   * @Route("/posts", name="posts_add", methods={"POST"})   */  public function addPost(Request $request, EntityManagerInterface $entityManager, PostRepository $postRepository){   try{    $request = $this->transformJsonBody($request);    if (!$request || !$request->get('name') || !$request->request->get('description')){     throw new \Exception();    }    $post = new Post();    $post->setName($request->get('name'));    $post->setDescription($request->get('description'));    $entityManager->persist($post);    $entityManager->flush();    $data = [     'status' => 200,     'success' => "Post added successfully",    ];    return $this->response($data);   }catch (\Exception $e){    $data = [     'status' => 422,     'errors' => "Data no valid",    ];    return $this->response($data, 422);   }  }  /**   * @param PostRepository $postRepository   * @param $id   * @return JsonResponse   * @Route("/posts/{id}", name="posts_get", methods={"GET"})   */  public function getPost(PostRepository $postRepository, $id){   $post = $postRepository->find($id);   if (!$post){    $data = [     'status' => 404,     'errors' => "Post not found",    ];    return $this->response($data, 404);   }   return $this->response($post);  }  /**   * @param Request $request   * @param EntityManagerInterface $entityManager   * @param PostRepository $postRepository   * @param $id   * @return JsonResponse   * @Route("/posts/{id}", name="posts_put", methods={"PUT"})   */  public function updatePost(Request $request, EntityManagerInterface $entityManager, PostRepository $postRepository, $id){   try{    $post = $postRepository->find($id);    if (!$post){     $data = [      'status' => 404,      'errors' => "Post not found",     ];     return $this->response($data, 404);    }    $request = $this->transformJsonBody($request);    if (!$request || !$request->get('name') || !$request->request->get('description')){     throw new \Exception();    }    $post->setName($request->get('name'));    $post->setDescription($request->get('description'));    $entityManager->flush();    $data = [     'status' => 200,     'errors' => "Post updated successfully",    ];    return $this->response($data);   }catch (\Exception $e){    $data = [     'status' => 422,     'errors' => "Data no valid",    ];    return $this->response($data, 422);   }  }  /**   * @param PostRepository $postRepository   * @param $id   * @return JsonResponse   * @Route("/posts/{id}", name="posts_delete", methods={"DELETE"})   */  public function deletePost(EntityManagerInterface $entityManager, PostRepository $postRepository, $id){   $post = $postRepository->find($id);   if (!$post){    $data = [     'status' => 404,     'errors' => "Post not found",    ];    return $this->response($data, 404);   }   $entityManager->remove($post);   $entityManager->flush();   $data = [    'status' => 200,    'errors' => "Post deleted successfully",   ];   return $this->response($data);  }  /**   * Returns a JSON response   *   * @param array $data   * @param $status   * @param array $headers   * @return JsonResponse   */  public function response($data, $status = 200, $headers = [])  {   return new JsonResponse($data, $status, $headers);  }  protected function transformJsonBody(\Symfony\Component\HttpFoundation\Request $request)  {   $data = json_decode($request->getContent(), true);   if ($data === null) {    return $request;   }   $request->request->replace($data);   return $request;  } }

Здесь мы определили пять маршрутов:


GET /api/posts: вернет список постов.



api получения всех постов


POST /api/posts: создаст новый пост.



api добавления нового поста


GET /api/posts/id: вернет пост, соответствующий определенному идентификатору,



получение конкретного поста


PUT /api/posts/id: обновит пост.



обновление поста


Это результат после обновления:



пост после обновления


DELETE /api/posts/id: удалит пост.



удаление поста


Это результат получения всех постов после удаления поста с идентификатором 3:



все посты после удаления


Исходный код можно найти здесь


Заключение


Итак, теперь мы понимаем, что такое REST и Restful. Restful API должен быть без состояний. Мы знаем, как создать Restful-приложение, используя HTTP-глаголы. В общем, теперь мы хорошо понимаем REST и готовы профессионально создавать Restful-приложения.


В следующей статье мы рассмотрим, как обеспечить секьюрность API с помощью JWT аутентификации.




Узнать подробнее о курсе Symfony Framework



Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru