$ composer create-project symfony/skeleton symfony-blog
$ cd symfony-blog
$ composer require annotations orm-pack serializer
$ composer require maker-bundle --dev
# настройка логина, пароля и имя БД
$ nano .env
# настройка версии mysql и кодировки
$ nano config/packages/doctrine.yaml
$ composer require api admin
$ php bin/console make:controller
Choose a name for your controller class (e.g. BravePizzaController):
> HomeController
created: src/Controller/HomeController.php
created: templates/home/index.html.twig
$ nano src/Controller/HomeController.php
# src/Controller/HomeController.php
# меняем роут
/**
* @Route("/", name="app_home")
*/
$ composer require symfony/security-bundle
# создание сущности для пользователей
$ php bin/console make:user
The name of the security user class (e.g. User) [User]:
> User
Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes
Enter a property name that will be the unique "display" name for the user (e.g.
email, username, uuid [email]
> email
Does this app need to hash/check user passwords? (yes/no) [yes]:
> yes
created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml
# миграции БД
$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate
# форма авторизации
$ php bin/console make:auth
What style of authentication do you want? [Empty authenticator]:
[0] Empty authenticator
[1] Login form authenticator
> 1
The class name of the authenticator to create (e.g. AppCustomAuthenticator):
> LoginFormAuthenticator
Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
> SecurityController
Do you want to generate a '/logout' URL? (yes/no) [yes]:
> yes
created: src/Security/LoginFormAuthenticator.php
updated: config/packages/security.yaml
created: src/Controller/SecurityController.php
created: templates/security/login.html.twig
# настройка переадресации при успешной авторизации
# src/Security/LoginFormAuthenticator.php:89
# return new RedirectResponse($this->urlGenerator->generate('app_home'));
$ nano src/Security/LoginFormAuthenticator.php
# Получить хеш строку для пароля:
php bin/console security:encode-password
Добавить в БД в таблицу user
пользователя с полученным хешем. В ячейку user
.roles
вставить ["ROLE_ADMIN"]
$ composer require "lexik/jwt-authentication-bundle"
# создание ключей
# секретная фраза для генерации находится в .env JWT_PASSPHRASE=**********
$ mkdir -p config/jwt
$ openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
$ openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem -pubout
# настройка времени жизни токена
# необходимо добавить token_ttl: 3600, где 3600 - время жизни в секундах
$ nano config/packages/lexik_jwt_authentication.yaml
# настройка безопасности
$ nano config/packages/security.yaml
# config/packages/security.yaml
security:
# ...
firewalls:
login:
pattern: ^/api/login
stateless: true
anonymous: true
json_login:
check_path: /api/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
access_control:
- { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
# настройка роута
$ nano config/routes.yaml
# config/routes.yaml
api_login_check:
path: /api/login_check
$ composer require "gesdinet/jwt-refresh-token-bundle"
# настройка роута
$ nano config/routes.yaml
# config/routes.yaml
gesdinet_jwt_refresh_token:
path: /api/token/refresh
controller: gesdinet.jwtrefreshtoken::refresh
# настройка безопасности
$ nano config/packages/security.yaml
# config/packages/security.yaml
firewalls:
refresh:
pattern: ^/api/token/refresh
stateless: true
anonymous: true
# ...
access_control:
# ...
- { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# ...
# ...
# миграции БД
$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate
# настройка времени жизни refresh токена
$ nano config/packages/gesdinet_jwt_refresh_token.yaml
# config/packages/gesdinet_jwt_refresh_token.yaml
gesdinet_jwt_refresh_token:
ttl: 2592000
$ php bin/console make:entity
После создания создаём и применяем миграции
# миграции БД
$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate
# src/Entity/Article.php
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="articles")
* @ORM\JoinColumn(nullable=false)
*/
private $category;
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(Category $category): self
{
$this->category = $category;
return $this;
}
# src/Entity/Category.php
/**
* @ORM\OneToMany(targetEntity="App\Entity\Article", mappedBy="category")
*/
private $articles;
public function __construct()
{
$this->articles = new ArrayCollection();
}
public function __toString()
{
return $this->name;
}
public function getArticles(): Collection
{
return $this->articles;
}
# миграции БД
$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate
$ nano config/packages/security.yaml
# config/packages/security.yaml
# ...
access_control:
# ...
- { path: ^/admin, roles: ROLE_ADMIN }
# ...
# ...
$ nano config/packages/easy_admin.yaml
# config/packages/easy_admin.yaml
easy_admin:
site_name: "Blog"
user:
display_name: true
display_avatar: true
name_property_path: "name"
avatar_property_path: "avatar"
entities:
# List the entity class name you want to manage
User:
class: App\Entity\User
form:
fields:
- "email"
- "name"
- "password"
Category:
class: App\Entity\Category
form:
fields:
- "name"
- "slug"
Article:
class: App\Entity\Article
form:
fields:
- "category"
- "user"
- "name"
- "slug"
- "content"
- "created_at"
- "is_active"
Comment:
class: App\Entity\Comment
form:
fields:
- "message"
- "created_at"
- "is_active"
$ composer require vich/uploader-bundle
# настройка
$ nano config/packages/vich_uploader.yaml
# config/packages/vich_uploader.yaml
vich_uploader:
db_driver: orm
mappings:
article_image:
uri_prefix: /images
upload_destination: "%kernel.project_dir%/public/images"
namer: Vich\UploaderBundle\Naming\UniqidNamer
avatar_image:
uri_prefix: /images/avatar
upload_destination: "%kernel.project_dir%/public/images/avatar"
namer: Vich\UploaderBundle\Naming\UniqidNamer
# src/Entity/Article
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
# ...
/**
# ...
* @Vich\Uploadable
*/
class Article
{
# ...
/**
* @Vich\UploadableField(mapping="article_image", fileNameProperty="image")
*
* @var File
*/
private $imageFile;
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* @param File|\Symfony\Component\HttpFoundation\File\UploadedFile $imageFile
*/
public function setImageFile(?File $imageFile = null): void
{
$this->imageFile = $imageFile;
// if (null !== $imageFile) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
// $this->updatedAt = new \DateTimeImmutable();
// }
}
public function getImageFile(): ?File
{
return $this->imageFile;
}
}
# config/packages/easy_admin.yaml
easy_admin:
# ...
entities:
# ...
Article:
class: App\Entity\Article
form:
fields:
# ...
- {
property: "imageFile",
type: 'Vich\UploaderBundle\Form\Type\VichFileType',
}
$ mkdir public/uploads
$ composer require artgris/filemanager-bundle
# настройка роута
$ nano config/routes.yaml
# config/routes.yaml
artgris_bundle_file_manager:
resource: "@ArtgrisFileManagerBundle/Controller"
type: annotation
prefix: /manager
# настройка менеджера
$ nano config/packages/config.yml
# config/packages/config.yml
framework:
translator: { fallbacks: [ "en" ] }
artgris_file_manager:
web_dir: public
conf:
default:
dir: "../public/uploads"
# ограничение доступа
# config/packages/security.yaml
# ...
access_control:
# ...
- { path: ^/manager, roles: ROLE_ADMIN }
# ...
# ...
Менеджер доступен по адресу:
/manager/?conf=default
API Platform: Overriding Default Order
Сортировка комментариев по дате публикации
$ nano src/Entity/Comment.php
# src/Entity/Comment.php
/**
* @ApiResource(
* attributes={
* "order"={"created_at": "DESC"}
* },
* .....
* )
*/
API Platform: Subresources
Для получения вложенных элементов через роут /api/articles/{id}/comments
:
# src/Entity/Article.php
# ...
use ApiPlatform\Core\Annotation\ApiSubresource;
# ...
/**
# ...
* @ApiSubresource()
*/
private $comments;
# src/Entity/User
use ApiPlatform\Core\Annotation\ApiResource;
# ...
/**
* @ApiResource(
* itemOperations={
* "get",
* "put"
* },
* collectionOperations={}
* )
# ...
*/
class User implements UserInterface
{
# ...
}
# src/Entity/Category
use ApiPlatform\Core\Annotation\ApiResource;
# ...
/**
* @ApiResource(
* itemOperations={
* "get"={
* "normalization_context"={
* "groups"={"get-category-with-articles"}
* }
* }
* },
* collectionOperations={
* "get"={
* "normalization_context"={
* "groups"={"get-category-with-articles"}
* }
* }
* }
* )
# ...
*/
class Category
{
# ...
}
# src/Entity/Article
use ApiPlatform\Core\Annotation\ApiResource;
# ...
/**
* @ApiResource(
* itemOperations={
* "get"
* },
* collectionOperations={
* "get"
* }
* )
# ...
*/
class Article
{
# ...
}
# src/Entity/Comment
use ApiPlatform\Core\Annotation\ApiResource;
# ...
/**
* @ApiResource(
* attributes={
* "order"={"created_at": "DESC"}
* },
* subresourceOperations={
* "api_articles_comments_get_subresource"={
* "method"="GET",
* "normalization_context"={
* "groups"={"get-comment-with-user"}
* }
* },
* },
* itemOperations={
* "get"
* },
* collectionOperations={
* "post"={
* "denormalization_context"={
* "groups"={"post-new-comment"}
* }
* }
* }
* )
# ...
*/
class Comment
{
# ...
}
В результате имеем следующее:
# src/Entity/Comment
use ApiPlatform\Core\Annotation\ApiResource;
# ...
/**
* @ApiResource(
* attributes={
* "order"={"created_at": "DESC"}
* },
* subresourceOperations={
* "api_articles_comments_get_subresource"={
* "method"="GET",
* "normalization_context"={
* "groups"={"get-comment-with-user"}
* }
* },
* },
* itemOperations={
* "get"
* },
* collectionOperations={
* "post"={
* "normalization_context"={
* "groups"={"get-comment-with-user"}
* },
* "access_control"="is_granted('ROLE_USER')"
* }
* },
* denormalizationContext={
* "groups"={"post"}
* }
* )
# ...
*/
class Comment
{
# ...
}
# src/Entity/User
use ApiPlatform\Core\Annotation\ApiResource;
# ...
/**
* @ApiResource(
* itemOperations={
* "get",
* "put"={"access_control"="is_granted('ROLE_USER') and object == user"}
* },
* collectionOperations={}
* )
# ...
*/
class User implements UserInterface
{
# ...
}
В HomeController добавим экшен:
/**
* @Route("/api/user_current", name="api_user_current")
*/
public function currentUser(): Response
{
$user = $this->get('security.token_storage')->getToken()->getUser();
$response = new Response(
json_encode([
'id' => $user->getId(),
'name' => $user->getName(),
'avatar' => $user->getAvatar()
]),
Response::HTTP_OK,
['content-type' => 'application/json']
);
return $response;
}
Для получения информации о текущем пользователе необходимо перейти по /api/user_current с токеном в заголовках